【凱子哥帶你夯實應用層】滾來滾去,滾來滾去...Scroller相關類使用大揭祕!!!

    轉載請註明出處:http://blog.csdn.net/zhaokaiqiang1992 

    話接上文,在前一篇文章裏面,咱們一起分析了“知乎”的回答詳情頁的需求,然後順便用代碼實現了下,忘了的可以再去看看【凱子哥帶你夯實應用層】都說“知乎”逼格高,我們來實現“知乎”回答詳情頁動畫效果 。其實在很多的界面效果中,這種“滾動”的效果能帶來很多的驚喜,各種效果也很有搞頭,說不定什麼時候,Boss看着哪個界面好看,就讓你去仿個過來,你要是說不會,那你下個月的工資還想發不!所以呢,今天這篇文章,就結合着一些案例,來稍微系統的總結一下Android系統中,如果要實現界面滾動,所涉及到的幾個常用類。

    Scroller和OverScroller,這兩個是AndroidUI框架下實現滾動效果的最關鍵的類,ScrollView內部的實現也是使用的OverScroller,所以熟練的使用這兩個類的相關API,可以讓我們滿足大部分的開發需求。

    在View類裏面,有兩個和滾動相關的類,scrollTo()和scrollBy。這兩個方法可以實現View內容的移動,注意,是內容,不是位置!是移動,不是滾動!什麼叫做內容呢?比如說一個TextView,如果使用scrollTo(),那麼移動的是裏面的文字,而不是位置,scrollBy()也是一樣的。那麼爲什麼是移動,不是滾動呢?這是因爲這兩個方法完成的都是瞬間完成,即瞬移,而不是帶有滾動過程的滾動,所以說,如果要實現效果比較好的滾動,光靠View自帶的方法還是不行滴,還是要Scrollers出馬~

    但是!Scrollers並不是控制View進行滾動,包括內容或者是位置,實際上,Scrollers只是一個控件移動軌跡的輔助計算類,如果你想滾,他能幫你計算什麼時間應該滾到什麼位置,但是滾不滾,全靠你自覺~所以說,滾動位置由Scrollers計算出來了,我們在什麼時候滾呢?滾多少呢?這時候,就要View的一個回調函數computeScroll()出馬了。

    我們看看View裏面的computeScroll()做了些什麼

/**
     * Called by a parent to request that a child update its values for mScrollX
     * and mScrollY if necessary. This will typically be done if the child is
     * animating a scroll using a {@link android.widget.Scroller Scroller}
     * object.
     */
    public void computeScroll() {
    }

    duang!空的!不過沒事,看看註釋,就是說,如果我們用Scroller實現一個滾動動畫的時候,這個方法就會被調用,被誰調用呢?parent,誰改變呢?child。所以一般來說,這個方法可以用來更新mScrollX和mScrollY,但是其實不光可以改變這些,我們還能做其他事情。

    如果我們調用Scroller.startScroll(int startX, int startY, int dx, int dy),那麼我們就可以在computeScroll()裏面執行實際的操作了,就像下面這樣

@Override
    public void computeScroll() {

        // 先判斷mScroller滾動是否完成
        if (mScroller.computeScrollOffset()) {
            // 這裏調用View的scrollTo()完成實際的滾動
            scrollTo( mScroller.getCurrX(), mScroller .getCurrY());
            // 必須調用該方法,否則不一定能看到滾動效果
            invalidate();
        }
        super.computeScroll();
    }

    Scroller.computeScrollOffset方法是來判斷滾動過程是否完成的,如果沒有完成,就需要不停的scrollTo下去,所以在最後需要加一個invalidate(),這樣可以再次觸發computScroll,直到滾動已經結束。


    其實說到這裏,有的同學可能比較迷惑,OverScroller和Scroller有什麼區別呢?事實上,這兩個類都屬於Scrollers,Scroller出現的比較早,在API1就有了,OverScroller是在API9才添加上的,出現的比較晚,所以功能比較完善,Over的意思就是超出,即OverScroller提供了對超出滑動邊界的情況的處理,這兩個類80%的API是一致的,OverScroller比Scroller添加了一下幾個方法

    ☞ isOverScrolled()
    ☞ springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) 
    ☞ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)
    ☞ notifyHorizontalEdgeReached(int startX, int finalX, int overX)
    ☞ notifyVerticalEdgeReached(int startY, int finalY, int overY)

    從名字也能看出來,都是對Over功能的支持,其他的API都一樣,所以介紹通用API的時候,並不區分OverScroller和Scroller。


    下面簡單介紹一下常用的API。

    ☞ computeScrollOffset() 這個就是來判斷當前的滑動動作是否完成的,用法很單一,就是在computeScroll()裏面來做判斷滾動是否完成

    ☞ getCurrX() 這個就是獲取當前滑動的座標值,因爲Scrollers只是一個輔助計算類,所以如果我們想獲取滑動時的時時座標,就可以通過這個方法獲得,然後在computeScroll()裏面調用

    ☞ getFinalX() 這個是用來獲取最終滑動停止時的座標

    ☞ isFinished() 用來判斷當前滾動是否結束

    ☞ startScroll(int startX, int startY, int dx, int dy) 用來開始滾動,這個是很重要的一個觸發computeScroll()的方法,調用這個方法之後,我們就可以在computeScroll裏面獲取滾動的信息,然後完成我們的需要。這個還有一個帶有滾動持續時間的重載函數,可以根據需求自由使用。特別要注意這四個參數,startX和startY是開始的座標位置,正數左上,負數右下,dx、dy同理,當在computeScroll()獲取getCurrX()的時候,變化範圍就與這裏地設置有關。

 /**
     * Start scrolling by providing a starting point and the distance to travel.
     * The scroll will use the default value of 250 milliseconds for the
     * duration.
     * 
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
     */
    public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    }
    ☞ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) 這個方法也很重要,如果你想實現滑動之後,佈局能夠根據移動速度,慢慢減速的話,就需要用這個來實現,這裏需要加速度的參數,我們可以通過VelocityTracker這個類來獲取,然後使用,具體參數函數,在下面的實例中進行說明。


    說了這麼多東西,都是最基礎的,也是最沒意思的,下面通過幾個小例子,我們來簡單地使用以下這些API,加深理解。

    因爲gif幀率太低,不能很好地展示效果,所以我錄取了一個視頻,請大家戳這裏(演示視頻)查看演示視頻,選擇720P高清播放。

    順便貼一下代碼,在後面對代碼進行解讀。

public void click(View view) {

		switch (view.getId()) {
			case R.id.btn_scroll_to:
				textView.scrollTo(distance, 0);
				distance += 10;
				break;
			case R.id.btn_scroll_by:
				textView.scrollBy(30, 0);
				break;
			case R.id.btn_sping_back:
				//不知道爲什麼第一次調用會貼牆,即到達x=0的位置
				textView.spingBack();
				break;
		}

	}

    首先點擊了scrollBy()三次,這個函數是相對座標移動,與當前座標無關,而scrollTo()則是絕對座標移動,如果distance相同的話,第二次就不會移動了,其實scrollBy()在源碼上也是scrollTo()

    /**
     * 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);
    }

    這樣就明白爲什麼3次scrollBy()之後,調用scrollTo()之後,內容會移動回來了,以爲前面的移動是30*3=90,而scrollTo()第一次調用distance是30,所以座標就回來了,視覺上就是後退回來。

    第三個拖拽回彈效果用的是一個自定義控件,下面我們會詳細的分析實現。

    第四個效果是spingBack(),即OverScroller的回彈效果,我們順便也介紹了。

    OK,咱們開始介紹這個可以回彈的自定義TextView是如何實現這種效果的。

    下面是實現的代碼

/**
 * Created by zhaokaiqiang on 15/2/28.
 */
public class JellyTextView extends TextView {

	private OverScroller mScroller;

	private float lastX;
	private float lastY;

	private float startX;
	private float startY;

	public JellyTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mScroller = new OverScroller(context, new BounceInterpolator());
	}


	@Override
	public boolean onTouchEvent(MotionEvent event) {

		switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				lastX = event.getRawX();
				lastY = event.getRawY();
				break;
			case MotionEvent.ACTION_MOVE:
				float disX = event.getRawX() - lastX;
				float disY = event.getRawY() - lastY;

				offsetLeftAndRight((int) disX);
				offsetTopAndBottom((int) disY);
				lastX = event.getRawX();
				lastY = event.getRawY();
				break;
			case MotionEvent.ACTION_UP:
				mScroller.startScroll((int) getX(), (int) getY(), -(int) (getX() - startX),
						-(int) (getY() - startY));
				invalidate();
				break;
		}

		return super.onTouchEvent(event);
	}


	@Override
	public void computeScroll() {

		if (mScroller.computeScrollOffset()) {
			setX(mScroller.getCurrX());
			setY(mScroller.getCurrY());
			invalidate();
		}

	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		startX = getX();
		startY = getY();
	}

	public void spingBack() {

		if (mScroller.springBack((int) getX(), (int) getY(), 0, (int) getX(), 0,
				(int) getY() - 100)) {
			Log.d("TAG", "getX()=" + getX() + "__getY()=" + getY());
			invalidate();
		}

	}


}

    代碼不到一百行,是不是很簡單呀,實現的效果是類似果凍的顫動效果,來來來,凱子哥帶你分析下代碼實現。

    首先我們用的是OverScroller,因爲和Scroller非常類似,而且增加了回彈支持,所以大部分情況下我們都可以使用OverScroller。我們在構造函數完成初始化,然後因爲我們需要記錄最開始的位置,在回彈的時候需要用,所以在onSizeChange()完成了起始座標的初始化。爲了完成拖拽功能,我們需要重寫onTouch,然後在MOVE事件中,完成控件的位置移動,用offsetLeftAndRight和offsetTopAndBottom即可,參數是一個相對位移的距離,所以很簡單就完成了控件跟隨手指移動的效果。

    最後的效果當然是控件回彈,但是這裏的回彈並不是用spingBack()完成,而是通過startScroll()完成,只要設置好當前的位置和我們需要位移的距離,然後記住invalidate一下,我們就可以去computeScroll()裏面實際的改變控件的位置了,通過getCurrX()就可以獲取到如果當前滾動應該的位置,所以setX()就OK啦,很簡單是不是?不過要記住invalidate(),這樣才能繼續往下觸發未完成的滾動操作。

    另外發現沒,這個控件叫JellyTextView,就是果凍TextView,因爲實現的是有來回顫動的效果,這個怎麼實現呢?也很簡單,設置一個BounceInterpolation就可以了,so easy~

    OK,其實現在大部分的Scroller的用法我們都用過了,還剩下一個OverScroll特有的spingBack()和fling(),我們先介紹一個spingBack的用法。

    springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) 

    看上面的參數,前兩個是開始位置,是絕對座標,minX和maxX是用來設定滾動範圍的,也是絕對座標範圍,如果startX不在這個範圍裏面,比如大於maxX,就會觸發computeScroll(),我們可以移動距離,最終回彈到maxX所在的位置,並返回true,從而完成後續的滾動效果,比minX小的話,就會回彈到minX,一樣的道理。所以我們可以像上面代碼裏面一樣,判斷是否在範圍內,在的話,就invalidate()一下,觸發滾動動畫,所以名字叫spingBack(),即回彈,在上面的視頻裏有演示效果。參照效果和代碼,你應該能看明白用法。


    OK,分析完上面的代碼,咱們就還有一個fling()沒用了,這個代碼咱們可以藉助鴻洋的Android 自定義控件 輕鬆實現360軟件詳情頁 裏面用到了這個方法,我簡單貼一下代碼,不過下面代碼經過了我的改造,添加了依附功能

@Override
	public boolean onTouchEvent(MotionEvent event) {

		mVelocityTracker.addMovement(event);
		int action = event.getActionMasked();
		float y = event.getY();

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			if (!mScroller.isFinished())
				mScroller.abortAnimation();
			mVelocityTracker.clear();
			mVelocityTracker.addMovement(event);
			mLastY = y;
			// return true;
			break;
		case MotionEvent.ACTION_MOVE:
			float dy = y - mLastY;

			if (!mDragging && Math.abs(dy) > mTouchSlop) {
				mDragging = true;
			}
			// 如果滑動的距離到達系統默認的最小值,就進行整體佈局的移動
			if (mDragging) {
				scrollBy(0, (int) -dy);
				mLastY = y;
			}
			break;
		case MotionEvent.ACTION_CANCEL:
			mDragging = false;
			if (!mScroller.isFinished()) {
				mScroller.abortAnimation();
			}
			break;
		case MotionEvent.ACTION_UP:
			mDragging = false;
			mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
			int velocityY = (int) mVelocityTracker.getYVelocity();
			// 手指離開之後,根據加速度進行滑動
			if (Math.abs(velocityY) > mMinimumVelocity) {
				fling(-velocityY);
			}
			mVelocityTracker.clear();

			int currentY = getScrollY();
			// 下拉
			isDownSlide = (event.getY() - mFirstY) > 0;

			if (isDownSlide) {
				if (currentY < mTopViewHeight) {
					Log.d(TAG, "下拉---下滑顯示");
					mScroller.startScroll(0, currentY, 0, -currentY);
					invalidate();
				}
			} else {
				if (currentY > 0) {
					Log.d(TAG, "上拉---上滑隱藏");
					mScroller.startScroll(0, currentY, 0, mTopViewHeight
							- currentY);
					invalidate();
				}
			}
			break;
		}

		return super.onTouchEvent(event);
	}

    fling()方法裏面有一個加速度的參數,我們需要通過VelocityTracker來獲取到加速度,VelocityTracker的用法很單一,就像上面一樣,當滑動的速度大於最小速度之後,調用fling(),我們再看看fling()的代碼


public void fling(int velocityY) {
		mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
		invalidate();
	}

  這個幾個參數如下,在上面的用法中,mxaY是上部佈局高度,所以就可以實現向上滑動的時候,鬆手後若加速度達到一定值,就能在佈局不顯示的時候停止,就完成了我們想要的效果。
public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY)

   不過爲了更好的模擬360的佈局效果,我對代碼進行了一點修改,主要是增加了依附效果,即上部佈局的依附,下面附上修改後的代碼,有興趣的可以試一下~

package com.zhy.view;

import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.OverScroller;
import android.widget.ScrollView;

import com.zhy.sample.StickyNavLayout.R;

public class StickyNavLayout extends LinearLayout {

	private static String TAG = "TAG";

	/**
	 * 最頂部的View
	 */
	private View mTop;
	/**
	 * 導航的View
	 */
	private View mNav;
	private ViewPager mViewPager;

	private int mTopViewHeight;
	private ScrollView mInnerScrollView;
	private boolean isTopHidden = false;

	private OverScroller mScroller;
	private VelocityTracker mVelocityTracker;
	private int mTouchSlop;
	private int mMaximumVelocity, mMinimumVelocity;

	private float mLastY;
	// Down時紀錄的Y座標
	private float mFirstY;
	// 是否是下拉
	private boolean isDownSlide;

	private boolean mDragging;

	public StickyNavLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
		setOrientation(LinearLayout.VERTICAL);

		mScroller = new OverScroller(context);

		mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
		mMaximumVelocity = ViewConfiguration.get(context)
				.getScaledMaximumFlingVelocity();
		mMinimumVelocity = ViewConfiguration.get(context)
				.getScaledMinimumFlingVelocity();

		mVelocityTracker = VelocityTracker.obtain();

	}

	@Override
	protected void onFinishInflate() {
		super.onFinishInflate();
		mTop = findViewById(R.id.id_stickynavlayout_topview);
		mNav = findViewById(R.id.id_stickynavlayout_indicator);
		View view = findViewById(R.id.id_stickynavlayout_viewpager);
		if (!(view instanceof ViewPager)) {
			throw new RuntimeException(
					"id_stickynavlayout_viewpager show used by ViewPager !");
		}
		mViewPager = (ViewPager) view;
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		// 這是爲了設置ViewPager的高度,保證TopView消失之後,能夠正好和NavView填充整個屏幕
		ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
		params.height = getMeasuredHeight() - mNav.getMeasuredHeight();
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		mTopViewHeight = mTop.getMeasuredHeight();
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		int action = ev.getAction();
		float y = ev.getY();

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mLastY = y;
			mFirstY = y;
			break;
		case MotionEvent.ACTION_MOVE:
			float dy = y - mLastY;
			getCurrentScrollView();
			if (Math.abs(dy) > mTouchSlop) {
				mDragging = true;
				// 如果Top的View是顯示狀態,或者是Fragment位於最上面的位置的時候,就攔截
				if (!isTopHidden
						|| (mInnerScrollView.getScrollY() == 0 && isTopHidden && dy > 0)) {
					Log.d(TAG, "-----觸摸攔截");
					return true;
				}
			}
			break;
		}
		return super.onInterceptTouchEvent(ev);
	}

	/**
	 * 獲取當前佈局裏面的ScrollView
	 */
	private void getCurrentScrollView() {

		int currentItem = mViewPager.getCurrentItem();
		PagerAdapter a = mViewPager.getAdapter();
		if (a instanceof FragmentPagerAdapter) {
			FragmentPagerAdapter fadapter = (FragmentPagerAdapter) a;
			Fragment item = fadapter.getItem(currentItem);
			mInnerScrollView = (ScrollView) (item.getView()
					.findViewById(R.id.id_stickynavlayout_innerscrollview));
		} else if (a instanceof FragmentStatePagerAdapter) {
			FragmentStatePagerAdapter fsAdapter = (FragmentStatePagerAdapter) a;
			Fragment item = fsAdapter.getItem(currentItem);
			mInnerScrollView = (ScrollView) (item.getView()
					.findViewById(R.id.id_stickynavlayout_innerscrollview));
		}

	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {

		mVelocityTracker.addMovement(event);
		int action = event.getActionMasked();
		float y = event.getY();

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			if (!mScroller.isFinished())
				mScroller.abortAnimation();
			mVelocityTracker.clear();
			mVelocityTracker.addMovement(event);
			mLastY = y;
			// return true;
			break;
		case MotionEvent.ACTION_MOVE:
			float dy = y - mLastY;

			if (!mDragging && Math.abs(dy) > mTouchSlop) {
				mDragging = true;
			}
			// 如果滑動的距離到達系統默認的最小值,就進行整體佈局的移動
			if (mDragging) {
				scrollBy(0, (int) -dy);
				mLastY = y;
			}
			break;
		case MotionEvent.ACTION_CANCEL:
			mDragging = false;
			if (!mScroller.isFinished()) {
				mScroller.abortAnimation();
			}
			break;
		case MotionEvent.ACTION_UP:
			mDragging = false;
			mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
			int velocityY = (int) mVelocityTracker.getYVelocity();
			// 手指離開之後,根據加速度進行滑動
			if (Math.abs(velocityY) > mMinimumVelocity) {
				fling(-velocityY);
			}
			mVelocityTracker.clear();

			int currentY = getScrollY();
			// 下拉
			isDownSlide = (event.getY() - mFirstY) > 0;

			if (isDownSlide) {
				if (currentY < mTopViewHeight) {
					Log.d(TAG, "下拉---下滑顯示");
					mScroller.startScroll(0, currentY, 0, -currentY);
					invalidate();
				}
			} else {
				if (currentY > 0) {
					Log.d(TAG, "上拉---上滑隱藏");
					mScroller.startScroll(0, currentY, 0, mTopViewHeight
							- currentY);
					invalidate();
				}
			}
			break;
		}

		return super.onTouchEvent(event);
	}

	public void fling(int velocityY) {
		mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
		invalidate();
	}

	@Override
	public void scrollTo(int x, int y) {
		if (y < 0) {
			y = 0;
		}
		if (y > mTopViewHeight) {
			y = mTopViewHeight;
		}
		if (y != getScrollY()) {
			super.scrollTo(x, y);
		}

		isTopHidden = getScrollY() == mTopViewHeight;

	}

	@Override
	public void computeScroll() {
		if (mScroller.computeScrollOffset()) {
			scrollTo(0, mScroller.getCurrY());
			invalidate();
		}
	}

}


    OK,關於滾動的這些東西基本上就這些吧,不過也都是最基礎的,如果能熟悉的運用這些API,就能創造出非常棒的用戶體驗,大家快來一起滾啊~~


演示代碼下載:https://github.com/ZhaoKaiQiang/ScrollerDemo


發佈了130 篇原創文章 · 獲贊 129 · 訪問量 148萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章