美文网首页
Kotlin在Fragment中监听手势并转场

Kotlin在Fragment中监听手势并转场

作者: s1991721 | 来源:发表于2017-12-28 22:40 被阅读0次

    引言


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


    预览

    解析布局:
    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点处结束即屏幕最左边
    

    相关文章

      网友评论

          本文标题:Kotlin在Fragment中监听手势并转场

          本文链接:https://www.haomeiwen.com/subject/foxygxtx.html