探索EdgeEffect的花样玩法
1、EdgeEffect是什么
当用户在一个可滑动的控件内(如RecyclerView),滑动内容已经超过了内容鸿沟时,RecyclerView经过EdgeEffect制作一个鸿沟图形来提示用户,滑动已经到鸿沟了,不要再滑动啦。
简言之:便是经过鸿沟图形来提示用户,没啥内容了,别滑了。
2、EdgeEffect在RecyclerView的现象是什么
1、抵达鸿沟后的暗影作用
在RecyclerView列表中,滑动到鸿沟还持续滑动或许快速滑动到鸿沟,则现象如下图中的抵达鸿沟后产生的暗影作用。
2、怎么去掉暗影作用
在布局中,能够设置overScrollMode的特点值为never即可。
或许在代码中设置,即可撤销
recyclerView?.overScrollMode = View.OVER_SCROLL_NEVER
3、EdgeEffect在RecyclerView的完成原理是什么
1、onMove事情对应EdgeEffect的onPull
EdgeEffect在RecyclerView中大致流程能够参考下面这个图,以onMove事情举例
经过上面这个图,并结合下面的源码,就能对这个流程有个大致的了解。
@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)先看下作用
上述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
}