android自訂View之自訂可置頂ScrollView,View滑動原理簡析,androidscrollview
使用各種App的時候,發現一個特點,現在的App在可以滑動的頁面中,基本上都添加了一個置頂按鈕的效果,使的使用者可以很方便的回到頂部,就像網頁中幾乎所有的頁面都有置頂效果按鈕一樣,覺得這個互動不錯,以後自己的項目裡也肯定會用到的,就抽空把這個小功能實現了一下,現在把實現過程記錄下,以方便以後使用。
先看下:
當ScrollView向上滑動超過一定距離後,就漸層的出現一個置頂的按鈕,當滑動距離小於我們指定的距離時,按鈕又以一個漸層的方式消失。
實現原理:
這個小案例的實現原理很簡單:1.就是監聽View的onScrollChanged()方法,擷取到ScrolView滑動的距離,如果大於我們的距離,則出現置頂按鈕,否則,隱藏
2.按鈕點擊事件的處理,點擊按鈕讓整個ScrollView滑動到(0,0)位置即可。
雖然這個案例很簡單,但是卻涉及到了android的滑動原理,從onScrollViewChanged這個方法屬於View這個類也可以看出,android的任何View都是可以滑動的,在這個案例後,我會說一下我對android滑動原理的理解。
自訂ScrollView代碼:
<span style="font-size:14px;">/********************************************************** * @檔案名稱:GoTopScrollView.java * @檔案作者:rzq * @檔案描述:滑動超過一定距離後,出現置頂按鈕 * @修改曆史:2015年3月26日建立初始版本 **********************************************************/public class GoTopScrollView extends ScrollView implements OnClickListener{private ImageView goTopBtn;private int screenHeight;public GoTopScrollView(Context context, AttributeSet attrs){super(context, attrs);}public void setScrollListener(ImageView goTopBtn){this.goTopBtn = goTopBtn;this.goTopBtn.setOnClickListener(this);}@Overrideprotected void onScrollChanged(int l, int t, int oldl, int oldt){super.onScrollChanged(l, t, oldl, oldt);/** * 滑動距離超過300px,出現向上按鈕,可以做為自訂屬性 */if (t >= 300){goTopBtn.setVisibility(View.VISIBLE);}else{goTopBtn.setVisibility(View.GONE);}}@Overridepublic void onClick(View v){if (v.getId() == R.id.go_top_btn){this.smoothScrollTo(0, 0);}}}</span> Activity使用代碼:
<span style="font-size:14px;">private void initView(){container = (RelativeLayout) findViewById(R.id.container);container.setLayoutTransition(new LayoutTransition());goTopBtn = (ImageView) findViewById(R.id.go_top_btn);scrollView = (GoTopScrollView) findViewById(R.id.scroll_view);scrollView.setScrollListener(goTopBtn);}</span> 這樣就很輕鬆的實現了這個置頂的效果,但僅僅這樣是不夠的,現在我們來分析一下android的滑動原理到底是怎麼回事,是不是只有ScrollView才可以滑動。
1.首先,android中任意的View,都是可以滑動的,為什麼呢?請看View中的關鍵源碼,只挑出和滑動有關的代碼:
<span style="font-size:14px;"> /** * 指定X軸的滑動,Y軸不動 * @param value the x position to scroll to */ public void setScrollX(int value) { scrollTo(value, mScrollY); } /** * 指定Y軸的滑動,X軸不動 * @param value the y position to scroll to */ public void setScrollY(int value) { scrollTo(mScrollX, value); } /** * 真正處理滑動的方法 * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } /** * Move the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally * @param y the amount of pixels to scroll by vertically */ public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); } /** * 觸發滑動後的監聽 * @param l Current horizontal scroll origin. * @param t Current vertical scroll origin. * @param oldl Previous horizontal scroll origin. * @param oldt Previous vertical scroll origin. */ protected void onScrollChanged(int l, int t, int oldl, int oldt) { if (AccessibilityManager.getInstance(mContext).isEnabled()) { postSendViewScrolledAccessibilityEventCallback(); } mBackgroundSizeChanged = true; final AttachInfo ai = mAttachInfo; if (ai != null) { ai.mViewScrollChanged = true; } }</span><span style="font-size:14px;"> </span>
由這幾個方法可以看出,android中所有View都是可以滑動的,其實所有的View都有兩套座標系,一個是指定View位置的X,Y,另一對則是指定View滑動座標系,mScrollX和mScrollY,兩對座標系統是完全獨立的,即X,Y座標的改變不會影響mScrollX和mScrollY,反之也成立。下面,我們則重點分析一下scrollTo這個方法。
scrollTo(int x, int y) 是將View中內容滑動到相應的位置,參考的座標系原點為parent View的左上方。
調用scrollTo(100, 0)表示將View中的內容移動到x = 100, y = 0的位置,如所示。注意,圖中黃色矩形地區表示的是一個parent View,綠色虛線矩形為parent view中的內容。一般情況下兩者的大小一致,本文為了顯示方便,將虛線框畫小了一點。圖中的黃色地區的位置始終不變,發生位置變化的是顯示的內容,:
mScrollX和mScrollY是View類中專門用於記錄滑動位置的變數。這兩個函數最終調用onScrollChanged()函數。
先總結一下,android的滑動原理就是:通過調用scrollTo(),使的View的滑動座標系發生改變,並儲存在mScrollX和mScrollY這兩個成員變數中,我們可以通過getScrollX()和getScrollY(),擷取滑動座標值。
由圖中我們可以看到,當我們調用scrollTo(100,0);時,發現View是向左滑動了,傳入一個正數滑動的方向卻是向左,這與我們平常理解的座標軸是相反的,其實並不矛盾,因為滑動座標系本來就與我們平常的座標系統不一樣,調用scrollTo方法,最終會執行到以下代碼:
public void invalidate(int l, int t, int r, int b) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE); } if (skipInvalidate()) { return; } if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) || (mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID || (mPrivateFlags & INVALIDATED) != INVALIDATED) { mPrivateFlags &= ~DRAWING_CACHE_VALID; mPrivateFlags |= INVALIDATED; mPrivateFlags |= DIRTY; final ViewParent p = mParent; final AttachInfo ai = mAttachInfo; //noinspection PointlessBooleanExpression,ConstantConditions if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } } if (p != null && ai != null && l < r && t < b) { final int scrollX = mScrollX; final int scrollY = mScrollY; final Rect tmpr = ai.mTmpInvalRect; tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY); p.invalidateChild(this, tmpr); } } } 看倒數第五行代碼,更新滑動座標系時,走的是 tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);傳入一個正數後一減,就成了負數,自然要向左移動了,這樣就又統一起來了。
總結:以上差不多就是View內建的滑動原理,但View中的滑動都是瞬時滑動。沒有任何的過渡效果,結合Scroller是可以做出各種各樣的過渡效果的,下篇文章,總結一下如何結合Scroller使View更加炫的滑動。
參考文章: 圖解android View的scroll原理