編輯:關於Android編程
之前的幾篇博客,我測試了View事件分發機制中的一些知識點,我們理解事件分發機制的目的就是為了能夠更好了知道View中事件的傳遞過程進而能夠對於滑動沖突有針對性的解決措施,今天我們通過一個翻頁實例來學習下滑動處理的方式之一-----外部攔截法;
因為要用到翻頁,那麼不可避免的要用到Scroller類,其實拿scrollBy和scrollTo也能做到翻頁的效果,但不足是兩者都是在瞬間完成對View內容的移動,用戶體驗度不好,注意這裡是View內容的移動而不是View本身的移動,而Scroll類卻能夠進行平滑的移動,原因在於他將大的滑動根據設置的時間段分割成多個小的滑動,這樣漸進式的移動體驗度明顯好於前者;
在正式講解案例之前,我們有必要稍微了解下Scroller類:
我們平常使用Scroller的過程如下:
(1)創建Scroller實例;
(2)通過Scroller實例調用startScroll進行滑動事件開始之前的一些設置;
(3)調用startScroll之後需要調用invalidate來進行View重繪,而View重繪中的draw方法就會調用computeScroll方法,這個方法在View中是一個空實現,也就是說我們想要實現彈性滑動就需要重寫computeScroll方法,在這個方法裡面我們可以通過Scroller實例獲取到當前滑動到的位置的scrollX以及scrollY,接著調用scrollTo來移動到這個位置即可,在scrollTo調用結束之後我們同樣需要調用invalidate或者postInvalidate來進行View的重繪,原因很簡單,因為你總不能移動一次就結束吧,後面的移動也需要重繪View的呢,那麼很多人就在想你這裡不也是使用的scrollT麼?為什麼你就能實現彈性滑動呢?第(4)點解釋原因;
(4)其實在上面第(3)點中少說了一個判斷條件,那就是當computeScrollOffset返回true的時候我們才會通過Scroller實例去獲得scrollX以及scrollY,computeScrollOffset返回true表示我們的滑動還沒有停止,還需要繼續滑動,返回false表示滑動已經結束了,這時候當然不再需要調用invalidate或者postInvalidate來進行View的重繪了;
通過上面的講述我們知道了在Scroller中最關鍵的兩個方法是startScroll和computeScrollOffset以及View中沒有實現的computeScroll方法,下面從源碼角度分析下前兩個方法,之後進入實例:
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 / mDuration; }
startScroll的四個參數startX和startY表示滑動起點,dx和dy表示滑動的距離,duration表示滑動的時間,可以看到我們把滑動模式設置為是SCROLL_MODE,設置mDuration這個事件默認是250ms,設置開始滑動時間mStartTime,並且將滑動起點、距離等進行相應的賦值操作;
Scroller$computeScrollOffset
/** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. loc will be altered to provide the * new location. */ 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); final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE[index]; final float d_sup = SPLINE[index + 1]; final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf); 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; }
剛開始判斷滑動是否結束了,結束的話執行第8行返回false,這也就驗證說明computeScrollOffset返回false的話表示滑動停止,否則的話會執行第13行,判斷滑動事件是否在mDuration時間范圍內,因為我們設定的mMode值是SCROLL_MODE,執行16--25行,設置mCurrX以及mCurrY,有了這兩個值之後我們就可以在computeScroll中調用getCurrX以及getCurrY來獲取到這兩個值,並且通過scrollTo來移動到這個位置了;
好了,我們該開始實例部分了:
先來看看效果:
我們采用三個Fragment實現了三個切換的頁面,其中第1個Fragment包含有一個ListView,因為ListView本身是上下滑動的,而我們這裡引入了左右滑動,那麼這種情況下可能就會出現滑動沖突了;第2和3個Fragment分別是顯示一張圖片,你叫簡單,下面退出來各自的布局文件:
fragment1.xml
就僅僅是包含一個ListView而已;
fragment2.xml
僅僅包含一個ImageView;
fragment3.xml
同樣僅僅包含一個ImageView;
接下來是三個Fragment的代碼:
Fragment1.java
public class Fragment1 extends Fragment{ public ListView mListView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { String[] strArray = new String[20]; for(int i = 0;i < 20;i++) strArray[i] = "Item"+i; MyListAdapter adapter = new MyListAdapter(strArray, this.getActivity()); View view = inflater.inflate(R.layout.fragment1, container,false); mListView = (ListView) view.findViewById(R.id.listView); mListView.setAdapter(adapter); return view; } }因為我們的Fragment1裡面有ListView,所以我們需要在他上面綁定數據,這些綁定操作是在Fragment1的onCreate方法裡面進行的;
Fragment2.java
public class Fragment2 extends Fragment{ public ImageView mImageView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment2, container,false); return view; } }就是簡單的返回fragment2對應的View;
Fragment3.kava
public class Fragment3 extends Fragment{ public ImageView mImageView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment3, container,false); return view; } }同樣也只是簡單的返回fragment3對應的View;
主界面的布局activity_main.xml:
可以看到主界面將三個Fragment添加進來,為了到達三個Fragment分別是三個頁面的效果,每個Fragment的layout_width以及layout_height都設置成了match_parent,注意到最外面的布局是我們自己定義的,定義代碼如下:
public class ScrollLayout extends ViewGroup{ //左邊界值 public int leftBoard; //右邊界值 public int rightBoard; //DOWN事件時x的位置 public float mXDown; //上一次MOVE事件的x坐標 public float mXLastMove; //當前MOVE事件的x坐標 public float mXMove; //被認為是滑動的最短距離 public int mTouchSlop; //獲取彈性滑動對象 public Scroller mScroller; public ScrollLayout(Context context) { super(context); } public ScrollLayout(Context context, AttributeSet attrs) { super(context, attrs); //獲取被認為是最短的滑動距離,也就是說滑動超過mTouchSlop才算是滑動 ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); mScroller = new Scroller(getContext()); } public ScrollLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //獲取子View的個數 int childCount = getChildCount(); //對每個子View進行measure操作 for(int i = 0;i < childCount;i++) { View childView = getChildAt(i); //測量每個子View measureChild(childView, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //如果發生變化,則對他進行重新布局 if(changed) { int childCount = getChildCount(); for(int i = 0;i < childCount;i++) { View childView = getChildAt(i); childView.layout(i*childView.getMeasuredWidth(), 0, (i+1)*childView.getMeasuredWidth(), childView.getMeasuredHeight()); } leftBoard = getChildAt(0).getLeft();//獲取左邊界值 rightBoard = getChildAt(childCount-1).getRight();//獲取右邊界值 } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mXDown = ev.getRawX();//獲取DOWN事件下相對於屏幕的x坐標 mXLastMove = mXDown;//初始化上一次MOVE事件的x坐標 break; case MotionEvent.ACTION_MOVE: mXMove = ev.getRawX(); float slideDistance = Math.abs(mXMove-mXLastMove); mXLastMove = mXMove; if(slideDistance > mTouchSlop) { //表示是翻頁操作,攔截事件,攔截事件之後接下來的MOVE和UP事件都將由該View來處理 return true; } break; default: break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_MOVE: //實現具體的移動操作 mXMove = event.getRawX(); int xScrolled = (int)(mXLastMove - mXMove); System.out.println(getScrollX()+xScrolled+getWidth()); //這裡的getScrollX的值等於View的左邊緣和View內容左邊緣的距離 if(getScrollX()+xScrolled < leftBoard) { //表示超出了左邊界,那麼我們需要讓其直接滑到左邊界 scrollTo(leftBoard, 0); return true; }else if(getScrollX()+xScrolled+getWidth() > rightBoard) { //表示超出了右邊界,那麼我們需要讓其直接滑到右邊界 scrollTo(rightBoard-getWidth(), 0); return true; } mXLastMove = mXMove; scrollBy(xScrolled,0); break; case MotionEvent.ACTION_UP: //在UP事件中要判斷我們現在已經滑動到的位置,如果已經滑動超過一半的屏幕,那麼應該翻到下一頁,不到一半的話應該回退滑動 int index = (getScrollX()+getWidth()/2) / getWidth(); int distance = index*getWidth() - getScrollX(); mScroller.startScroll(getScrollX(), 0, distance, 0); invalidate();//重新繪制 break; default: break; } return super.onTouchEvent(event); } @Override public void computeScroll() { //這個方法會在draw方法中調用 //判斷滑動是否完成,返回true表示滑動沒有完成 if(mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } }來解釋下這段代碼的實現:
首先在第22行的構造函數中創建了Scroller實例同時給mTouchSlop進行賦值,這裡的mTouchSlop指的是系統認為的被認為是滑動的最小距離,獲方法就是25/26行代碼;
接著我們重寫了onMeasure方法,該方法是用來測量View的,第44行調用ViewGroup的measureChild方法進行測量;
接著重寫onLayout方法來對View進行布局,因為我們的翻頁操作是在水平方向進行的,所以可以看到第56行layout參數只有水平方向的左右有值;第58行和59行獲取左右邊界值,為了防止我們已經滑到最左邊或者最右邊了繼續滑動還會出現頁面的情況;
接著onInterceptTouchEvent的DOWN事件中主要進行的是mXDown以及mXLastMove的初始化操作;在MOVE事件中,首先獲取到當前滑動到的位置mXMove,計算出當前位置與上次mXLastMove的絕對值差,第74行判斷這個值是否大於默認的認為是滑動的最短距離,如果大於的話會直接返回true來攔截MOVE事件,這樣MOVE事件就不會傳遞到當前View的子View上了,也就是說這時候進行的是水平滑動了,不再會進行ListView的上下滑動;
在onInterceptTouchEvent攔截MOVE事件後就會執行當前View的onTouchEvent方法,我們來看看MOVE操作部分,首先當然是獲取到當前位置,接著第97行是在判斷有沒有超出左邊界,超出的話執行100行直接調用scrollTo讓View滑到左邊界處;第102行判斷有沒有超出右邊界,有的話執行第105行直接執行scrollTo將當前View滑動到右邊界處;如果沒有超出左邊界或者右邊界的話,執行第109行,注意這裡是scrollBy,因為我們這裡的移動是相對於上一次的移動,當然你可以使用scrollTo,但是每次你需要計算出絕對移動的坐標;
在我們結束滑動時會執行第111行的UP方法,該方法裡面首先判斷我們在釋放之後要滑動到哪個Fragment裡面,這裡的滑動就用到了Scroller類了,因為如果手釋放之後一下子滑動到某一個位置,這樣用戶體驗度一點都不好,這裡的index就是我們將要滑到的Fragment編號,接著計算出滑動距離,調用Scroller的startScroll開始滑動,調用invalidate進行重繪,invalidate轉而會去執行draw方法,而draw會去執行computeScroll,也就是我們這裡重寫的computeScroll,該方法裡面首先通過computeScrollOffset判斷滑動沒有停止的話,獲取當前位置,調用scrollTo進行滑動,隨後同樣調用invalidate,這樣間接的在遞歸調用computeScroll,直到滑動結束,達到了彈性滑動的效果;
注意一點的是:在onTouchEvent的DOWN下面要加return true這行代碼;也就是第90行代碼,原因在於,如果你的Fragment裡面是ImageView或者TextView的話,默認情況下是沒有為他們設置clickable以及longclickable屬性的,這回導致當事件傳遞到他們上面的時候他們的onTouchEvent返回false,ScrollLayout作為他們的父View,他的onTouchEvent方法默認返回false,這會導致隨後到來的MOVE事件將不再會由當前Fragment的父View執行,也就是你會發現你的程序將不再能滑動翻頁;
最後就是MainActivity的代碼了,他繼承自FragmentActivity:
public class MainActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }這樣,這個例子就講解結束了;上面我們采用的解決滑動沖突的方法是外部攔截法,也就是說因為事件首先傳遞給父View,那麼我們首先在父View上判斷需不需要攔截這個事件,我們的這個例子很簡單,只要我們在水平方向MOVE的距離大於了系統默認認為滑動的距離,我們就攔截事件,當然實際中可以通過速度、水平方向移動距離大於豎直方向等等來進行判斷,我們實例中的ListView是上下移動的,而我們的頁面切換是水平移動的,當出現水平移動的時候就讓當前父View攔截了事件,而不會將他傳遞給ListView了,這也就解決了滑動沖突啦!
與其他圖片加載庫相同,Glide除了可以加載網絡圖片之外,也可以加載本地圖片。甚至還可以從各種各樣奇葩的數據源中加載圖片。加載網絡圖片很多情況下,我們使用圖片加載庫就是為
一、VC與模板概念的理解MVC本來是存在於Desktop程序中的,M是指數據模型,V是指用戶界面,C則是控制器。使用MVC的目的是將M和V的實現代碼分離,從而使同一個程序
(一)LinearLayout常用屬性1. orientation —–布局組件中的排列方式,有水平(horizontal),垂直(vertica
在開始講述touch事件流程之前,還簡單介紹下TouchEvent,View和ViewGroup。1. MotionEvent 整個事件分發流程中,會