Kotlin在Fragment中监听手势并转场

引言


先看以下将要实现目标的效果


预览

解析布局:
1、启动页由于类型不同,因此选用fragment显示
2、fragment根布局采用的VideoViewIjk
3、底部闪烁的上三角MotionalArrowView
4、指示器-IndicatorView
5、幕布式TextView-CurtainTextView

3、4、5都是由RelativeLayout包裹

整个页面能够识别左右上三个方向的手势,根据滑动的方向选用不同的转场动画。

仔细观察的人是否能够察觉在第一页左滑时与原作的不同呢?这是因为原作中使用了ViewPager(嘻嘻别问我怎么知道的),接下来开始讲述编码历程。

正文


顺序按交互与否排序,IndicatorView和CurtainTextView属于有用户交互,MotionalArrowView则没有,最后是交互的实现GestureDetector

  • MotionalArrowView

实现思路是自定义VireGroup将两个三角形上下摆放,设置属性动画改变其透明度。

中途遇到的坑:由于图素选取时尺寸大于控件显示的尺寸,导致了自定义控件内部ImageView不按约束显示,所以在使用此控件时要将其设置成宽小于高的矩形。

    fun initView() {
        upImageView = ImageView(context)
        downImageView = ImageView(context)

        upImageView.setImageDrawable(ContextCompat.getDrawable(context, R.mipmap.ic_action_up))
        downImageView.setImageDrawable(ContextCompat.getDrawable(context, R.mipmap.ic_action_up))

        //如果是正方形,则看不出效果,因为图片太大了
        var params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        params.addRule(ALIGN_PARENT_BOTTOM)

        addView(upImageView)
        addView(downImageView, params)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        if (!isInEditMode) {
            showAnimation()
        }
    }

    fun showAnimation() {
        var upAnimator = ObjectAnimator.ofFloat(upImageView, "alpha", 0.3f, 1f, 0.3f)
        var downAnimator = ObjectAnimator.ofFloat(downImageView, "alpha", 0.3f, 1f, 0.3f)
        upAnimator.duration = 1000
        downAnimator.duration = 1000
        upAnimator.startDelay = 500

        var animatorSet = AnimatorSet()
        animatorSet.playTogether(upAnimator, downAnimator)
        animatorSet.addListener(object : Animator.AnimatorListener {

            override fun onAnimationEnd(animation: Animator?) {
                animatorSet.startDelay = 500
                animatorSet.start()
            }

            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {
            }
        })
        animatorSet.start()
    }
  • IndicatorView

这个就比较简单了,用LinearLayout包裹ImageView,切换时更换ImageView的Drawable。

这里踩了一个kotlin的坑:在typedArray.getDrawable()时,如果控件并没有设置此属性而是采用默认值

        //定义
        private var normalBG: Drawable

        //如果这么写
        normalBG = typedArray.getDrawable(R.styleable.IndicatorView_indicatorView_normal)
        if (normalBG == null) {
            normalBG = ContextCompat.getDrawable(context, R.mipmap.ic_indicator_normal)
        }

        //结果
Caused by: java.lang.IllegalStateException: typedArray.getDrawable(R…iew_indicatorView_normal) must not be null

因为定义normalBG时认定不为空,所以当typedArray.getDrawable()取空值时报异常

如果定义其为private var normalBG: Drawable?则不报异常

因为我定义的normalBG有默认值,肯定不为空所以改了如下写法(究其原因还是kotlin对于空指针异常的把控,再加上自己kotlin写法的不熟练)

    init {
        gravity = Gravity.CENTER
        var typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndicatorView)
        contentMargin = typedArray.getDimensionPixelSize(R.styleable.IndicatorView_indicatorView_margin, 15)

        var tempBG = typedArray.getDrawable(R.styleable.IndicatorView_indicatorView_normal)
        if (tempBG != null) {
            normalBG = tempBG
        } else {
            normalBG = ContextCompat.getDrawable(context, R.mipmap.ic_indicator_normal)
        }
        tempBG = typedArray.getDrawable(R.styleable.IndicatorView_indicatorView_checked)
        if (tempBG != null) {
            selectBG = tempBG
        } else {
            selectBG = ContextCompat.getDrawable(context, R.mipmap.ic_indicator_selected)
        }

        setSize(typedArray.getInt(R.styleable.IndicatorView_indicatorView_count, 0))
        typedArray.recycle()
    }

    fun setSize(size: Int) {
        removeAllViews()
        for (i in 0 until size) {
            var imageView = ImageView(context)
            var params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
            params.leftMargin = contentMargin
            imageView.scaleType = ImageView.ScaleType.CENTER
            if (i == 0) {
                imageView.setImageDrawable(selectBG)
            } else {
                imageView.setImageDrawable(normalBG)
            }
            addView(imageView, params)
        }
    }

    fun select(position: Int) {
        if (position < childCount) {
            for (i in 0 until childCount) {
                var imageView: ImageView = getChildAt(i) as ImageView
                if (position == i) {
                    imageView.setImageDrawable(selectBG)
                } else {
                    imageView.setImageDrawable(normalBG)
                }
            }
        }
    }
  • CurtainTextView

这个就比较叼了!最开始我自定义了TypeTextView控件,通过ValueAnimator.ofInt(0, content.length)不断setText,能够实现动态打字的效果,但其并不能达到预期的动画效果。因为每一次的setText,TextView本身都要重新测算一下自身,结果就像是一个不断变长的矩形。

而我想要的则是像将矩形上的遮布逐渐揭开的效果。

这让我想到了之前有一篇介绍Span的文章文中虽然效果图和代码并不完全匹配,细读一下代码还是很有帮助的。于是有了一下代码

    init {
        animator = ObjectAnimator.ofFloat(this, "textAlpha", 0f, 1f)
        animator.duration = 1000
        animator.addUpdateListener { animation -> text = spannableString }
    }

    fun setContentText(string: String) {
        spannableString = SpannableString(string)
        spanList = ArrayList()
        for (i in 0 until string.length) {
            var span = MutableForegroundColorSpan()
            spanList.add(span)
            spannableString.setSpan(span, i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }
        animator.start()
    }


    class MutableForegroundColorSpan : CharacterStyle(), UpdateAppearance {
        var alpha = 0

        override fun updateDrawState(tp: TextPaint) {
            tp.alpha = alpha
        }

    }

    fun setTextAlpha(alpha: Float) {
        var size = spanList.size
        var total = size * alpha
        for (i in 0 until size) {
            var span = spanList.get(i)
            if (total >= 1) {
                span.alpha = 255
                --total
            } else {
                span.alpha = (255 * total).toInt()
                total = 0f
            }
        }
    }

其原理是将要设置的文字全部拆成字符,并对每个字符设置CharacterStyle,通过ObjectAnimator改变每个字符CharacterStyle的透明度。效果就像是原本一行透明的文字逐渐地从第一个字符慢慢显示出来

  • GestureDetector

终于到了文章标题的主旨,由于在fragment中无法重写onTouchEvent所以将重任交给了宿主Activity。

(其实也可以将GestureDetector放到布局中的View上,由于kotlin还是不太顺手所以一直都报View的空指针,现在想想应该是调用的时间不对,无法在onCreate和onCreateView附近的生命周期调用)

        gestureDetector = GestureDetector(activity, object : GestureDetector.OnGestureListener {
            override fun onShowPress(e: MotionEvent?) {
            }

            override fun onSingleTapUp(e: MotionEvent?): Boolean {
                return false
            }

            override fun onDown(e: MotionEvent?): Boolean {
                return false
            }

            override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
                var yDifference = e2.y - e1.y
                var xDifference = e2.x - e1.x
                if (Math.abs(xDifference) > Math.abs(yDifference)) {//横向
                    if (xDifference > 0) {//right
                        setPosition(--currentPosition)
                    } else {
                        setPosition(++currentPosition)
                    }
                } else {//纵向
                    if (yDifference > 0) {//down


                    } else {
                        goMainLeft(false)
                    }
                }

                return true
            }

            override fun onLongPress(e: MotionEvent?) {
            }

            override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
                return false
            }
        })

        //交接重任
        (activity as SplashActivity).gestureDetector = gestureDetector

关键方法是onFling()其中参数e1、e2分别代表滑动的起始点和结束点。以手机屏幕左上角为原点,向右x轴逐渐增加,向下y轴逐渐增加,以此为依据,y值相同时e2.x > e1.x表示右滑、x值相同时e2.y > e1.y表示下滑

    //Activity
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (gestureDetector != null) {
            return gestureDetector.onTouchEvent(event)
        }
        return super.onTouchEvent(event)
    }

至此手势已经获取到了,转场的代码与java并无二致

//由于不会用到退场动画,所以就一样了
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_in_right)
//R.anim.slide_in_right
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_mediumAnimTime"
    android:fromXDelta="50.0%p"//x轴在屏幕50%的地方开始 p代表parent
    android:interpolator="@android:anim/decelerate_interpolator"
    android:toXDelta="0.0" />//在x轴0点处结束即屏幕最左边
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,190评论 25 707
  • 效果图: Github链接:https://github.com/boycy815/PinchImageView ...
    CQ_TYL阅读 2,221评论 0 0
  • 原文地址:http://www.android100.org/html/201606/06/241682.html...
    AFinalStone阅读 930评论 0 1
  • 一、概述当用户触摸屏幕的时候,会产生许多手势,例如down,up,scroll,filing等等。一般情况下,我们...
    GB_speak阅读 20,710评论 1 18
  • 那一年 她卸下牙套后就没有笑过了 本不该这么美的 现在想想还心有余悸 就像最黑暗的路 人们看不见前方 人们磕磕碰碰...
    留子尧阅读 196评论 1 5