前言
最近去小米之家體驗了下小米9,發現MIUI有一個挺特別的列表動畫效果,在系統上的各種應用上都能見到它的身影。
ScrollView
,然后重寫onInterceptTouchEvent
方法和OnTouchEvent
方法,計算手指滑動距離來縮放內部控件。 這種方式適合對View觸摸分發機制比較熟悉的同學,代碼比較復雜,看了下現有的庫也都沒能實現MIUI中Fling狀態的彈性效果。正好最近看了下NestedScrolling
的相關知識,發現能很好地實現這些效果,所以就讓我們來看看吧。
預備知識
需要先了解下NestedScrollChild
和NestedScrollParent
,所謂的NestedScrolling機制是這樣的:內部NestedScrollingChild在滾動的時候,預先將dx,dy通過NestedScrollingChildHelper
傳遞給NestedScrollingParent
,NestedScrollingParent
可先對其進行部分消耗,Parent處理完后,再將剩余的部分還給內部NestedScrollingChild
處理,最后再把剩下的dx,dy再給Parent做最后處理,這樣一次觸摸滑動事件將可以由多個控件共同消耗處理,這樣就可以很方便解決之前一次觸摸滑動事件只能被一個控件響應而產生的嵌套滑動問題。
先看下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();
}
復制代碼
先看下NestedScrollingChild
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。
彈性列表實現
為方便解析,我們先只實現下滑的彈性動畫:
//子view,需事先NestedScrollingChildprivate var childView: View? = nullprivate val mNestedScrollingParentHelper: NestedScrollingParentHelperprivate var offsetScale = 0fprivate var flingScale = 0fprivate var consumedDy = 0set(value) {field = if (value > 0) {0} else {value}}//是否是Fling滑動private var filing = false//判定滑動的最小距離private var touchSlop: Int = 0private var animator: ValueAnimator? = nullinit {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 = falseconsumedDy = 0return 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 += dyif (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 = trueconsumedDy = (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 FloatstartBouncingTop()}animator!!.start()}/*** 從頂部開始滑動*/private fun startBouncingTop() {childView!!.pivotY = 0fchildView!!.pivotX = 0fchildView!!.scaleY = offsetScale}
復制代碼
彈性效果
參考文章
Android 8.0 NestedScrollingChild2與NestedScrollingParent2實現RecyclerView阻尼回彈效果