自定义控件:侧滑面板
本篇博客講解的是自定義View之側滑面板,應用場景:QQ,知乎,效果圖如下
1. 內容摘要
- 了解ViewDragHelper 的產生及解決的問題
- 掌握ViewDragHelper 的使用步驟
- 掌握屬性動畫的使用
- 掌握狀態更新及事件回調的用法
2. 實現最簡單的拖拽
2.1 實現最簡單的拖拽
在創建DragLayout 時,繼承FrameLayout,這里需要注意兩個問題
為什么不繼承ViewGroup,因為繼承ViewGroup 需要重寫onMeasure()和實現onLayout()方法,自己實現子view 的測量和擺放,在這里我們不需要自己去做測量和擺放,而FrameLayout 已經對這兩個方法進行了具體實現,所以繼承FrameLayout 更加簡單省事
為什么不繼承RelativeLayout,因為這里我們只需要層級關系,不需要相對關系,繼承RelativeLayout界面效果是一樣的,但RelativeLayout 對FrameLayout 多了相對關系的計算,效率會低一些,所以選擇繼承FrameLayout
public class DragLayout extends FrameLayout {public DragLayout(Context context) {super(context);}public DragLayout(Context context, AttributeSet attrs) {super(context, attrs);}public DragLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}}2.2 串聯構造方法
DragLayout 實例化時需要做一些初始化操作,如果我們定義一個init()方法,則我們需要在三個構造方法中都調用init()方法,這樣非常麻煩,我們可以通過串連三個構造方法的方式實現只調用一次init()方法這樣無論是代碼創建還是布局在xml 中都能調用到我們的初始化代碼
public class DragLayout extends FrameLayout {public DragLayout(Context context) {//代碼創建時調用this(context, null);}public DragLayout(Context context, AttributeSet attrs) {//布局在xml 中,實例化時調用this(context, attrs, 0);}public DragLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);//在這里初始化}}2.3 ViewDragHelper 簡介
我們要實現拖拽的效果,則需要自己去解析Touch 事件的ACTION_DOWN,ACTION_MOVE,ACTION_UP,相當的麻煩。所以Google 在2013 年的IO 大會上發布了ViewDragHelper 這個類,用來解決滑動拖拽問題,用這個類可以非常簡單的實現view 的拖拽
2.4 創建ViewDragHelper
由于eclipse 創建項目時,為我們添加的android-support-v4.jar 沒有包含ViewDragHelper,我們需要將最新的android-support-v4.jar 拷貝到libs 下面,然后clean 一下工程。
在這里我們需要關聯android-support-v4.jar 的源碼,通過配置文件的方法來關聯源碼
在libs 下面創建一個android-support-v4.jar.properties 的文件
android-support-v4.jar.properties 中的內容為src = V4 包源碼路徑
我們只需要在第三個構造方法中實現ViewDragHelper 的實例即可
public DragLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);// 在這里初始化// forParent 父類容器// sensitivity 敏感度,越大越敏感,1.0f 是默認值// Callback 回調事件//1.通靜態方法創建拖拽輔助類mViewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);}ViewDragHelper 三個參數的創建的方法源碼中的mTouchSlop 表示觸摸的最小敏感范圍,越小越敏感即在界面拖動的瞬間變化量大于mTouchSlop 時才可以成功觸發拖拽事件
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb){final ViewDragHelper helper = create(forParent, cb);helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));return helper;}2.5 觸摸事件轉交
ViewDragHelper 創建成功了,但它和DragLayout 并沒有任何關系,我們需要讓它們建立關系
//2.轉交觸摸事件@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {//由ViewDragHelper 判斷是否攔截return mViewDragHelper.shouldInterceptTouchEvent(event);};重寫onInterceptTouchEvent 方法,將觸摸事件交給ViewDragHelper 判斷是否攔截,這樣它們就建立了關系,事件攔截后,還需要對攔截到的事件進行處理,注意返回值必須是true
@Overridepublic boolean onTouchEvent(MotionEvent event) {try {//由ViewDragHelper 處理攔截的事件mViewDragHelper.processTouchEvent(event);} catch (Exception e) {}//事件已被處理,所以需要返回truereturn true;};2.6 處理回調事件
ViewDragHelper 在處理觸摸事件時會通過傳入的callback 給我們反饋,通過對回調方法的處理即可實現簡單的拖拽
//3.處理回調事件ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {@Override//返回值決定了child 是否可以被拖拽public boolean tryCaptureView(View child, int pointerId) {//child 被用戶拖拽的孩子//pointerId 多點觸摸的手指idreturn true;}@Override//修正子view 水平方向上的位置,此時還沒有真正的移動,返回值決定view 將移動到的位置public int clampViewPositionHorizontal(View child, int left, int dx) {//left 建議移動到的位置return left;}};2.7 DragLayout 布局到xml 中
給左面板和主面板設置不同的背景顏色便于拖拽時觀察效果,運行工程,即可實現簡單的拖拽
<com.example.draglayout.widget.DragLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/bg"><LinearLayout android:layout_width="match_parent"android:layout_height="match_parent"android:background="#66ff0000"></LinearLayout><LinearLayout android:layout_width="match_parent"android:layout_height="match_parent"android:background="#00ff00"></LinearLayout></com.example.draglayout.widget.DragLayout>3. 限定拖拽范圍
現在左面板和主面板可以任意拖動,本節要實現左面板不動,拖動時,主面板在一定范圍內拖動
3.1 OnFinishInflate()介紹
onFinishInflate()在控件inflate 完成時會被調用,可以在這個方法中查找子控件
- 可以通過findViewById()的方式查找子控件
- 可以通過子view 索引的方式查找子控件
這里采用第二種方式
@Overrideprotected void onFinishInflate() {super.onFinishInflate();//增強代碼的健壯性if(getChildCount() < 2){//必須有兩個子viewthrow new IllegalStateException("Your viewgroup must have two children.");}if(!(getChildAt(0)instanceofViewGroup)||!(getChildAt(1)instanceof ViewGroup)){//子view 必須是viewgroup 的子類throw new IllegalStateException("The child must an instance of viewgroup.");}mLeftContent = getChildAt(0);mMainContent = getChildAt(1);};3.2 獲取控件寬高
在onMeasure()方法中可以獲取到控件的寬高,也可以在onSizeChanged()方法中去獲取寬高,onMeasure()方法調用后會檢測寬高值有沒有變化,有變化才調用onSizeChanged()方法,無變化則不調用,所以onSizeChanged()調用的次數比onMeasure()少,在這里我們在onSizeChanged()方法中去獲取寬高,同時計算出拖拽范圍為寬度的60%
@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mWidth = getMeasuredWidth();mHeight = getMeasuredHeight();//拖拽的范圍mRange = (int) (mWidth * 0.6f);System.out.println("mWidth:"+mWidth+" mHeight:"+mHeight +" mRange:"+mRange);}3.3 限定主面板的拖動范圍
對callback 中的其它幾個方法進行重寫
//3.處理回調事件ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {@Override//返回值決定了child 是否可以被拖拽public boolean tryCaptureView(View child, int pointerId) {//child 被用戶拖拽的孩子//pointerId 多點觸摸的手指idreturn true;}@Overridepublic int getViewHorizontalDragRange(View child) {return super.getViewHorizontalDragRange(child);}@Override//修正子view 水平方向上的位置,此時還沒有真正的移動,返回值決定view 將移動到的位置public int clampViewPositionHorizontal(View child, int left, int dx) {//left 建議移動到位置return left;}@Overridepublic void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {super.onViewPositionChanged(changedView, left, top, dx, dy);}@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {super.onViewReleased(releasedChild, xvel, yvel);}};回調方法中的getViewHorizontalDragRange(View child)方法返回拖拽的范圍,但不會真正限定這個范圍,只要返回一個大于零的值即可。
在ViewDragHelper 源碼中,computeSettleDuration()會調用這個返回值來計算動畫執行的時長,checkTouchSlop()方法會調用這個返回值檢查左面板,主面板是否可以被滑動,所以需要返回一個大于0的值才能實現拖動。
如果返回值為0,左面板,主面板中不能有子view 或子view 沒有對touch 事件做處理,最后觸摸還是會交給ViewDragHelper 處理,所以也能實現拖動
@Override//返回拖拽的范圍,返回一個大于零的值,計算動畫執行的時長,水平方向是否可以被滑開public int getViewVerticalDragRange(View child) {//computeSettleDuration 計算動畫執行的時長//checkTouchSlop 檢查是否可以被滑動(沒有孩子處理觸摸事件,最后返回給DragLayout 處理)return mRange;}限定主面板的拖拽范圍,當建議的值left 小于0 時,讓left 等于0,大于mRange 時等于mRange,然后再將left 返回
@Override// 修正子view 水平方向上的位置,此時還沒有真正的移動,返回值決定view 將移動到的位置public int clampViewPositionHorizontal(View child, int left, int dx) {// child 被用戶拖拽的孩子// left 建議移動到位置// dx 新的位置與舊的位置的差值int oldLeft = mMainContent.getLeft();System.out.println("clamp: left:" + left + " oldLeft:" + oldLeft+ " dx:" + dx);if (child == mMainContent) {left = fixLeft(left);}return left;}/*** 修正左邊的位置,限定拖拽范圍在0 到mRange 間變化** @param left* @return*/private int fixLeft(int left) {if (left < 0) {left = 0;} else if (left > mRange) {left = mRange;}return left;}當控件位置變化時會調用onViewPositionChanged()方法,可以在此方法中做伴隨動畫,狀態更新,事件回調,left 表示最新的水平位置,dx 表示剛剛發生的水平變化量。
此時左面板還可以任意拖動,為了實現拖動左面板時界面表現為拖動主面板,可以對changedView 進行判斷,如果changedView 是左面板,則通過layout()把左面板放回到原來的位置,然后把變化量dx 累加給主面板,再通過layout()方法來移動主面板
@Override// 當控件位置變化時調用,可以做伴隨動畫,狀態更新,事件回調public void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {super.onViewPositionChanged(changedView, left, top, dx, dy);// left 最新的水平位置// dx 剛剛發生的水平變化量System.out.println("onViewPositionChanged: left:" + left + " dx:"+ dx);if (changedView == mLeftContent) {// 如果滑動的是左面板// 1.放回到原來的位置mLeftContent.layout(0, 0, mWidth, mHeight);// 2.把變化量傳遞給主面板,主面板舊的值+變化量int newLeft = mMainContent.getLeft() + dx;// 需要修正左邊值newLeft = fixLeft(newLeft);mMainContent.layout(newLeft, 0, newLeft + mWidth, mHeight);}// offsetLeftAndRight 在低版本中沒有重繪界面,手動調用重繪invalidate();}注意:由于onViewPositionChanged()方法調用前調用了offsetLeftAndRight()方法,此方法在低版本中沒有重繪界面,并且在高版本中也有一個bug,最后一幀沒有被繪制,所以需要手動調用一次invalidate(),否則在低版本中無法實現拖拽效果
4. 結束動畫
拖拽過程中當手指抬起時,需要實現一個打開,關閉面板的動畫,結束動畫可以在 onViewReleased()方法實現
4.1 跳轉的結束動畫
onViewReleased()方法在松手之后會被調用,此時可以做結束動畫,結束動畫只需要考慮需要打開的
情況,其它則為需要關閉情況
- 當水平方向的速度等于 0,并且主面板此時左邊的位置在拖拽范圍中軸線的右邊則需要執行打開動
畫,即 mMainContent.getLeft() > mRange*0.5f
- 當水平方向的速度大于 0 時,則需要執行打開動畫
- 其它情況則需要執行關閉動畫
open(),close()創建為 DragLayout 的方法,這樣方便外界調用
//直接打開protected void open() {mMainContent.layout(mRange, 0, mRange + mWidth, mHeight);}//直接關閉protected void close() {mMainContent.layout(0, 0, 0 + mWidth, mHeight);}4.2 平滑的結束動畫
首先實現平滑的打開動畫,在這里需要用到 ViewDragHelper 提供的一個方法smoothSlideViewTo(child,finalLeft,finalTop),三個參數的意思分別是:
- child 需要平滑移動的 view
- finalLeft 需要移動到的終點左邊位置
- finalTop 需要移動到的終點的上邊位置
smoothSlideViewTo()方法的返回值為 true,表示位置不是最終位置,需要重繪界面
重載一個 open(boolean isSmooth)方法,用參數 isSmooth 標識是調用平滑動畫還是跳轉動畫,open()方法則直接調用 open(true),默認為平滑動畫
protected void open() {open(true);}protected void open(boolean isSmooth) {int finalLeft = mRange;if(isSmooth){//觸發一個平滑動畫if(mViewDragHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){//invalidate();可能會漏幀ViewCompat.postInvalidateOnAnimation(this);};}else{//直接跳轉mMainContent.layout(finalLeft, 0, finalLeft + mWidth, mHeight);}}注意:smoothSlideViewTo()方法返回 true,需要重繪界面,此時不建議使用 invalidate(),因為在動畫的過程中可能會丟幀,推薦使用 ViewCompat.postInvalidateOnAnimation(this),參數一定要傳子 view 所在的容器,因為只有容器才知道子 view 的具體位置
重繪命令調用后,還需要重寫 computScroll()方法,重繪時,系統會在 draw()方法后調用 computScroll(),在該方法中調用 ViewDragHelper 的維持動畫的方法
continueSettling(deferCallbacks)參數 deferCallbacks 表示是否延遲畫下一幀,此處傳入 true,返回值表示是否已經移動到最終位置,如果為 true,還沒有移動到最終位置,需要重繪界面,這樣 computeScroll()方法就會不斷的調用,界面也就會不斷的重繪,直到移動到最終位置
同樣的道理,關閉的平滑動畫只需要修改 finalLeft = 0 即可
protected void close() {close(true);}protected void close(boolean isSmooth) {int finalLeft = 0;if(isSmooth){//觸發一個平滑動畫if(mViewDragHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){//invalidate();可能會漏幀ViewCompat.postInvalidateOnAnimation(this);};}else{mMainContent.layout(finalLeft, 0, finalLeft + mWidth, mHeight);}}5. 伴隨動畫
5.1 分解伴隨動畫
伴隨動畫是拖拽的過程中,左面板,主面板會跟隨拖拽百分比所做的動畫,該動畫需要在onViewPositionChanged()回調方法中實現
- 左面板:縮放動畫,平移動畫,透明度動畫
- 主面板:縮放動畫
- 背景: 亮度變化
5.2 實現伴隨動畫
創建一個方法 dispatchDragEvent(),在 onViewPositionChanged()方法中調用
public void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {super.onViewPositionChanged(changedView, left, top, dx, dy);//...此處代碼省略dispatchDragEvent();invalidate();}實現左面板的縮放動畫
protected void dispatchDragEvent() {//0.0f->1.0f 獲取動畫的百分比,主面板左邊的位置引起的一系列變化float percent = mMainContent.getLeft()*1.0f/mRange;System.out.println("dispatchDragEvent: percent:"+percent);//左面板:縮放動畫,平移動畫,透明度動畫//0.0f ->1.0f percent*0.5f => 0.0f -> 0.5f//尋找規律->拷貝 FloatEvaluator.java 中的估值方法//percent*0.5f + 0.5f => 0.5f -> 1.0f//percent*(1.0f -0.6f)+0.6f => 0.6f -> 1.0f => start + percent(end - start)//兼容低版本引入 nineoldandroid.jar//用 ViewHelper 做屬性動畫//1.縮放動畫ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));}//源碼 FloatEvaluator.java 中拷貝的估值方法public Float evaluate(float fraction, Number startValue, Number endValue) {float startFloat = startValue.floatValue();return startFloat + fraction * (endValue.floatValue() - startFloat);}- 第 3 行通過主面板左邊位置與拖拽范圍的相除可以得到一個 0.0f ->1.0f 的比例值,因為在整個拖拽過
程中,主面板左邊位置的變化是引起一系列變化的原因 - 第 7-10 行可以推出一個公式 start + percent(end - start),即通過 percent 的變化可以計算出 start 到 end 間
的任意值。源碼 FloatEvaluator.java 中已經提供了這么一個方法,將其拷貝到代碼中,即第 20-23 行 - 第 12-16 行為了兼容低版本引入 nineoldandroid.jar 中的 ViewHelper 做屬性動畫
同理可以實現其它伴隨動畫
protected void dispatchDragEvent() {//0.0f->1.0f 獲取動畫的百分比,主面板左邊的位置引起的一系列變化float percent = mMainContent.getLeft()*1.0f/mRange;System.out.println("dispatchDragEvent: percent:"+percent);//左面板:縮放動畫,平移動畫,透明度動畫//0.0f ->1.0f percent*0.5f => 0.0f -> 0.5f//尋找規律->拷貝 FloatEvaluator.java 中的估值方法//percent*0.5f + 0.5f => 0.5f -> 1.0f//percent*(1.0f -0.6f)+0.6f => 0.6f -> 1.0f => start + percent(end - start)//兼容低版本引入 nineoldandroid.jar//用 ViewHelper 做屬性動畫//1.縮放動畫,從 50%->100%ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));//2.平移動畫,從寬度一半在屏幕外->全部移到屏幕內ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth*0.5f, 0f));//3.透明度動畫,從 20%->100%ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.2f, 1.0f));//主面板:縮放動畫,從 100%->80%ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));//背景亮度變化,PorterDuff.Mode.SRC_OVER 疊加模式,直接疊加在上面getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK,Color.TRANSPARENT), PorterDuff.Mode.SRC_OVER);}//源碼 ArgbEvaluator.java 中拷貝的估值方法public Object evaluateColor(float fraction, Object startValue, Object endValue) {//api18 以上的代碼才有透明度的過濾int startInt = (Integer) startValue;int startA = (startInt >> 24) & 0xff;int startR = (startInt >> 16) & 0xff;int startG = (startInt >> 8) & 0xff;int startB = startInt & 0xff;int endInt = (Integer) endValue;int endA = (endInt >> 24) & 0xff;int endR = (endInt >> 16) & 0xff;int endG = (endInt >> 8) & 0xff;int endB = endInt & 0xff;return (int)((startA + (int)(fraction * (endA - startA))) << 24) |(int)((startR + (int)(fraction * (endR - startR))) << 16) |(int)((startG + (int)(fraction * (endG - startG))) << 8) |(int)((startB + (int)(fraction * (endB - startB))));}- 第 27 行疊加模式 PorterDuff.Mode.SRC_OVER 表示直接疊加在上面
- 第 30-48 行 ArgbEvaluator.java 源碼中拷貝的估值方法,api18 以上的代碼才有透明度的過濾
6. 狀態更新及事件回調
6.1 狀態分析
拖拽的狀態可以分為:
- 打開狀態
- 關閉狀態
- 拖拽狀態
通過枚舉定義這三種狀態,且定義默認狀態為關閉
//默認狀態為關閉private Status status = Status.Close;//提供 get()方法public Status getStatus() {return status;}//狀態的枚舉值,有三種狀態,打開,關閉,拖拽中public enum Status{Open,Close,Draging;}6.2 事件回調分析
定義一個事件回調接口,事件回調和狀態密切相關
- 打開狀態時回調 onOpen()方法
- 關閉狀態時回調 onClose()方法
拖拽中回調 onDraging(float percent)方法,并將拖拽百分比傳出去
//接收外界注冊的接口類,以便回調接口方法private OnDragChangeListener onDragChangeListener;//提供 set()方法,讓外界注冊監聽接口類public void setOnDragChangeListener(OnDragChangeListener onDragChangeListener) {this.onDragChangeListener = onDragChangeListener;}//模仿 View 的 OnClickListener 的寫法,定義一個內部的公開的接口public interface OnDragChangeListener{/*** 打開時調用*/public void onOpen();/*** 關閉時調用*/public void onClose();/*** 拖拽中調用* @param percent 當前拖拽的百分比*/public void onDraging(float percent);}6.3 實現狀態更新及事件回調
通過拖拽百分比可以判斷當前的狀態,在 dispatchDragEvent()方法中實現狀態更新及事件回調
- 百分比為 0,則為關閉狀態
- 百分比為 1,則為打開狀態
- 其它百分比,則為拖拽狀態
事件回調需要先做空判斷,拖拽狀態調用頻率高,直接調用即可,打開和關閉可以判斷上次狀態和當
前狀態是否一致,不一致則調用
7. 觸摸優化
7.1 填充界面數據
1.修改主界面 xml,左面板,主面板分別加入 ListView 及頭像
<com.example.draglayout.widget.DragLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/dl"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/bg"tools:context=".MainActivity" ><LinearLayout android:layout_width="match_parent"android:layout_height="match_parent"android:paddingBottom="50dp"android:paddingLeft="10dp"android:paddingRight="50dp"android:orientation="vertical"android:paddingTop="50dp" ><ImageView android:layout_width="50dp"android:layout_height="50dp"android:contentDescription="@null"android:src="@drawable/head" /><ListView android:id="@+id/lv_left"android:layout_width="match_parent"android:layout_height="match_parent" ></ListView></LinearLayout><com.example.draglayout.widget.MyLinearLayout android:id="@+id/ll_my"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#ffffff"android:orientation="vertical" ><RelativeLayout android:layout_width="match_parent"android:layout_height="50dip"android:background="#18b6ef"android:gravity="center_vertical" ><ImageView android:id="@+id/iv_header"android:layout_width="30dp"android:layout_height="30dp"android:layout_marginLeft="10dp"android:contentDescription="@null"android:src="@drawable/head" /></RelativeLayout><ListView android:id="@+id/lv_main"android:layout_width="match_parent"android:layout_height="match_parent" ></ListView></com.example.draglayout.widget.MyLinearLayout></com.example.draglayout.widget.DragLayout>2.ListView 數據源
public class Cheeses {public static final String[] sCheeseStrings = {"Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi","Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale","Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue","Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano"};public static final String[] NAMES = new String[]{"宋江", "盧俊義", "吳用","公孫勝", "關勝", "林沖", "秦明", "呼延灼", "花榮", "柴進", "李應", "朱仝", "魯智 深","武松", "董平", "張清", "楊志", "徐寧", "索超", "戴宗", "劉唐", "李逵", "史進", " 穆弘","雷橫", "李俊", "阮小二", "張橫", "阮小五", " 張順", "阮小七", "楊雄", "石秀", " 解珍"," 解寶", "燕青", "朱武", "黃信", "孫立", "宣贊", "郝思文", "韓滔", "彭玘", "單廷珪 ","魏定國", "蕭讓", "裴宣", "歐鵬", "鄧飛", " 燕順", "楊林", "凌振", "蔣敬", "呂方 ","郭 盛", "安道全", "皇甫端", "王英", "扈三娘", "鮑旭", "樊瑞", "孔明", "孔亮", " 項充","李袞", "金大堅", "馬麟", "童威", "童猛", "孟康", "侯健", "陳達", "楊春", "鄭天壽 ","陶宗旺", "宋清", "樂和", "龔旺", "丁得孫", "穆春", "曹正", "宋萬", "杜遷", "薛永 ", " 施恩","周通", "李忠", "杜興", "湯隆", "鄒淵", "鄒潤", "朱富", "朱貴", "蔡福", "蔡慶", " 李立","李云", "焦挺", "石勇", "孫新", "顧大嫂", "張青", "孫二娘", " 王定六", "郁保四", " 白勝","時遷", "段景柱"};} public class DragLayout extends FrameLayout {private static final String TAG = "TAG";private View mLeftContent;private View mMainContent;private View mRightContent;private int mWidth;private int mHeight;private int mRangeLeft;private ViewDragHelper mDragHelper;private Status mStatus = Status.Close;private Direction mDirction = Direction.Left;private OnDragListener mDragListener;private boolean mScaleEnable = true;private int mRightWidth;private int mRangeRight;public interface OnDragListener {void onClose();void onStartOpen(Direction direction);void onOpen();void onDrag(float percent);}public static enum Status {Open, Close, Draging}public static enum Direction {Left, Right, Default}public Direction getDirction() {return mDirction;}public void setDirction(Direction dirction) {mDirction = dirction;}public DragLayout(Context context) {this(context, null);}public DragLayout(Context context, AttributeSet attrs) {this(context, attrs, 0);}public DragLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);mDragHelper = ViewDragHelper.create(this, mCallBack);mGestureDetector = new GestureDetectorCompat(context, mYGestureListener);}SimpleOnGestureListener mYGestureListener = new SimpleOnGestureListener() {public boolean onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY) {return Math.abs(distanceX) >= Math.abs(distanceY);};};@Overrideprotected void onFinishInflate() {Log.i(TAG, "--onFinishInflate");mLeftContent = (View) getChildAt(0);mRightContent = getChildAt(1);mMainContent = (View) getChildAt(2);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);Log.i(TAG, "--onSizeChanged");mWidth = mMainContent.getMeasuredWidth();mHeight = mMainContent.getMeasuredHeight();mRightWidth = mRightContent.getMeasuredWidth();mRangeLeft = (int) (mWidth * 0.6f);mRangeRight = mRightWidth;}private int mMainLeft = 0;ViewDragHelper.Callback mCallBack = new ViewDragHelper.Callback() {@Overridepublic boolean tryCaptureView(View child, int pointerId) {// 1. 決定當前被拖拽的child是否拖的動。(抽象方法,必須重寫)Log.d(TAG, "tryCaptureView: " + (child == mMainContent) + " : "+ (child == mLeftContent) + " : "+ (child == mRightContent));return true;}@Overridepublic int getViewHorizontalDragRange(View child) {// 2. 決定拖拽的范圍return mWidth;}@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {// 3. 決定拖動時的位置,可在這里進行位置修正。(若想在此方向拖動,必須重寫,因為默認返回0)Log.d(TAG, "clampViewPositionHorizontal left: " + left + " dx: "+ dx + " mRange: " + mRangeLeft);return clampResult(mMainLeft + dx, left);}@Overridepublic void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {// 4. 決定了當View被拖動時,希望同時引發的其他變化Log.d(TAG, "onViewPositionChanged left: " + left + " dx: " + dx);if (changedView == mMainContent) {mMainLeft = left;} else {mMainLeft += dx;}mMainLeft = clampResult(mMainLeft, mMainLeft);if(changedView == mLeftContent || changedView == mRightContent){layoutContent();}dispathDragEvent(mMainLeft);invalidate();};/*** @param releasedChild* 被釋放的孩子* @param xvel* 釋放時X方向的速度* @param yvel* 釋放時Y方向的速度*/@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {// 5. 決定當childView被釋放時,希望做的事情——執行打開/關閉動畫,更新狀態boolean scrollRight = xvel > 1.0f;boolean scrollLeft = xvel < -1.0f;if (scrollRight || scrollLeft) {if (scrollRight && mDirction == Direction.Left) {open(true, mDirction);} else if (scrollLeft && mDirction == Direction.Right) {open(true, mDirction);} else {close(true);}return;}if (releasedChild == mLeftContent && mMainLeft > mRangeLeft * 0.7f) {open(true, mDirction);} else if (releasedChild == mMainContent) {if (mMainLeft > mRangeLeft * 0.3f)open(true, mDirction);else if (-mMainLeft > mRangeRight * 0.3f)open(true, mDirction);elseclose(true);} else if (releasedChild == mRightContent&& -mMainLeft > mRangeRight * 0.7f) {open(true, mDirction);} else {close(true);}}@Overridepublic void onViewDragStateChanged(int state) {if (mStatus == Status.Close && state == ViewDragHelper.STATE_IDLE&& mDirction == Direction.Right) {mDirction = Direction.Left;}}@Overridepublic void onViewCaptured(View capturedChild, int activePointerId) {};};private int clampResult(int tempValue, int defaultValue) {Integer minLeft = null;Integer maxLeft = null;if (mDirction == Direction.Left) {minLeft = 0;maxLeft = 0 + mRangeLeft;} else if (mDirction == Direction.Right) {minLeft = 0 - mRangeRight;maxLeft = 0;}if (minLeft != null && tempValue < minLeft)return minLeft;else if (maxLeft != null && tempValue > maxLeft)return maxLeft;elsereturn defaultValue;}private GestureDetectorCompat mGestureDetector;@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);Log.i(TAG, "--onMeasure");}@Overrideprotected void onLayout(boolean changed, int left, int top, int right,int bottom) {Log.i(TAG, "--onLayout");layoutContent();}private void layoutContent() {mLeftContent.layout(0, 0, mWidth, mHeight);mRightContent.layout(mWidth - mRightWidth, 0, mWidth, mHeight);mMainContent.layout(mMainLeft, 0, mMainLeft + mWidth, mHeight);}@Overridepublic void computeScroll() {if (mDragHelper.continueSettling(true)) {ViewCompat.postInvalidateOnAnimation(this);}}public void setDragListener(OnDragListener mDragListener) {this.mDragListener = mDragListener;}/*** 處理其他同步動畫* * @param mainLeft*/protected void dispathDragEvent(int mainLeft) {// 注意轉換成floatfloat percent = 0;if (mDirction == Direction.Left)percent = mainLeft / (float) mRangeLeft;else if (mDirction == Direction.Right)percent = Math.abs(mainLeft) / (float) mRangeRight;if (mDragListener != null) {mDragListener.onDrag(percent);}// 更新動畫if (mScaleEnable) {animViews(percent);}// 更新狀態Status lastStatus = mStatus;if (updateStatus() != lastStatus) {if(lastStatus == Status.Close && mStatus == Status.Draging){mLeftContent.setVisibility(mDirction == Direction.Left ? View.VISIBLE : View.GONE);mRightContent.setVisibility(mDirction == Direction.Right ? View.VISIBLE : View.GONE);if(mDragListener != null){mDragListener.onStartOpen(mDirction);}}if (mStatus == Status.Close) {if (mDragListener != null)mDragListener.onClose();} else if (mStatus == Status.Open) {if (mDragListener != null)mDragListener.onOpen();}}}private Status updateStatus() {if (mDirction == Direction.Left) {if (mMainLeft == 0) {mStatus = Status.Close;} else if (mMainLeft == mRangeLeft) {mStatus = Status.Open;} else {mStatus = Status.Draging;}} else if (mDirction == Direction.Right) {if (mMainLeft == 0) {mStatus = Status.Close;} else if (mMainLeft == 0 - mRangeRight) {mStatus = Status.Open;} else {mStatus = Status.Draging;}}return mStatus;}private void animViews(float percent) {Log.d(TAG, "percent: " + percent);animMainView(percent);animBackView(percent);}private void animBackView(float percent) {if (mDirction == Direction.Right) {// 右邊欄X, Y放大,向左移動, 逐漸顯示ViewHelper.setScaleX(mRightContent, 0.5f + 0.5f * percent);ViewHelper.setScaleY(mRightContent, 0.5f + 0.5f * percent);ViewHelper.setTranslationX(mRightContent,evaluate(percent, mRightWidth + mRightWidth / 2.0f, 0.0f));ViewHelper.setAlpha(mRightContent, percent);} else {// 左邊欄X, Y放大,向右移動, 逐漸顯示ViewHelper.setScaleX(mLeftContent, 0.5f + 0.5f * percent);ViewHelper.setScaleY(mLeftContent, 0.5f + 0.5f * percent);ViewHelper.setTranslationX(mLeftContent,evaluate(percent, -mWidth / 2f, 0.0f));ViewHelper.setAlpha(mLeftContent, percent);}// 背景逐漸變亮getBackground().setColorFilter(caculateValue(percent, Color.BLACK, Color.TRANSPARENT),PorterDuff.Mode.SRC_OVER);}private void animMainView(float percent) {Float inverseP = null;if (mDirction == Direction.Left) {inverseP = 1 - percent * 0.25f;} else if (mDirction == Direction.Right) {inverseP = 1 - percent * 0.25f;}// 主界面X,Y縮小if (inverseP != null) {if (mDirction == Direction.Right) {ViewHelper.setPivotX(mMainContent, mWidth);ViewHelper.setPivotY(mMainContent, mHeight / 2.0f);} else {ViewHelper.setPivotX(mMainContent, mWidth / 2.0f);ViewHelper.setPivotY(mMainContent, mHeight / 2.0f);}ViewHelper.setScaleX(mMainContent, inverseP);ViewHelper.setScaleY(mMainContent, inverseP);}}public Float evaluate(float fraction, Number startValue, Number endValue) {float startFloat = startValue.floatValue();return startFloat + fraction * (endValue.floatValue() - startFloat);}private int caculateValue(float fraction, Object start, Object end) {int startInt = (Integer) start;int startIntA = startInt >> 24 & 0xff;int startIntR = startInt >> 16 & 0xff;int startIntG = startInt >> 8 & 0xff;int startIntB = startInt & 0xff;int endInt = (Integer) end;int endIntA = endInt >> 24 & 0xff;int endIntR = endInt >> 16 & 0xff;int endIntG = endInt >> 8 & 0xff;int endIntB = endInt & 0xff;return ((int) (startIntA + (endIntA - startIntA) * fraction)) << 24| ((int) (startIntR + (endIntR - startIntR) * fraction)) << 16| ((int) (startIntG + (endIntG - startIntG) * fraction)) << 8| ((int) (startIntB + (endIntB - startIntB) * fraction));}float mDownX;private SwipeListAdapter adapter;@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {if(getStatus() == Status.Close){int actionMasked = MotionEventCompat.getActionMasked(ev);switch (actionMasked) {case MotionEvent.ACTION_DOWN:mDownX = ev.getRawX();break;case MotionEvent.ACTION_MOVE:if(adapter.getUnClosedCount() > 0){return false;}float delta = ev.getRawX() - mDownX;if(delta < 0){return false;}break;default:mDownX = 0;break;}}return mDragHelper.shouldInterceptTouchEvent(ev)& mGestureDetector.onTouchEvent(ev);}public void close(){close(true);}public void close(boolean withAnim) {mMainLeft = 0;if (withAnim) {if (mDragHelper.smoothSlideViewTo(mMainContent, mMainLeft, 0)) {ViewCompat.postInvalidateOnAnimation(this);}} else {layoutContent();dispathDragEvent(mMainLeft);}}public void open(){open(true);}public void open(boolean withAnim) {open(withAnim, Direction.Left);}public void open(boolean withAnim, Direction d) {mDirction = d;if (mDirction == Direction.Left)mMainLeft = mRangeLeft;else if (mDirction == Direction.Right)mMainLeft = -mRangeRight;if (withAnim) {// 引發動畫的開始if (mDragHelper.smoothSlideViewTo(mMainContent, mMainLeft, 0)) {// 需要在computeScroll中使用continueSettling方法才能將動畫繼續下去(因為ViewDragHelper使用了scroller)。ViewCompat.postInvalidateOnAnimation(this);}} else {layoutContent();dispathDragEvent(mMainLeft);}}@Overridepublic boolean onTouchEvent(MotionEvent event) {try {mDragHelper.processTouchEvent(event);} catch (Exception e) {e.printStackTrace();}return true;}public Status getStatus() {return mStatus;}public void switchScaleEnable() {this.mScaleEnable = !mScaleEnable;if (!mScaleEnable) {animBackView(1.0f);}}public void setAdapterInterface(SwipeListAdapter adapter) {this.adapter = adapter;}} public class DragRelativeLayout extends RelativeLayout {private DragLayout dl;public DragRelativeLayout(Context context) {super(context);}public DragRelativeLayout(Context context, AttributeSet attrs) {super(context, attrs);}public DragRelativeLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public void setDragLayout(DragLayout dl) {this.dl = dl;}@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {if (dl.getStatus() != Status.Close) {return true;}return super.onInterceptTouchEvent(event);}@Overridepublic boolean onTouchEvent(MotionEvent event) {if (dl.getStatus() != Status.Close) {if (event.getAction() == MotionEvent.ACTION_UP) {dl.close(true);}return true;}return super.onTouchEvent(event);} }代碼:https://github.com/JackChen1999/DragLayout
總結
以上是生活随笔為你收集整理的自定义控件:侧滑面板的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自定义组合控件:Banner、轮播图、广
- 下一篇: Retrofit2 multpart多文