Android 7 种方式实现自定义ViewGroup的滚动与惯性滚动
本文作者
作者:Newki
链接:
https://juejin.cn/post/7311602485865496586
本文由作者授权发布。
前言
之前的文章在讲完 ViewGroup 的布局与测量之后直接上实战了,其实并没有细说到 ViewGroup 的滚动和一些触摸的细节问题,今天我补上准备单独讲一讲。有兴趣可以查看之前的对应专栏【传送门】
https://juejin.cn/column/7170970245723586574
例如本文的示例,就是类似 ScrollView 与 ViewPager 的那种效果所以我选用 scrollTo 的方式,而在之前的文章【Android自定义ViewGroup嵌套与交互实战,幕布全屏滚动效果】 中,由于它内部还有其他的动画如缩放平移等,所以我选择使用 setTranslationX / setTranslationY 的方式来处理会更加的方便。
https://juejin.cn/post/7189473736729788473
话不多说,Let's go
public class ViewGroup7 extends ViewGroup implements View.OnTouchListener {
private int mScreenHeight;
private int mLastY;
private int mStart;
private int mEnd;
private Scroller mScroller;
public ViewGroup7(Context context) {
this(context, null);
}
public ViewGroup7(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewGroup7(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScreenHeight = ScreenUtils.getScreenHeith(context);
mScroller = new Scroller(context);
setOnTouchListener(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
//设置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
child.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
}
}
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
//当停止动画的时候,它会马上滚动到终点,然后向动画设置为结束。
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
int dy = mLastY - y;
if (getScrollY() < 0) {
dy = 0;
}
//开始滚动
scrollBy(0, dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if (dScrollY > 0) {
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
} else {
mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
}
} else {
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
} else {
mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
}
}
invalidate();
break;
}
return true;
}
}
所以说其实 Scroller 的 startScroll 方法本身也只是计算并赋值了当前的 CurrY 中,具体的滚动方法还是要重写 computeScroll 在内部取出 Scroller 赋值的 CurrY 然后再通过 scrollTo / scrollBy 的方式实现具体的滚动,通过不停的刷新从而视觉看起来滚动了。
public class ViewGroup7 extends LinearLayout {
private int mScreenHeight;
private GestureDetector mGestureDetector;
private Scroller mScroller;
private int mStart;
private int mEnd;
//...
@SuppressLint("ClickableViewAccessibility")
public ViewGroup7(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mStart = getScrollY();
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//如果是按下抬起,可以触发 onSingleTapUp ,但是如果是按下滑动,则不会触发
doEventUp();
return true;
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 处理滑动事件
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
scrollBy(0, (int) distanceY);
return true;
}
});
setFocusable(true);
setClickable(true);
setEnabled(true);
setLongClickable(true);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) { // 检查是否滚动操作完成
scrollTo(0, mScroller.getCurrY());
if (mScroller.getCurrX() == getScrollX() && mScroller.getCurrY() == getScrollY()) {
postInvalidate();
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mGestureDetector.onTouchEvent(event); //全部事件都交给 GestureDetector 处理
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// 手指抬起时并且 GestureDetector 没有触发onSingleTapUp, 那么你可以在这里处理
doEventUp();
}
return true;
}
return result;
}
private void doEventUp() {
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if (dScrollY > 0) {
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
} else {
mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
}
} else {
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
} else {
mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
}
}
invalidate(); // 必须调用此方法来触发重绘
}
}
需要注意的是 GestureDetector 中的 Up 事件,只有点击的时候抬起手指才会触发,而滚动结束之后抬起手指是不会触发的,所以我们在 onTouchEvent 的时候单独处理了手指抬起的事件,做了兼容处理。
public class ViewGroup7 extends LinearLayout {
private int mScreenHeight;
private GestureDetector mGestureDetector;
private ValueAnimator mValueAnimator;
private int mStart;
private int mEnd;
//...
@SuppressLint("ClickableViewAccessibility")
public ViewGroup7(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mStart = getScrollY();
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//如果是按下抬起,可以触发 onSingleTapUp ,但是如果是按下滑动,则不会触发
doEventUp();
return true;
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 处理滑动事件
if (mValueAnimator != null && mValueAnimator.isRunning()) {
mValueAnimator.cancel();
}
scrollBy(0, (int) distanceY);
return true;
}
});
setFocusable(true);
setClickable(true);
setEnabled(true);
setLongClickable(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mGestureDetector.onTouchEvent(event); //全部事件都交给 GestureDetector 处理
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// 手指抬起时并且 GestureDetector 没有触发onSingleTapUp, 那么你可以在这里处理
doEventUp();
}
return true;
}
return result;
}
private void doEventUp() {
if (mValueAnimator != null && mValueAnimator.isRunning()) {
mValueAnimator.cancel();
}
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
YYLogUtils.w("doEventUp执行了 dScrollY:" + dScrollY);
int startY = getScrollY();
int endY = dScrollY > 0 ?
(dScrollY < mScreenHeight / 3 ? startY - dScrollY : startY + mScreenHeight - dScrollY) :
(Math.abs(dScrollY) < mScreenHeight / 3 ? startY - dScrollY : startY - mScreenHeight - dScrollY);
startAnim(startY, endY);
}
private void startAnim(int startY, int endY) {
mValueAnimator = ValueAnimator.ofInt(startY, endY);
mValueAnimator.setDuration(250); // 动画执行时间可以根据需要进行调整
mValueAnimator.setInterpolator(new DecelerateInterpolator()); // 设置动画插值器
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
scrollTo(0, (Integer) animation.getAnimatedValue());
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 在动画结束时执行需要的操作,例如校正位置确保对齐
YYLogUtils.w("动画执行结束");
scrollTo(0, endY);
}
});
mValueAnimator.start();
}
}
效果如上图,也可以让内部布局跟随手指滚动。
public class ViewGroup7 extends LinearLayout {
private int mScreenHeight;
private GestureDetector mGestureDetector;
private ValueAnimator mValueAnimator;
private int mStart;
private int mEnd;
private int mCurrY;
// ...
@SuppressLint("ClickableViewAccessibility")
public ViewGroup7(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mStart = getScrollY();
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//如果是按下抬起,可以触发 onSingleTapUp ,但是如果是按下滑动,则不会触发
doEventUp();
return true;
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 处理滑动事件
if (mValueAnimator != null && mValueAnimator.isRunning()) {
mValueAnimator.cancel();
}
scrollBy(0, (int) distanceY);
return true;
}
});
setFocusable(true);
setClickable(true);
setEnabled(true);
setLongClickable(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mGestureDetector.onTouchEvent(event); //全部事件都交给 GestureDetector 处理
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// 手指抬起时并且 GestureDetector 没有触发onSingleTapUp, 那么你可以在这里处理
doEventUp();
}
return true;
}
return result;
}
private void doEventUp() {
if (mValueAnimator != null && mValueAnimator.isRunning()) {
mValueAnimator.cancel();
}
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
YYLogUtils.w("doEventUp执行了 dScrollY:" + dScrollY);
int startY = getScrollY();
int endY = dScrollY > 0 ?
(dScrollY < mScreenHeight / 3 ? startY - dScrollY : startY + mScreenHeight - dScrollY) :
(Math.abs(dScrollY) < mScreenHeight / 3 ? startY - dScrollY : startY - mScreenHeight - dScrollY);
startAnim(startY, endY);
}
private void startAnim(int startY, int endY) {
mValueAnimator = ValueAnimator.ofInt(startY, endY);
mValueAnimator.setDuration(250); // 动画执行时间可以根据需要进行调整
mValueAnimator.setInterpolator(new DecelerateInterpolator()); // 设置动画插值器
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrY = (Integer) animation.getAnimatedValue();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
invalidate();
}
@Override
public void onAnimationEnd(Animator animation) {
// 在动画结束时执行需要的操作,例如校正位置确保对齐
scrollTo(0, endY);
}
});
mValueAnimator.start();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mValueAnimator != null && mValueAnimator.isRunning()) {
scrollTo(0, mCurrY);
postInvalidate();
}
}
}
效果也是如上图所示,子布局跟随手指滚动。这么多方式都能实现滚动,那么如果我想做惯性的滚动是不是可以用同样的这么多方式实现呢?又通过哪些方式可以实现惯性的滚动呢?让我们带着问题往下看。
public class ViewGroup7 extends ViewGroup implements View.OnTouchListener {
private int mScreenHeight;
private int mLastY;
private int mStart;
private int mEnd;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
// ...
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
// 真正fling滚动中
scrollTo(0, mScroller.getCurrY());
postInvalidate();
} else if (mScroller.isFinished()) {
// 当fling操作完成后的逻辑
adjustToEndPosition();
}
}
private void adjustToEndPosition() {
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if (dScrollY == 0 || Math.abs(dScrollY) == mScreenHeight) return;
YYLogUtils.w("computeScroll mScroller.isFinished dScrollY:" + dScrollY);
// 接下来你可以判断滚动接近哪个屏幕并滚动到那里
int finalY;
if (dScrollY > 0) {
if (dScrollY < mScreenHeight / 3) {
finalY = mEnd - dScrollY;
} else {
finalY = mEnd + mScreenHeight - dScrollY;
}
} else {
if (-dScrollY < mScreenHeight / 3) {
finalY = mEnd - dScrollY;
} else {
finalY = mEnd - mScreenHeight - dScrollY;
}
}
// 这里需要确保finalY不会超出你的视图界限,比如不要小于0或大于最大滚动高度
finalY = Math.max(0, Math.min(finalY, mScreenHeight * (getChildCount() - 1)));
mScroller.startScroll(0, mEnd, 0, finalY - mEnd);
postInvalidate();
}
@Override
public boolean onTouch(View v, MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 初始化 VelocityTracker
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
mVelocityTracker.addMovement(event);
//当停止动画的时候,它会马上滚动到终点,然后向动画设置为结束。
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastY = y;
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
//当停止动画的时候,它会马上滚动到终点,然后向动画设置为结束。
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
//将移动事件添加到 VelocityTracker 中
mVelocityTracker.addMovement(event);
int dy = mLastY - y;
if (getScrollY() < 0) {
dy = 0;
}
//开始滚动
scrollBy(0, dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(1000); // 计算速度
int initialYVelocity = (int) mVelocityTracker.getYVelocity();
doFling(-initialYVelocity);
mVelocityTracker.recycle(); // 回收 VelocityTracker
mVelocityTracker = null;
break;
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return true;
}
private void doFling(int initialVelocity) {
int scrollY = getScrollY();
int maxY = Math.max(0, getChildCount() * mScreenHeight - mScreenHeight); // 计算最大滚动距离
mScroller.fling(0, scrollY, 0, initialVelocity, 0, 0, 0, maxY);
invalidate();
}
}
通过自定义实现 VelocityTracker 的配置与计算,通过 Scroller 实现相对的滚动,当然了通过上面的示例我们也都知道了 Scroller 的 Fling 其实也是一个动画,真正的动起来还是在 computeScroll 中自己实现的。就算不用 Scroller 通过自定义动画我们一样能实现同样的效果,下面会讲到。
public class ViewGroup7 extends LinearLayout {
private int mScreenHeight;
private GestureDetector mGestureDetector;
private Scroller mScroller;
private int mStart;
private int mEnd;
//...
@SuppressLint("ClickableViewAccessibility")
public ViewGroup7(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mStart = getScrollY();
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//如果是按下抬起,可以触发 onSingleTapUp ,但是如果是按下滑动,则不会触发
doEventUp();
return true;
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 处理滑动事件
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
scrollBy(0, (int) distanceY);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
doFling((int) -velocityY);
return true;
}
});
setFocusable(true);
setClickable(true);
setEnabled(true);
setLongClickable(true);
}
private void doFling(int velocityY) {
int scrollY = getScrollY();
int maxY = Math.max(0, getChildCount() * mScreenHeight - mScreenHeight);
mScroller.fling(0, scrollY, 0, velocityY, 0, 0, 0, maxY);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) { // 检查是否滚动操作完成
scrollTo(0, mScroller.getCurrY());
if (mScroller.getCurrX() == getScrollX() && mScroller.getCurrY() == getScrollY()) {
postInvalidate();
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mGestureDetector.onTouchEvent(event); //全部事件都交给 GestureDetector 处理
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// 手指抬起时并且 GestureDetector 没有触发onSingleTapUp, 那么你可以在这里处理
doEventUp();
}
return true;
}
return result;
}
private void doEventUp() {
YYLogUtils.w("doEventUp doEventUp");
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if (dScrollY > 0) {
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
} else {
mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
}
} else {
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
} else {
mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
}
}
invalidate(); // 必须调用此方法来触发重绘
}
}
GestureDetector 更为方便快捷,可以快速实现默认的惯性,如果需要更精细化的控制或自定义惯性,那么VelocityTracker 更加适合。具体使用哪个,取决于应用场景和开发者的具体需求。
public class ViewGroup7 extends LinearLayout {
private int mScreenHeight;
private GestureDetector mGestureDetector;
private ValueAnimator mValueAnimator;
private FlingAnimation flingY = null;
private int mStart;
private int mEnd;
private int mCurrY;
@SuppressLint("ClickableViewAccessibility")
public ViewGroup7(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mStart = getScrollY();
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//如果是按下抬起,可以触发 onSingleTapUp ,但是如果是按下滑动,则不会触发
doEventUp();
return true;
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 处理滑动事件
if (mValueAnimator != null && mValueAnimator.isRunning()) {
mValueAnimator.cancel();
}
scrollBy(0, (int) distanceY); //跟随手指滑动
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
YYLogUtils.w("onFling == 开始");
if (flingY != null) {
flingY.cancel();
}
// 设置起始值为当前滚动的Y值
int startY = getScrollY();
// 设置最大和最小值,这里设置为从头到尾滚动的范围
int minY = 0;
int maxY = Math.max(0, getChildCount() * mScreenHeight - mScreenHeight);
flingY = new FlingAnimation(new FloatValueHolder());
flingY.setStartValue(startY)
.setStartVelocity(-velocityY)
.setMinValue(minY)
.setMaxValue(maxY)
.addUpdateListener((animation, value, velocity) -> {
YYLogUtils.w("onFling value:" + value);
mCurrY = (int) value;
scrollTo(0, (int) value); //注意动画惯性的方式需要处理滚动的边界否则会报错,这里我懒没有处理
})
.start();
return true;
}
});
setFocusable(true);
setClickable(true);
setEnabled(true);
setLongClickable(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mGestureDetector.onTouchEvent(event); //全部事件都交给 GestureDetector 处理
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// 手指抬起时并且 GestureDetector 没有触发onSingleTapUp, 那么你可以在这里处理
doEventUp();
}
return true;
}
return result;
}
private void doEventUp() {
if (mValueAnimator != null && mValueAnimator.isRunning()) {
mValueAnimator.cancel();
}
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
int startY = getScrollY();
int endY = dScrollY > 0 ?
(dScrollY < mScreenHeight / 3 ? startY - dScrollY : startY + mScreenHeight - dScrollY) :
(Math.abs(dScrollY) < mScreenHeight / 3 ? startY - dScrollY : startY - mScreenHeight - dScrollY);
startAnim(startY, endY);
}
private void startAnim(int startY, int endY) {
mValueAnimator = ValueAnimator.ofInt(startY, endY);
mValueAnimator.setDuration(250); // 动画执行时间可以根据需要进行调整
mValueAnimator.setInterpolator(new DecelerateInterpolator()); // 设置动画插值器
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrY = (Integer) animation.getAnimatedValue();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
invalidate(); //启动重绘,否则无法实现滚动效果
}
@Override
public void onAnimationEnd(Animator animation) {
// 在动画结束时执行需要的操作,例如校正位置确保对齐
scrollTo(0, endY);
}
});
mValueAnimator.start();
}
@Override
public void computeScroll() {
super.computeScroll();
YYLogUtils.w("直接computeScroll了");
if (flingY != null && flingY.isRunning()) {
YYLogUtils.w("computeScroll - flingY - scroll");
scrollTo(0, mCurrY); //注意动画惯性的方式需要处理滚动的边界否则会报错,这里我懒没有处理
postInvalidate();
}
if (mValueAnimator != null && mValueAnimator.isRunning()) {
scrollTo(0, mCurrY);
postInvalidate();
}
}
}
关于本文的内容如果想查看源码可以点击这里 【传送门】。你也可以关注我的这个Kotlin项目,我有时间都会持续更新。
https://gitee.com/newki123456/Kotlin-Room
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!