編輯:關於Android編程
其實github上已經有這個開源庫了,我是個菜鳥,我喜歡用開源庫,同時也非常好奇它的實現原理。很多大神寫的代碼注釋都特別少,可能是他們覺得很簡單就懶得寫了,這點對新手來講就有點坑爹了;所以我只是借助大神的代碼向大家講解下這個的實現原理。介紹原理之前我先說下原創的問題,老實講我博客上講的東西以前絕對有人寫過,很多別人寫的很好,而我只是站在他們的肩膀上幫助下新手。我真的不喜歡那些很繞的代碼,我喜歡來的直一點的,寫出讓超新手都能看的懂的代碼,因為其中注釋代碼的程度到了令人發指的地步(大神可以無視)。
關鍵之處還是在於自定義控件SwipeBackLayout這裡。
public class SwipeBackLayout extends FrameLayout { /** * SwipeBackLayout的主布局 */ private View mContentView; /** * 是一個距離,表示滑動的時候手的移動要大於這個距離才開始移動 控件。如果小於這個距離就不觸發移動控件, * 如 viewpager就是用這個距離來判斷用戶是否翻頁,這個距離打印出來是16px */ private int mTouchSlop; /** * 手指點擊屏幕時的Y坐標 */ private int downY; /** * 手指點擊屏幕時的X坐標 */ private int downX; /** * 手指點擊屏幕時,臨時的X坐標 */ private int tempX; /** * Android裡 Scroller類是為了實現View平滑滾動的一個Helper類 */ private Scroller mScroller; /** * 手機屏幕的寬度 */ private int viewWidth; /** * 表示屏幕是否正在滑動的標記 */ private boolean isSilding; /** * 表示是否finish掉當前的activity */ private boolean isFinish; /** * 獲取系統資源的 drawable文件,帶有陰影的 */ private Drawable mShadowDrawable; /** * SwipeBackLayout依附的activity */ private Activity mActivity; /** * 當前activity裡所存在的 viewpager的集合 */ private ListmViewPagers = new LinkedList ();// 創建一個空的 viewpager的集合 /** * 是否對手勢進行攔截的設置,默認為true。若為false,則SwipeBackLayout這個 viewgroup對手勢不攔截不消費 */ private boolean mEnable = true; public SwipeBackLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // 是一個距離,表示滑動的時候手的移動要大於這個距離才開始移動控件。如果小於這個距離就不觸發移動控件,如 viewpager就是用這個距離來判斷用戶是否翻頁 Log. d("xiao" , "mTouchSlop:" + mTouchSlop ); mScroller = new Scroller(context); // Android裡Scroller 類是為了實現View平滑滾動的一個Helper類 mShadowDrawable = getResources().getDrawable(R.drawable.shadow_left );// 獲取系統資源的 drawable文件 } /** * 把swipeBackLayout附加到指定的activity中,放到 decor頂層窗口下,decorChild上 * 這樣做的作用就是,讓SwipeBackLayout附加到任何activity時,就立於此activity主視圖之上 * * @param activity */ public void attachToActivity(Activity activity) { mActivity = activity; // 傳進來的activity int[] attrs = new int[] { android.R.attr.windowBackground }; // 返回一個與主題Theme定義的 attrs數組對應的typedArray類型數組 TypedArray a = activity.getTheme().obtainStyledAttributes(attrs); // 獲取typedArray數組中指定位置的資源id值 int background = a.getResourceId(0, 0); // 回收TypedArray類型數組 a.recycle(); // 返回頂層窗口裝飾視圖 ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView(); // 返回裝飾視圖的指定位置的view,就是 decor的child,很形象 ViewGroup decorChild = (ViewGroup) decor.getChildAt(0); // 給頂層窗口裝飾視圖的第一個子視圖設置背景資源, // background就是上面獲得的android.R.attr.windowBackground decorChild.setBackgroundResource(background); // decor頂層窗口裝飾視圖移除decorChild,殺了他的兒子 decor.removeView(decorChild); // 這個應該是給SwipeBackLayout添加子view,因為SwipeBackLayout繼承自FrameLayout // 這時,SwipeBackLayout就是decorChild的父布局了 this.addView(decorChild); // 把decorChild當成SwipeBackLayout的contentView進行設置 this.setContentView(decorChild); // 然後給decor添加一個子view,這個this就是SwipeBackLayout decor.addView( this); } /** * 設置主布局視圖 * * @param decorChild */ private void setContentView(View decorChild) { mContentView = (View) decorChild.getParent(); // 返回decorChild的父布局,這個時候父布局就是SwipeBackLayout } /** * 設置手勢 * * @param enable */ public void setEnableGesture( boolean enable) { mEnable = enable; } /** * 事件攔截操作 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (! mEnable) { // false表示不攔截 return false; } // 處理ViewPager沖突問題 ViewPager mViewPager = getTouchViewPager( mViewPagers, ev);// 獲取到觸摸地方的 viewpager if (mViewPager != null && mViewPager.getCurrentItem() != 0) { // 當viewpager 不為空且viewpager不處在第一個item時,swipeBackLayout就不攔截 return super.onInterceptTouchEvent(ev); // 默認返回false,表示不攔截 } switch (ev.getAction()) { case MotionEvent. ACTION_DOWN: downX = tempX = ( int) ev.getRawX(); // 當點擊時候的X坐標 downY = ( int) ev.getRawY(); // 當點擊時候的Y坐標 break; case MotionEvent. ACTION_MOVE: int moveX = ( int) ev.getRawX(); // 滿足此條件屏蔽SildingFinishLayout裡面子類的touch事件 if (moveX - downX > mTouchSlop && Math. abs((int) ev.getRawY() - downY) < mTouchSlop) { // 當水平移動的距離大於16px,且豎直方向的移動距離小於16px時,SwipeBackLayout攔截此次觸摸事件 // 就是說當手指move的時候,滿足上述條件時,觸摸事件就會被SwipeBackLayout的onInterceptTouchEvent方法攔截 // 繼而傳遞給SwipeBackLayout的onTouchEvent方法 return true; } break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if (! mEnable) { return false; } switch (event.getAction()) { case MotionEvent. ACTION_MOVE: int moveX = ( int) event.getRawX(); // 移動後的X坐標 int deltaX = tempX - moveX; // 這個是點擊時和移動後,X坐標差值 Log. d("xiao" , "deltaX:" + deltaX);// 右滑為負值 tempX = moveX; // 給tempX重新賦值 if (moveX - downX > mTouchSlop && Math. abs((int) event.getRawY() - downY) < mTouchSlop) { // 滿足上述條件時,觸摸事件由onInterceptTouchEvent傳遞至onTouchEvent方法中 isSilding = true; // 標記為正在滑動 } if (moveX - downX >= 0 && isSilding) { // 當右滑且處於正在滑動的時候,主布局通過scrollBy整體移動,且通過打印deltaX為負值 mContentView.scrollBy(deltaX, 0); } break; case MotionEvent. ACTION_UP: isSilding = false; // 講滑動標記設置為false Log. d("xiaok" , "mContentView:" + mContentView.getScrollX()); Log. d("xiaok" , "viewWidth:" + viewWidth ); if ( mContentView.getScrollX() <= - viewWidth / 2) { // 當右滑距離超過屏幕寬度的一半時,標記isFinish為true表示滾動出界面,然後滾動出界面 isFinish = true; scrollRight(); } else { // 否則界面回滾至原點,標記isFinish為false scrollOrigin(); isFinish = false; } break; } return true; } /** * 獲取SwipeBackLayout裡面的ViewPager的集合,這裡用到的好像是遞歸思想 * * @param mViewPagers * @param parent */ private void getAlLViewPager(List mViewPagers, ViewGroup parent) { int childCount = parent.getChildCount(); for ( int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); if (child instanceof ViewPager) { mViewPagers.add((ViewPager) child); } else if (child instanceof ViewGroup) { getAlLViewPager(mViewPagers, (ViewGroup) child); } } } /** * 返回我們touch的ViewPager * * @param mViewPagers * @param ev * @return */ private ViewPager getTouchViewPager(List mViewPagers, MotionEvent ev) { if (mViewPagers == null || mViewPagers.size() == 0) { // 如果mViewPagers集合為空,或者mViewPagers的size=0,那麼直接返回空 return null; } Rect mRect = new Rect(); // 創建一個新的空矩形。所有坐標被初始化為0。 for (ViewPager v : mViewPagers) { // 遍歷mViewPagers集合,判斷我現在觸摸的地方是不是在某一個 viewpager范圍裡 // 如果在,那麼就返回這個 viewpager v.getHitRect(mRect); // 獲取每一個viewpager的矩形的坐標值,並賦值給mRect矩形 if (mRect.contains(( int) ev.getX(), ( int) ev.getY())) { // 返回true,如果(x,y)坐標在mRect矩形的范圍內 return v; // 返回viewpager } } return null; // 否則返回空 } @Override protected void onLayout( boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); // 不明白為什麼onLayout方法執行了兩次 Log. d("xiao" , "changed:" + changed); if (changed) { viewWidth = this.getWidth(); Log. d("xiao" , "viewWidth:" + viewWidth ); getAlLViewPager( mViewPagers, this); // 獲得SwipeBackLayout中的ViewPager的集合 } } @Override protected void dispatchDraw(Canvas canvas) { // 當需要繪制子view的時候,才會調用此方法 super.dispatchDraw(canvas); /** * 這個方法是用來繪制右滑退出時,SwipeBackLayout左側的那個陰影效果 */ if ( mShadowDrawable != null && mContentView != null) { int left = mContentView.getLeft() - mShadowDrawable.getIntrinsicWidth(); int right = left + mShadowDrawable.getIntrinsicWidth(); int top = mContentView.getTop(); int bottom = mContentView.getBottom(); mShadowDrawable.setBounds(left, top, right, bottom); mShadowDrawable.draw(canvas); } } /** * 滾動出界面 */ private void scrollRight() { /** * 這裡解釋下getScrollX的意思:返回視圖左邊緣的X坐標,但是是反向的X軸,就是值是相反的 */ final int delta = ( viewWidth + mContentView.getScrollX()); /** * 調用startScroll方法來設置一些滾動的參數,我們在computeScroll()方法中調用scrollTo來滾動item * 當 dx的值為正數時view向左滑動 */ Log. d("xiao" , "delta:" + delta); mScroller.startScroll( mContentView.getScrollX(), 0, -delta + 1, 0, Math. abs(delta)); postInvalidate(); } /** * 滾動到起始位置 */ private void scrollOrigin() { int delta = mContentView.getScrollX(); Log. d("xiao" , "1delta:" + delta); mScroller.startScroll( mContentView.getScrollX(), 0, -delta, 0, Math. abs(delta)); postInvalidate(); } @Override public void computeScroll() { /** * 當我們執行 ontouch或invalidate()或postInvalidate()都會導致這個copmuteScroll方法的執行 * 所以底下加一個判斷,computeSrcollOffset是在startScroll方法啟動時就會返回true */ // 調用startScroll的時候scroller.computeScrollOffset()返回true,從文字上理解是計算偏移量 if ( mScroller.computeScrollOffset()) { mContentView.scrollTo( mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); if ( mScroller.isFinished() && isFinish) { // // 當滑動結束,且當前view滑動出界面時,執行activity,finish的命令 mActivity.finish(); } } } }
說實話,稍微有點基礎的,認真看下代碼和注釋就已經看的懂了。為了幫助新手能看懂我再進行三點講解:
一、首先這個SwipeBackLayout繼承FrameLayout,注意attachToActivity方法。這個方法就是讓SwipeBackLayout包裹住我們XML裡寫的contentView這個步驟方便我們後面實現滑動finish的功能。圖示如下:
二、重寫SwipeBackLayout中的onInterceptTouchEvent和onTouchEvent方法,對手勢進行監聽。手勢監聽問題又涉及到了事件分發問題,這裡只是簡單說下。onInterceptTouchEvent方法表示父布局是否攔截當前事件,true表示攔截,false表示不攔截,且默認為不攔截。當手指向右滑動超過16px且上下滑動距離小於16px時讓onIntecpetTouchEvent方法返回true,表示攔截。攔截的意思是,SwipeBackLayout自身處理這個滑動事件,就不會傳遞給子view了。
然後再onTouchEvent方法的手指觸摸移動方法ACTION_MOVE中,根據不斷算出move移動的距離來對我們的SwipeBackLayout進行scrollBy方法。
在手指抬起ACTION_UP中,判斷是否右滑距離超過屏幕的一半,如果超過一半,則將SwipeBackLayout滾動出界面,finish掉當前activity,反之,則將SwipeBackLayout滾動回原點。
最後貼下scrollTo和scrollBy的區別,直接看源碼!
三、注意橫向滑動的事件沖突,如viewpager的右滑。當viewpager的currentItem不等於0的時候,右滑事件應該是讓viewpager觸發的,這個時候SwipeBackLayout是不應該對滑動事件進行攔截的。也就是onInterceptTouchEvent方法這個時候返回false,表示不攔截,把事件交給子view中的viewpager進行處理。
if (mViewPager != null && mViewPager.getCurrentItem() != 0) { // 當viewpager 不為空且viewpager不處在第一 //個item時,swipeBackLayout就不攔截 return super.onInterceptTouchEvent(ev); // 默認返回false,表示不攔截 }
使用Scroller實現絢麗的ListView左右滑動刪除Item效果這裡來給大家帶來使用Scroller的小例子,同時也能用來幫助初步解除的讀者更加熟悉的掌握Scrol
溫故而知新。最近復習了一些android常用控件,接下來,根據android 官方API,總結一下它們的一些常見用法。(開發測試環境為Android4.4) 一、Text
此前我們用HorizontalScrollView也實現了類似網易選項卡動態滑動效果,詳見 Android選項卡動態滑動效果這篇文章這裡我們用TabLayout來實現這一
前言最近這幾個月都是在准備找工作和找工作中,付出了很多,總算是有點收獲,所以都沒有怎麼整理筆記。到了最近才有空把自己的筆記整理一下發上來,分享一下我的學習經驗。推送由於最