探索EdgeEffect的花样玩法

1、EdgeEffect是什么

当用户在一个可滑动的控件内(如RecyclerView),滑动内容已经超过了内容鸿沟时,RecyclerView经过EdgeEffect制作一个鸿沟图形来提示用户,滑动已经到鸿沟了,不要再滑动啦。

简言之:便是经过鸿沟图形来提示用户,没啥内容了,别滑了。

2、EdgeEffect在RecyclerView的现象是什么

1、抵达鸿沟后的暗影作用

在RecyclerView列表中,滑动到鸿沟还持续滑动或许快速滑动到鸿沟,则现象如下图中的抵达鸿沟后产生的暗影作用。

基于EdgeEffect实现RecyclerView列表阻尼滑动效果

2、怎么去掉暗影作用

在布局中,能够设置overScrollMode的特点值为never即可。

或许在代码中设置,即可撤销

recyclerView?.overScrollMode = View.OVER_SCROLL_NEVER

3、EdgeEffect在RecyclerView的完成原理是什么

1、onMove事情对应EdgeEffect的onPull

EdgeEffect在RecyclerView中大致流程能够参考下面这个图,以onMove事情举例

基于EdgeEffect实现RecyclerView列表阻尼滑动效果

经过上面这个图,并结合下面的源码,就能对这个流程有个大致的了解。

@Override
public boolean onTouchEvent(MotionEvent e) {
    ...
    switch (action) {
       ...
        case MotionEvent.ACTION_MOVE: {
          ...
          // (1) move事情
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e, TYPE_TOUCH)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
               ...
            }
        }
        break;
     }
 }
boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
  ...
     // (2)判别是否设置了过度滑动,所以经过布局设置overScrollMode的特点值为never就走不进了分支逻辑中了
    if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
        if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
            pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
        }
        considerReleasingGlowsOnScroll(x, y);
    }
   ...
    if (!awakenScrollBars()) {
        // 改写当时界面
        invalidate();
    }
    return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
private void pullGlows(float x, float overscrollX, float y, float overscrollY) {
    boolean invalidate = false;
   ...
   // 顶部鸿沟
    if (overscrollY < 0) {
        // 构建顶部鸿沟的EdgeEffect目标
        ensureTopGlow();
        // 调用EdgeEffect的onPull办法 设置些特点
        EdgeEffectCompat.onPull(mTopGlow, -overscrollY / getHeight(), x / getWidth());
        invalidate = true;
    } 
    ...
    if (invalidate || overscrollX != 0 || overscrollY != 0) {
        // 改写界面
        ViewCompat.postInvalidateOnAnimation(this);
    }
}
void ensureTopGlow() {
   ...
    mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP);
    // 设置鸿沟图形的巨细
    if (mClipToPadding) {
        mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
    } else {
        mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
    }
}
// RecyclerView的制作
@Override
public void draw(Canvas c) {
    super.draw(c);
    ...
    if (mTopGlow != null && !mTopGlow.isFinished()) {
        final int restore = c.save();
        if (mClipToPadding) {
            c.translate(getPaddingLeft(), getPaddingTop());
        }
        // 调用 EdgeEffect的draw办法
        needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
        c.restoreToCount(restore);
    }
    ...
}
// EdgeEffect的draw办法
public boolean draw(Canvas canvas) {
   ...
    update();
    final int count = canvas.save();
    final float centerX = mBounds.centerX();
    final float centerY = mBounds.height() - mRadius;
    canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0);
    final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f;
    float translateX = mBounds.width() * displacement / 2;
    canvas.clipRect(mBounds);
    canvas.translate(translateX, 0);
    mPaint.setAlpha((int) (0xff * mGlowAlpha));
    // 制作扇弧
    canvas.drawCircle(centerX, centerY, mRadius, mPaint);
    canvas.restoreToCount(count);
        ...

同理:RecyclerView的 up 及Cancel事情对应调用EdgeEffect的onRelease;fling过度滑动对应EdgeEffect的onAbsorb办法

2、EdgeEffect的onPull、onRelease、onAbsorb办法

(1)onPull

关于RecyclerView列表而言,内容已经在顶部抵达鸿沟了,此时用户仍向下滑动时,会调用onPull办法及后续流畅,来更新当时视图,提示用户已经到鸿沟了。

(2)onRelease

关于(1)的状况,用户松开了,不向下滑动了,此时释放拉动的间隔,并改写界面消失当时的图形界面。

(3)onAbsorb

用户过度滑动时,RecyclerView调用Fling办法,把内容抵达鸿沟后耗费不掉的间隔传递给onAbsorb办法,让其显现图形界面提示用户已抵达内容鸿沟。

4、运用EdgeEffect在RecyclerView中完成列表阻尼滑动等作用

(1)先看下作用

基于EdgeEffect实现RecyclerView列表阻尼滑动效果

上述gif图中展现了两个作用:RecyclerView的阻尼下拉 及 复位,这便是运用上面的EdgeEffect的三个办法能够完成。

上述的gif图中,运用MultiTypeAdapter完成RecyclerView的多类型页面(ViewModel、json数据源),能够参考这篇文章快速写个RecyclerView的多类型页面

下面首要展现怎么构建一个EdgeEffect,充分地运用onPull、onRelease及onAbsorb才能

(2)代码暗示

// 构建一个自定义的EdgeEffectFactory 并设置给RecyclerView
recyclerView?.edgeEffectFactory = SpringEdgeEffect()
// SpringEdgeEffect
class SpringEdgeEffect : RecyclerView\.EdgeEffectFactory() {
        override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect {
            return object : EdgeEffect(recyclerView.context) {
                override fun onPull(deltaDistance: Float) {
                    super.onPull(deltaDistance)
                    handlePull(deltaDistance)
                }
                override fun onPull(deltaDistance: Float, displacement: Float) {
                    super.onPull(deltaDistance, displacement)
                    handlePull(deltaDistance)
                }
                private fun handlePull(deltaDistance: Float) {
                    val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
                    val translationYDelta =
                        sign * recyclerView.width * deltaDistance * 0.8f
                    Log.d("qlli1234-pull", "deltDistance: " + translationYDelta)
                    recyclerView.forEach {
                        if (it.isVisible) {
                        // 设置每个RecyclerView的子item的translationY特点
                            recyclerView.getChildViewHolder(it).itemView.translationY += translationYDelta
                        }
                    }
                }
                override fun onRelease() {
                    super.onRelease()
                    Log.d("qlli1234-onRelease", "onRelease")
                    recyclerView.forEach {
                        //复位
                        val animator = ValueAnimator.ofFloat(recyclerView.getChildViewHolder(it).itemView.translationY, 0f).setDuration(500)
                        animator.interpolator = DecelerateInterpolator(2.0f)
                        animator.addUpdateListener { valueAnimator ->
                            recyclerView.getChildViewHolder(it).itemView.translationY = valueAnimator.animatedValue as Float
                        }
                        animator.start()
                    }
                }
                override fun onAbsorb(velocity: Int) {
                    super.onAbsorb(velocity)
                    val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
                    Log.d("qlli1234-onAbsorb", "onAbsorb")
                    val translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE
                    recyclerView.forEach {
                        if (it.isVisible) {
                          // 在这个能够做动画
                        }
                    }
                }
                override fun draw(canvas: Canvas?): Boolean {
                    // 设置巨细之后,就不会有绘画暗影作用
                    setSize(0, 0)
                    val result =  super.draw(canvas)
                    return result
                }
            }
    }

这里有一个小细节,怎么在运用onPull等办法时,去掉制作的暗影部分:其实,能够重写draw办法,重置巨细为0即可,如上述代码中的这一小块内容:

override fun draw(canvas: Canvas?): Boolean {
    // 设置巨细之后,就不会有绘画暗影作用
    setSize(0, 0)
    val result =  super.draw(canvas)
    return result
}

5、参考

1、google的motion示例中的ChessAdapter内容

2、仿QQ的recyclerview作用完成