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點處結束即屏幕最左邊
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章