使用各種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);
}
@Override
protected 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);
}
}
@Override
public 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原理