mScroller = new Scroller(getContext(), null, true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 2) {
throw new RuntimeException(“Only need two child view! Please check you xml file!”);
}
mLeftView = getChildAt(0);
mRightView = getChildAt(1);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitX = ev.getX();
mInitY = ev.getY();
super.dispatchTouchEvent(ev);
return true;
case MotionEvent.ACTION_MOVE:
//>0为手势向右下
mOffsetX = ev.getX() - mInitX;
mOffsetY = ev.getY() - mInitY;
//横向手势跟随移动
if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) {
int offset = (int) -mOffsetX;
if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) {
return true;
}
this.scrollBy(offset, 0);
mInitX = ev.getX();
mInitY = ev.getY();
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//松手时刻滑动
int offset = ((getScrollX() / (float)mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;
// this.scrollTo(offset, 0);
mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset-this.getScrollX(), 0);
invalidate();
mInitX = 0;
mInitY = 0;
mOffsetX = 0;
mOffsetY = 0;
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
}
简单吧,使用Scroller就能这么优雅的滑动,不解释,简单的Demo,哈哈;有了这个基本映像我们直接高速——源码探测,搞清源码基本原理流程就能用的顺手喽。
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
3 Scroller源码浅析
==================
通过上面实例我们可以发现在自定义View的过程中使用Scroller的流程如下图所示:
既然有了这么明确的流程图,那我们下面就来依据这个流程简单分析下Scroller的源码。可以发现Scroller这类的代码不多哇,确实是一个工具类,哈哈,我们先看下构造方法:
public Scroller(Context context) {
this(context, null);
}
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
mFinished = true;
if (interpolator == null) {
mInterpolator = new ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
//摩擦力计算单位时间减速度
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mFlywheel = flywheel;
mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}
可以看见,构造方法没啥特殊的,只是一些基础的设置,唯一要重点关注可能自定义的也就动画插值器那个参数了,默认是ViscousFluidInterpolator的(不懂动画插值器的请看《Android应用开发之所有动画使用详解》),我们可以自定义修改。两参构造方法中其实也就是对第三个参数做了HONEYCOMB兼容性处理,三参是所有构造方法最终调运的方法,其实也就是初始化了一些变量而已,没啥重要的。
下面我们看看与Scroller相关的startScroll()和fling()方法,源码如下:
//在我们想要滚动的地方调运,准备开始滚动,默认滚动时间为DEFAULT_DURATION
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
//在我们想要滚动的地方调运,准备开始滚动,手动设置滚动时间
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
//在快速滑动松开的基础上开始惯性滚动,滚动距离取决于fling的初速度
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
…
mMode = FLING_MODE;
mFinished = false;
…
mStartX = startX;
mStartY = startY;
…
mDistance = (int) (totalDistance * Math.signum(velocity));
mMinX = minX;
mMaxX = maxX;
mMinY = minY;
mMaxY = maxY;
…
mFinalY = Math.min(mFinalY, mMaxY);
mFinalY = Math.max(mFinalY, mMinY);
}
可以看见,上面这几个美其名曰滑动的Scroller方法其实都只是一个幌子,没有进行滑动,而是初始化了一堆成员变量;譬如滚动模式、开始时间、持续时间等,也就是说他们都只是工具方法而已,实质的滑动其实是需要我们在他后面手动调运View的invalidate()进行刷新,然后在View进行刷新时又会调运自己的View.computeScroll()方法(不了解View绘制的请看《Android应用层View绘制流程与源码分析》一文),在View.computeScroll()方法中进行Scroller.computeScrollOffset()判断与触发View的滑动方法。
既然这样那我们粗略给出View的绘制流程,详细的请看《Android应用层View绘制流程与源码分析》一文。当我们调运invalidate()会触发View的如下方法:
public void draw(Canvas canvas) {
…
/*
-
Draw traversal performs several drawing steps which must be executed
-
in the appropriate order:
-
1. Draw the background
-
2. If necessary, save the canvas' layers to prepare for fading
-
3. Draw view's content
-
4. Draw children
-
5. If necessary, draw the fading edges and restore layers
-
6. Draw decorations (scrollbars for instance)
*/
…
// Step 4, draw the children
dispatchDraw(canvas);
…
}
可以发现,View的draw()方法被触发时总共会进行6步,最重要的一步我们看第四步,下面是第四步dispatchDraw()方法源码:
protected void dispatchDraw(Canvas canvas) {}
可以看见,View的该方法为空方法,那我们看下他子类ViewGroup的该方法,如下:
protected void dispatchDraw(Canvas canvas) {
…
for (int i = 0; i < childrenCount; i++) {
…
more |= drawChild(canvas, child, drawingTime);
…
}
…
}
可以发现,ViewGroup的dispatchDraw()方法实质又跑到了drawChild()方法,如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
额额,实质又是child的另一个draw()方法而已,我们回到View去看下这个方法,如下:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
…
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
…
}
额额,这就解释了为何View调运invalidate()就会触发computeScroll()方法了。而ViewGroup最终调运scrollTo()方法都只能滚动内部子View的问题其实是因为ViewGroup它本身并没有任何可画的东西,它是一个透明的控件,所以一般不会触发onDraw()方法,但是当你给他设置背景等就会调用onDraw方法了,可是走的是绘制背景流程。
View相关的扯完了,下面我们来看看Scroller的computeScrollOffset()方法,下面我们简单分析这个方法,如下:
//判断滚动是否还在继续,true继续,false结束
public boolean computeScrollOffset() {
//mFinished为true表示已经完成了滑动,直接返回为false
if (mFinished) {
return false;
}
//mStartTime为开始时的时间戳,timePassed就是当前滑动持续时间
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//mDuration为我们设置的持续时间,当当前已滑动耗时timePassed小于总设置持续时间时才进入if
if (timePassed < mDuration) {
//mMode有两中,如果调运startScroll()则为SCROLL_MODE模式,调运fling()则为FLING_MODE模式
switch (mMode) {
case SCROLL_MODE:
//根据Interpolator插值器计算在该时间段里移动的距离赋值给mCurrX和mCurrY
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
//各种数学运算获取mCurrY、mCurrX,实质类似上面SCROLL_MODE,只是这里时惯性的
…
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
以前一直是自己在网上东平西凑的找,找到的东西也是零零散散,很多时候都是看着看着就没了,时间浪费了,问题却还没得到解决,很让人抓狂。
后面我就自己整理了一套资料,还别说,真香!
资料有条理,有系统,还很全面,我不方便直接放出来,大家可以先看看有没有用得到的地方吧。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
最后
以前一直是自己在网上东平西凑的找,找到的东西也是零零散散,很多时候都是看着看着就没了,时间浪费了,问题却还没得到解决,很让人抓狂。
后面我就自己整理了一套资料,还别说,真香!
资料有条理,有系统,还很全面,我不方便直接放出来,大家可以先看看有没有用得到的地方吧。
[外链图片转存中…(img-bPo6yQDR-1712710502553)]
[外链图片转存中…(img-dinSf8Io-1712710502553)]
[外链图片转存中…(img-vgO3dflT-1712710502553)]
[外链图片转存中…(img-dxBLQpdT-1712710502554)]
[外链图片转存中…(img-na9bnH1k-1712710502554)]
[外链图片转存中…(img-qRrJDRCw-1712710502554)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!