編輯:關於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 List mViewPagers = 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,表示不攔截
}
詳解Android中實現ListView左右滑動刪除條目的方法
使用Scroller實現絢麗的ListView左右滑動刪除Item效果這裡來給大家帶來使用Scroller的小例子,同時也能用來幫助初步解除的讀者更加熟悉的掌握Scrol
Android常見控件初探
溫故而知新。最近復習了一些android常用控件,接下來,根據android 官方API,總結一下它們的一些常見用法。(開發測試環境為Android4.4) 一、Text
Android用TabLayout實現類似網易選項卡動態滑動效果
此前我們用HorizontalScrollView也實現了類似網易選項卡動態滑動效果,詳見 Android選項卡動態滑動效果這篇文章這裡我們用TabLayout來實現這一
Android端小米推送Demo解析和實現方法
前言最近這幾個月都是在准備找工作和找工作中,付出了很多,總算是有點收獲,所以都沒有怎麼整理筆記。到了最近才有空把自己的筆記整理一下發上來,分享一下我的學習經驗。推送由於最