帶你徹徹底底弄懂Scroller
Scroller的使用
這是一個滑動幫助類。並不能夠使View真正的滑動,而是依據時間的流逝。獲取插值器中的數據。傳遞給我們。讓我們去配合scrollTo/scrollBy去讓view產生緩慢滑動,產生動畫的效果。事實上是和屬性動畫同一個原理。以下是官方文檔對於這個類所給的解釋:
This class encapsulates scrolling. You can use scrollers (Scroller or OverScroller) to collect the data you need to produce a scrolling animation—for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don’t automatically apply those positions to your view. It’s your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.
首先。我們要先獲得這個對象,我們一起看看它的構造方法:
public Scroller (Context context)
public Scroller (Context context, Interpolator interpolator)
public Scroller (Context context, Interpolator interpolator, boolean flywheel)
一共同擁有三個構造方法。我們通經常使用第二個比較多,給定Scroller一個插值器,讓其從這樣的插值器中取值。
那麽,怎樣使用Scroller呢,僅僅需調用以下的代碼就可以:
Scroller.(int startX, int startY, int dx, int dy, int duration)。
invalidate();
就能夠為Scroller指定從起始位置和結束位置以及滑動的時間。Scroller就能夠幫助我們獲取某一時刻,我們的view所在的位置了。
接著我們須要在view的computeScroll()的方法中推斷scroller是否結束,假設沒有結束就用scrollTo方法使view處於正確的位置就可以。
@Override
public void computeScroll() {
//推斷是否還在滾動,還在滾動為true
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//更新界面
postInvalidate();
isMove = true;
}
super.computeScroll();
}
怎麽樣,用起來是不是非常easy粗暴。
Scroller的源代碼分析
僅僅是會使用可不行。我們不僅要知其然還要知其所以然。接下來,我們從源代碼的角度來分析下Scroller的工作原理。從哪裏分析呢。事實上也不用從Scroller類的第一行代碼開始看。撿重要的看就可以了。
首先看看startScroll()這種方法吧:
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;
}
我們能夠從中看到,Scroller僅僅是給他的成員變量一一賦值而已,比方模式啊,位置信息,時間等。並沒有做關於view滑動的不論什麽工作。我們接下來調用了View的invalidate()方法,讓View樹又一次繪制,讓它繪制什麽呢?什麽東西都沒有,這不是坑我們麽?事實上。View的draw方法中都會調用我們上面說的computeScroll() 方法,可是這種方法在View中卻是一個空方法。接下來。我們就繼續分析Scroller的computeScrollOffset()方法:
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float x = timePassed * mDurationReciprocal;
if (mInterpolator == null)
x = viscousFluid(x);
else
x = mInterpolator.getInterpolation(x);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
從這種方法中,我們能夠看出。Scroller先推斷滑動有沒有結束,假設沒有結束就去獲取View此時應該所處的位置信息。這種方法中重要的是它的返回值。假設完畢了返回false,沒有完畢才返回true。繼續接著Scroller的使用段落往下看。假設mScroller.computeScrollOffset()的返回值是true的話,也就是Scroller還沒結束,我們就讓我們的View滑動到這裏,並刷新。值得註意的是,View中的computeScroll()方法並非執行在主線程中的,所以我們要使postInvalidate()方法來調用重繪。
接著重繪又會調用View的draw方法,draw方法又會調用computeScroll()方法,直至Scroller結束。到這裏我們依據平時使用的代碼的走向,了解了Scroller的大致工作流程。
怎麽樣。是不是對Scroller的理解更深刻一些了呢?
Scroller的實例
僅僅是理解還不夠啊,我們得從實際開發中使用Scroller。才幹真正的會用他。接下來,我會用兩個實例,來升華你對Scroller的理解。
實例一:仿微信朋友圈刷新
廢話不多說,直接上代碼吧!
/**
* 仿微信刷新
* @author Nipuream
*/
public class WXLayout extends LinearLayout{
private static final String TAG = "WXLayout";
private int mTouchSlop;
private boolean mIsBeingDragged = false;
private float mLastMotionY;
private float mInitialMotionY;
private float resistance = 0.6f;
private Scroller mScroller;
private ListView mListView;
private boolean isMove = false;
private int duration = 300;
private ScrollRershListener l;
private boolean isRersh = false;
public WXLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
init(context);
}
private void init(final Context context){
ViewConfiguration config = ViewConfiguration.get(context);
mTouchSlop = config.getScaledTouchSlop();
DecelerateInterpolator interpolator = new DecelerateInterpolator();
mScroller = new Scroller(context,interpolator);
post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
mListView = (ListView) WXLayout.this.getChildAt(0);
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
final int action = ev.getAction();
if(action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP){
mIsBeingDragged = false;
return false;
}
if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
return true;
}
switch(action){
case MotionEvent.ACTION_DOWN:{
mLastMotionY = mInitialMotionY = ev.getY();
mIsBeingDragged = false;
break;
}
case MotionEvent.ACTION_MOVE:{
final float y = ev.getY(), x = ev.getX();
final float diff, absDiff;
diff = y - mLastMotionY;
absDiff = Math.abs(diff);
if(absDiff > mTouchSlop){
if(diff > 1){
if(mListView.getFirstVisiblePosition()==0){
View view = mListView.getChildAt(0);
Rect rect = new Rect ();
view.getLocalVisibleRect(rect);
if(rect.top == 0){
mLastMotionY = y;
mIsBeingDragged = true;
}
}
}
}
break;
}
}
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
//假設碰觸到控件的邊緣。就不接受這一系列的action了
if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
return false;
}
//假設Scroller正在滑動。就不接受這次事件了
if(isMove){
return false;
}
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
mLastMotionY = mInitialMotionY = event.getY();
return true;
}
case MotionEvent.ACTION_MOVE:{
if (mIsBeingDragged) {
if(l!=null && !isRersh){
l.startRersh();
isRersh = true;
}
mLastMotionY = event.getY();
float moveY = mLastMotionY - mInitialMotionY;
pullEvent(moveY);
return true;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:{
if(mIsBeingDragged){
mIsBeingDragged = false;
startMoveAnim(getScrollY(), Math.abs(getScrollY()), duration);
if(l!= null && isRersh && (event.getY() - mInitialMotionY) > 0){
l.endRersh(event.getY() - mInitialMotionY);
isRersh = false;
}
return true;
}
break;
}
}
return super.onTouchEvent(event);
}
private void pullEvent(float moveY){
if(l != null){
l.Rersh(moveY);
}
if(moveY > 0){
int value = (int) Math.abs(moveY);
scrollTo(0, - (int)(value*resistance));
}
}
public void startMoveAnim(int startY, int dy, int duration) {
isMove = true;
mScroller.startScroll(0, startY, 0, dy, duration);
invalidate();//通知UI線程的更新
}
@Override
public void computeScroll() {
//推斷是否還在滾動,還在滾動為true
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//更新界面
postInvalidate();
isMove = true;
} else {
isMove = false;
}
super.computeScroll();
}
public interface ScrollRershListener{
void Rersh(float value);
void startRersh();
void endRersh(float value);
}
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public void setOnScrollRershListener(ScrollRershListener l){
this.l = l;
}
}
實例二:仿QQ側滑刪除
得多謝夏安明大神的博客,讓我有了清晰的思路,對他的代碼加以改造。從而完畢了這個實例。
/**
* 仿QQ側滑刪除
* @author Nipuream
*
*/
public class SlideListView extends ListView implements OnTouchListener,OnClickListener{
private static final String TAG = "SlideListView";
private Context mContext;
private Scroller mScroller;
/**
* 初始值
*/
private float initalXvalue,mLastXvalue;
private float initalYvalue,mLastYvalue;
/**
* 確認滑動的最小速度
*/
private int MIN_VELOCITY = 800;
/**
* 速度跟蹤器
*/
private VelocityTracker velocityTracker;
/**
* 正在被拖動的view
*/
private View dragView;
/**
* 正在被拖動的position
*/
private int touchPos;
/**
* 默認的最小滑動距離
*/
private int mTouchSlop;
/**
* 滑動時間
*/
private int DURATION_TIME = 300;
/**
* 出現刪除button的Item
*/
private View tempView ;
/**
* 出現刪除button的position
*/
private int tempPos ;
/**
* 刪除button
*/
private Button deleteBtn;
/**
* 移除接口
*/
private RemoveItemListener l;
/**
* 能否夠滑動
*/
private boolean isSlide = false;
public SlideListView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
init(context);
}
private void init(Context context){
mContext = context;
AccelerateInterpolator interpolator = new AccelerateInterpolator();
mScroller = new Scroller(context,interpolator);
velocityTracker = VelocityTracker.obtain();
ViewConfiguration config = ViewConfiguration.get(context);
mTouchSlop = config.getScaledTouchSlop();
setOnTouchListener(this);
}
/**
* 捕捉用戶究竟拖動了哪個view
* 攔截事件
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
final int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
return false;
}
if(action == MotionEvent.ACTION_DOWN){
mLastYvalue = initalYvalue = ev.getY();
initalXvalue = mLastXvalue = ev.getX();
if(!mScroller.isFinished()){
return super.dispatchTouchEvent(ev);
}
touchPos = pointToPosition((int)mLastXvalue, (int)mLastYvalue);
if(touchPos == AdapterView.INVALID_POSITION){
return super.dispatchTouchEvent(ev);
}
GetTracker(ev);
dragView = getChildAt(touchPos - getFirstVisiblePosition());
isSlide = false;
}else if(action == MotionEvent.ACTION_MOVE){
mLastXvalue = ev.getX();
mLastYvalue = ev.getY();
if(velocityTracker == null){
GetTracker(ev);
}
velocityTracker.computeCurrentVelocity(1000);
if(Math.abs(velocityTracker.getXVelocity()) > MIN_VELOCITY
||Math.abs(mLastXvalue - initalXvalue)> mTouchSlop
&& Math.abs(mLastYvalue - initalYvalue) < mTouchSlop){
isSlide = true;
}
}else if(action == MotionEvent.ACTION_UP){
CloseTracker();
}
return super.dispatchTouchEvent(ev);
}
/**
* 消費事件
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
if(isSlide){
switch(ev.getAction()){
case MotionEvent.ACTION_MOVE:
{
mLastXvalue = ev.getX();
final float moveX = mLastXvalue - initalXvalue;
if(Math.abs(moveX) > dip2px(mContext, 20))
{
if(moveX < 0 && Math.abs(moveX)<dip2px(mContext, 100))
{
if(dragView != null){
dragView.scrollTo(Math.abs((int)moveX), 0);
}
return true;
}
}
}
break;
case MotionEvent.ACTION_UP:
{
mLastXvalue = ev.getX();
float scrollDistance = mLastXvalue - initalXvalue;
if(scrollDistance < 0){
if(Math.abs(scrollDistance) < dip2px(mContext, 50)){
//滑動回去
if(dragView != null){
mScroller.startScroll(dragView.getScrollX(), 0, -dragView.getScrollX(), 0, DURATION_TIME);
invalidate();
}
}else if(Math.abs(scrollDistance) > dip2px(mContext, 50) ){
//滑動究竟
if(dragView != null){
mScroller.startScroll(dragView.getScrollX(), 0, (dip2px(mContext, 100) - Math.abs(dragView.getScrollX())), 0,DURATION_TIME);
invalidate();
tempView = dragView;
tempPos = touchPos;
setListener();
}
}
}
}
break;
}
}
return super.onTouchEvent(ev);
}
private void GetTracker(MotionEvent ev){
if(velocityTracker == null){
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(ev);
}
private void CloseTracker() {
if(velocityTracker != null){
velocityTracker.recycle();
velocityTracker.clear();
velocityTracker = null;
}
}
private void setListener(){
deleteBtn = (Button) tempView.findViewById(R.id.delete);
deleteBtn.setOnClickListener(this);
}
@Override
public void computeScroll() {
// TODO Auto-generated method stub
if(mScroller.computeScrollOffset()){
dragView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
super.computeScroll();
}
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
if(tempView != null){
dragView = tempView ;
mScroller.startScroll(dragView.getScrollX(), 0, - dragView.getScrollX(), 0,DURATION_TIME);
invalidate();
deleteBtn.setOnClickListener(null);
deleteBtn = null;
tempView = null;
return true;
}
return false;
}
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
if(l != null){
l.remove(tempPos);
dragView = tempView;
dragView.scrollTo(0, 0);
tempView = null;
invalidate();
}
}
public interface RemoveItemListener{
void remove(int pos);
}
public void setOnRemoveItemListener(RemoveItemListener l){
this.l = l;
}
}
我認為這兩個樣例都非常easy。所以沒有什麽好解釋的,我在以下也會給出下載的地址。假設有什麽疑惑的,或者什麽地方須要改進的請聯系我QQ:571829491。
源代碼下載
仿微信刷新
仿QQ側滑刪除
帶你徹徹底底弄懂Scroller