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
//在x轴0点处结束即屏幕最左边

你可能感兴趣的:(Kotlin在Fragment中监听手势并转场)