仿MIUI彈性列表

前言

最近去小米之家體驗了下小米9,發現MIUI有一個挺特別的列表動畫效果,在系統上的各類應用上都能見到它的身影。api

網上查了下,小米早在幾個系統版本前就有這個,網上也有了實現這個效果的控件庫。實現方法大同小異,大多都是經過繼承 ScrollView,而後重寫 onInterceptTouchEvent方法和 OnTouchEvent方法,計算手指滑動距離來縮放內部控件。

這種方式適合對View觸摸分發機制比較熟悉的同窗,代碼比較複雜,看了下現有的庫也都沒能實現MIUI中Fling狀態的彈性效果。正好最近看了下NestedScrolling的相關知識,發現能很好地實現這些效果,因此就讓咱們來看看吧。bash

預備知識

須要先了解下NestedScrollChildNestedScrollParent,所謂的NestedScrolling機制是這樣的:內部NestedScrollingChild在滾動的時候,預先將dx,dy經過NestedScrollingChildHelper傳遞給NestedScrollingParentNestedScrollingParent可先對其進行部分消耗,Parent處理完後,再將剩餘的部分還給內部NestedScrollingChild處理,最後再把剩下的dx,dy再給Parent作最後處理,這樣一次觸摸滑動事件將能夠由多個控件共同消耗處理,這樣就能夠很方便解決以前一次觸摸滑動事件只能被一個控件響應而產生的嵌套滑動問題。ide

先看下NestedScrollParent動畫

public interface NestedScrollingParent {
 
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
 
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
 
    public void onStopNestedScroll(View target);
    
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);
    
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
 
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
 
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
 
    public int getNestedScrollAxes();
}
複製代碼

先看下NestedScrollingChildui

public interface NestedScrollingChild {
  ​
      void setNestedScrollingEnabled(boolean enabled);
  ​
      boolean isNestedScrollingEnabled();
  ​
      boolean startNestedScroll(int axes);
  ​
      void stopNestedScroll();
  ​
      boolean hasNestedScrollingParent();
  ​
      boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
              int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
  ​
      boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
      
      boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
     
      boolean dispatchNestedPreFling(float velocityX, float velocityY);
  }
複製代碼

能夠看到parent和child的api命名很相似,是成對出現的,確實,它們以前存在發起方和接收方的事件調用關係,都是由child先響應滑動觸摸實現,經過NestedScrollingChildHelper分發給parent。this

彈性列表實現

爲方便解析,咱們先只實現下滑的彈性動畫:spa

//子view,需事先NestedScrollingChild
    private var childView: View? = null

    private val mNestedScrollingParentHelper: NestedScrollingParentHelper

    private var offsetScale = 0f
    private var flingScale = 0f
    private var consumedDy = 0
        set(value) {
            field = if (value > 0) {
                0
            } else {
                value
            }
        }
    //是不是Fling滑動
    private var filing = false
    //斷定滑動的最小距離
    private var touchSlop: Int = 0

    private var animator: ValueAnimator? = null


    init {
        mNestedScrollingParentHelper = NestedScrollingParentHelper(this)
        touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    }


    override fun onFinishInflate() {
        super.onFinishInflate()
        childView = getChildAt(0)
    }

    /** * 滾動開始 */
    override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {
        filing = false
        consumedDy = 0
        return child === childView && ViewCompat.SCROLL_AXIS_VERTICAL == nestedScrollAxes
    }


    /** * 先於child滾動 */
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
        if (childView!!.scrollY == 0 && (dy < 0 || consumedDy < 0)) {
            consumedDy += dy
            if (Math.abs(consumedDy) > touchSlop) {
                //計算縮放值,最大放大1.3倍
                offsetScale = (1.3 - 600f / (2000 + Math.pow(Math.abs(consumedDy).toDouble(), 2.0))).toFloat()
                startBouncingTop()
                //存放消耗的距離,child會接收
                consumed[1] = dy
            }
        }
    }

    override fun onNestedScrollAccepted(child: View, target: View, nestedScrollAxes: Int) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes)
    }

    /** * 先於child處理Fling */
    override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        if (velocityY < 0 && childView!!.scrollY == 0) {
            filing = true
            consumedDy = (consumedDy + velocityY).toInt()
            flingScale = (0.3 - 600f / (2000 + Math.pow(Math.abs(consumedDy).toDouble(), 2.0))).toFloat()
            return true
        }
        return false
    }

    override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        return filing
    }

    override fun onStopNestedScroll(target: View) {
        mNestedScrollingParentHelper.onStopNestedScroll(target)
        backBouncing(filing)
    }

    override fun getNestedScrollAxes(): Int {
        return mNestedScrollingParentHelper.nestedScrollAxes
    }

    /** * 進行回彈 */
    private fun backBouncing(filing: Boolean) {
        //初始化
        if (animator != null && animator!!.isRunning) {
            animator!!.cancel()
            animator = null
        }
        if (filing) {
            animator = ValueAnimator.ofFloat(offsetScale, flingScale, 0f)
            animator!!.duration = 400
        } else {
            animator = ValueAnimator.ofFloat(offsetScale, 0f)
            animator!!.duration = 250
        }
        animator!!.interpolator = OvershootInterpolator()
        animator!!.addUpdateListener {
            offsetScale = it.animatedValue as Float
            startBouncingTop()
        }
        animator!!.start()
    }

    /** * 從頂部開始滑動 */
    private fun startBouncingTop() {
        childView!!.pivotY = 0f
        childView!!.pivotX = 0f
        childView!!.scaleY = offsetScale
    }
複製代碼

彈性效果 .net

Fling彈性效果

參考文章

Android 8.0 NestedScrollingChild2與NestedScrollingParent2實現RecyclerView阻尼回彈效果3d

相關文章
相關標籤/搜索