笔者是个菜鸟,最近对百度贴吧的滑动效果感兴趣,想研究下怎么实现的,滑动效果很多框架都有实现,大家平时用的很多app都有这个效果,大家肯定都熟悉了。那我们不能满足于使用啊,作为开发者,对于原理要晓得才行啊。这个研究的过程可就一波三折了。
百度搜索一下,很多目标都指向了scroller。scroller的工作过程大家可以看看其他文章,
http://blog.csdn.net/chaoyue0071/article/details/44038641和http://www.cnblogs.com/wanqieddy/archive/2012/05/05/2484534.html
这两篇文章其实对于scroller的基本使用方法已经说明了。那有读者要说了,别人都已经讲了基本用法,那你还写这篇文章干毛线啊。其实,笔者写这篇文章的目的是优化
如果读者看过相关的文章,就会发现,现在网上文章里,真正实现滑动效果是在ViewGroup的computeScoll方法中不断拿到scroller计算的值,然后不停放scrollTo,直到整个动画过程结束。说实话,这么做性能很好了。但是,笔者在研究到这的时候有一个疑惑,scrollTo方法是移动View内部的视图用的,比如说,重写了LinearLayout的computeScoll方法,在里面调用scrollTo,那么LinearLayout的所有子view都会平移,而google官方的和很多商业应用的第三方侧滑框架滑动逻辑可不是这么简单的,比如官方的,滑动只是侧边栏在滑动,百度贴吧和微信的滑动效果更棒,在侧边栏滑动的同时,主界面也是在动的,这个滑动效果速度慢一点,那么,如果只是在computeScoll中简单的调用scrollTo方法,那么根本不可能实现这么复杂的滑动效果,另外笔者研究之前,是担心scrollTo方法会造成性能损失,所以才研究DrawerLayout的实现的。
我们从google官方的DrawerLayout开始,不熟悉DrawerLayout的朋友先了解下这个控件,很简单。这里是官方的demo
http://download.csdn.net/detail/lee_tianya/8378589
我们讨论的还要涉及视图的绘制流程,对绘制流程不熟悉的朋友可以看看这篇文章
http://blog.csdn.net/guolin_blog/article/details/12921889
视图绘制时,系统会遍历视图二叉树,直到遍历完成,其中在drawChild方法中,会调用到child的computeScroll方法。
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
......
......
if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&
(child.mPrivateFlags & DRAW_ANIMATION) == 0) {
return more;
}
child.computeScroll();
final int sx = child.mScrollX;
final int sy = child.mScrollY;
boolean scalingRequired = false;
Bitmap cache = null;
......
......
}
注意这个方法调用的位置,这个和之后的scroller工作的逻辑要牵扯到的。
这是一个典型的drawerLayout布局
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- As the main content view, the view below consumes the entire
space available using match_parent in both dimensions. -->
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- android:layout_gravity="start" tells DrawerLayout to treat
this as a sliding drawer on the left side for left-to-right
languages and on the right side for right-to-left languages.
The drawer is given a fixed width in dp and extends the full height of
the container. A solid background is used for contrast
with the content view. -->
<ListView
android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:background="#111"/>
</android.support.v4.widget.DrawerLayout>
布局中的ListView就是侧滑栏。在左边还是右边由layout_gravity控制,start为左,end为右。
查看DrawerLayout的源码,可以看到有openDrawer和closeDrawer方法,从名字上看应该是打开关闭侧滑的方法。
openDrawer方法
public void openDrawer(View drawerView) {
if (!isDrawerView(drawerView)) {
throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
}
if (mFirstLayout) {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 1.f;
lp.knownOpen = true;
updateChildrenImportantForAccessibility(drawerView, true);
} else {
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop());
} else {
mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(),
drawerView.getTop());
}
}
invalidate();
}
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
// If we're in an IDLE state to begin with and aren't moving anywhere, we
// end up having a non-null capturedView with an IDLE dragState
mCapturedView = null;
}
return continueSliding;
}
smoothSlideViewTo方法中,首先把需要滑动的view设为帮助类的mCapturedView对象引用,然后调用了forceSettleCapturedViewAt方法,我们进入这个方法看看。
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
我们终于看到了我们想看到的一个方法,scroller的startScroll方法!说明这个地方启动了滑动动画的计算,那么这个方法走完了,在openDrawer方法中调用了invalidate方法,所以系统需要重绘视图了。
系统在重绘的过程中,肯定要取mScroller中的数据,scroller都是在computeScrollOffSet方法中计算的,那么我们搜索下这个方法被哪引用了,发现了帮助类的continueSettling方法中有使用
public boolean continueSettling(boolean deferCallbacks) {
if (mDragState == STATE_SETTLING) {
boolean keepGoing = mScroller.computeScrollOffset();
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
final int dx = x - mCapturedView.getLeft();
final int dy = y - mCapturedView.getTop();
if (dx != 0) {
mCapturedView.offsetLeftAndRight(dx);
}
if (dy != 0) {
mCapturedView.offsetTopAndBottom(dy);
}
if (dx != 0 || dy != 0) {
mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
}
if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
// Close enough. The interpolator/scroller might think we're still moving
// but the user sure doesn't.
mScroller.abortAnimation();
keepGoing = false;
}
if (!keepGoing) {
if (deferCallbacks) {
mParentView.post(mSetIdleRunnable);
} else {
setDragState(STATE_IDLE);
}
}
}
return mDragState == STATE_SETTLING;
}
这个方法中,可以看到拿到了在动画生成时应该对应的滑动view的位置信息,例如,dx,可以发现是从此时到下一帧动画,x移动的距离,然后调用了mCapturedViewde的offsetoffsetLeftAndRight的方法。y的变化也类似。
我们看看offsetLeftAndRight方法
public void offsetLeftAndRight(int offset) {
if (offset != 0) {
final boolean matrixIsIdentity = hasIdentityMatrix();
if (matrixIsIdentity) {
if (isHardwareAccelerated()) {
invalidateViewProperty(false, false);
} else {
final ViewParent p = mParent;
if (p != null && mAttachInfo != null) {
final Rect r = mAttachInfo.mTmpInvalRect;
int minLeft;
int maxRight;
if (offset < 0) {
minLeft = mLeft + offset;
maxRight = mRight;
} else {
minLeft = mLeft;
maxRight = mRight + offset;
}
r.set(0, 0, maxRight - minLeft, mBottom - mTop);
p.invalidateChild(this, r);
}
}
} else {
invalidateViewProperty(false, false);
}
mLeft += offset;
mRight += offset;
mRenderNode.offsetLeftAndRight(offset);
if (isHardwareAccelerated()) {
invalidateViewProperty(false, false);
invalidateParentIfNeededAndWasQuickRejected();
} else {
if (!matrixIsIdentity) {
invalidateViewProperty(false, true);
}
invalidateParentIfNeeded();
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
我们看看DrawerLayout的computeScroll方法
public void computeScroll() {
final int childCount = getChildCount();
float scrimOpacity = 0;
for (int i = 0; i < childCount; i++) {
final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen;
scrimOpacity = Math.max(scrimOpacity, onscreen);
}
mScrimOpacity = scrimOpacity;
// "|" used on purpose; both need to run.
if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
DrawerLayout是一个viewGroup,所以当然也是view,所以在重绘view的时候,computeScroll方法是会不停的被调用的,这里调用了continueSettling方法,然后调用postInvalidateOnAnimation使得下一帧动画中应用新的值。
现在,大家应该对官方的DrawerLayout中如何实现滑动效果有个了解了吧?其实笔者也没讲全,有兴趣的可以看看onTouchEvent方法研究下跟着手指滑动是如何实现的。
之前笔者说了,官方的和一些第三方的框架的滑动实现可不是像网上的一些文章那样简单的调用scrollTo就皆大欢喜的,那么,有什么区别呢,之前笔者认为可能会有性能损失的,不过看了scrollTo方法再对比下,发现都是主要调用了postInvalidateOnAnimation方法,所以性能上应该不会有多少损失。但是,我们之前说到,重写ViewGroup中computeScroll方法调用scrollTo只是简单的让ViewGroup的所有子View简单的平移而已,如果一个ViewGroup有3个子view,我想让3个子View的滑动效果不同呢,有的慢,有的快如何做?
既然我们在computeScroll中可以拿到scroller的计算值,那我们可以在里面实现自己想要的逻辑,而不是简单scrollTo。
在http://www.cnblogs.com/wanqieddy/archive/2012/05/05/2484534.html这篇文章中,作者有给一个demo,看了他的代码,也是在computeScroll中调用scrollTo方法,笔者要在这歌demo基础之上修改代码,实现只让第一个红色view滚动,而其他的不跟随滚动。
首先发demo中MutiViewGroup中的int方法中的三个子view的引用抽到全局中,如下
private LinearLayout oneLL;
private LinearLayout twoLL;
private LinearLayout threeLL;
然后修改MutiViewGroup的computeScroll方法
if (mScroller.computeScrollOffset()) {
Log.d("biu","computeScrollOffset : true");
Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY());
// 产生了动画效果 每次滚动一点
// scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// Log.e("biu", "### getleft is " + getLeft() + " ### getRight is " + getRight());
final int x = mScroller.getCurrX();
final int dx = x - oneLL.getLeft();
oneLL.offsetLeftAndRight(dx);
//刷新View 否则效果可能有误差
postInvalidateOnAnimation();
}
运行代码,点击按钮,会发现只有第一个红色的view跟随滚动,而不是所有的子view,读者读到这应该明白如何给不同的子view设置不同的滑动效果了吧?
笔者建议大家,在重写computeScroll方法实现滑动效果,尽量不要直接调用scrollTo方法。