- 這是自定義控件的第二篇學(xué)習(xí)筆記,側(cè)滑菜單,也叫抽屜菜單,在大多數(shù)的應(yīng)用中都有用到,而側(cè)滑的滑字很關(guān)鍵,說(shuō)白了就是移動(dòng);移動(dòng)誰(shuí)呢,自定義控件當(dāng)然是移動(dòng)View,移動(dòng)View有幾種方法:
- 1.通過(guò)改變view在父View的layout位置來(lái)移動(dòng),但是只能移動(dòng)指定的View:
view.layout(l,t,r,b);
view.offsetLeftAndRight(offset);//同時(shí)改變left和right
view.offsetTopAndBottom(offset);//同時(shí)改變top和bottom - 2.通過(guò)改變scrollX和scrollY來(lái)移動(dòng),但是可以移動(dòng)所有的子View;
scrollTo(x,y);
scrollBy(xOffset,yOffset); - 3.通過(guò)改變Canvas繪制的位置來(lái)移動(dòng)View的內(nèi)容:
canvas.drawBitmap(bitmap, left, top, paint)
- 1.通過(guò)改變view在父View的layout位置來(lái)移動(dòng),但是只能移動(dòng)指定的View:
- 本文所寫(xiě)的自定義控件沒(méi)有用到上面的移動(dòng)方法,而是使用ViewDragHelper這個(gè)類(lèi)來(lái)處理移動(dòng)。
- 該類(lèi)是谷歌在2013年開(kāi)發(fā)者大會(huì)上提出的,谷歌能在開(kāi)發(fā)者大會(huì)上提出一個(gè)類(lèi),想必這個(gè)類(lèi)一定非常的強(qiáng)大,他主要用于封裝對(duì)View的觸摸位置,觸摸速度,移動(dòng)距離等的檢測(cè),并通過(guò)接口回調(diào)的方式告訴調(diào)用者,處理ViewGroup中子View的拖拽處理,該類(lèi)的本質(zhì)是一個(gè)對(duì)觸摸事件的解析類(lèi)。
- 對(duì)于ViewDragHelper的使用首先要知道他是在高版本的V4包中(Android 4.4以上的V4包中),其次要明白我們需要用到哪些回調(diào)方法:
- 首先是 boolean tryCaptureView(View child, int pointerId),它用于判斷是否捕獲當(dāng)前子View的觸摸事件,返回值true:就捕獲并解析 false:不處理
- int getViewHorizontalDragRange(View child),獲取view水平方向的拖拽范圍,返回的值用在手指抬起的時(shí)候view緩慢移動(dòng)的動(dòng)畫(huà)計(jì)算上面,最好不要返回0
- int clampViewPositionHorizontal(View child, int left, int dx),控制子View在水平方向的移動(dòng),可以在該方法中控制子View的移動(dòng)范圍,left 表示ViewDragHelper認(rèn)為你想讓當(dāng)前子View的left改變的值(left=child.getLeft()+dx),dx 表示子View水平方向移動(dòng)的距離.
- onViewPositionChanged(View changedView, int left, int top, int dx, int dy),表示當(dāng)子View的位置改變的時(shí)候執(zhí)行,一般用來(lái)做其他子View的伴隨移動(dòng)
- onViewReleased(View releasedChild, float xvel, float yvel),手指抬起的執(zhí)行該方法,xvel:表示x方向的移動(dòng)的速度 正:向右移動(dòng), 負(fù):向左移動(dòng); yvel: 同理表示y方向移動(dòng)的速度 正:向上移動(dòng), 負(fù):向下移動(dòng)
下里面來(lái)一發(fā)控件效果圖:


看完效果,開(kāi)始擼這個(gè)控件:
自定義控件,自定義View中有子View一般都是繼承ViewGroup,但是我們這個(gè)自定義控件對(duì)于子View擺放位置沒(méi)有特殊的需求,本質(zhì)就是將兩個(gè)子View疊放在一起,這時(shí)候我們就不必要去ViewGroup,然后又去重寫(xiě)OnMessure()方法測(cè)量,onLayout()擺放這么麻煩,直接繼承系統(tǒng)已有的控件FrameLayout就可以幫我把事情給做好了。
繼承FrameLayout,添加必要的構(gòu)造方法,首先重寫(xiě)onFinishInflate()方法,在該方法中獲取兩個(gè)子View對(duì)象,并初始化ViewDragHelper,并且將事件的攔截處理移交給ViewDragHelper,該控件只能有兩個(gè)子View,一個(gè)作為側(cè)邊欄菜單頁(yè),一個(gè)為主界面,不等于兩個(gè)則報(bào)出異常,
/**
* Created by 毛麒添 on 2017/2/23 0023.
* 自定義側(cè)滑菜單控件
*/
public class MySlideMenu extends FrameLayout {
private View leftMenu;//左邊欄對(duì)象
private View mainMenu;//主界面對(duì)象
private FloatEvaluator floatEvaluator;//浮點(diǎn)數(shù)計(jì)算器
private IntEvaluator intEvaluator;//整數(shù)計(jì)算器
private ViewDragHelper viewDragHelper;
public MySlideMenu(Context context) {
super(context);
init();
}
public MySlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public MySlideMenu(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
viewDragHelper=ViewDragHelper.create(this,callback);
floatEvaluator=new FloatEvaluator();
intEvaluator=new IntEvaluator();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//檢測(cè)控件異常
if(getChildCount()!=2){
throw new IllegalArgumentException("MySlideMenu only have two childView!");
}
leftMenu = getChildAt(0);
mainMenu = getChildAt(1);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//讓viewDragHelper幫助我們判斷是否攔截
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//讓觸摸事件交給viewDragHelper來(lái)處理
viewDragHelper.processTouchEvent(event);
return true;
}
private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return false;
}
@Override
public int getViewHorizontalDragRange(View child) {
return super.getViewHorizontalDragRange(child);
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return super.clampViewPositionHorizontal(child, left, dx);
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
}
};
}
- 接下來(lái)首先需要在boolean tryCaptureView(View child, int pointerId)方法中設(shè)置捕獲兩個(gè)子view的觸摸事件,并設(shè)置側(cè)滑菜單的拖拽范圍
private int width;//控件寬度
private float dragRange;//拖拽范圍
/**
* 該方法在onMeasure執(zhí)行完成后執(zhí)行,可以在該方法中初始化自己和子View的寬高
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = getMeasuredWidth();
dragRange = width*0.6f;
}
private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child==leftMenu||child==mainMenu;
}
@Override
public int getViewHorizontalDragRange(View child) {
return (int) dragRange;
}
};
- 這樣ViewDragHelper已經(jīng)可以獲取兩個(gè)子View的觸摸事件,則要讓觸摸可以移動(dòng),則應(yīng)該在clampViewPositionHorizontal(View child, int left, int dx)方法中返回left,前面已經(jīng)解釋過(guò),該返回值就是你真正想讓子View移動(dòng)的距離,但是這時(shí)候返回這個(gè)值還不夠,你需要做出限制,不讓他可以完全移動(dòng)出屏幕,具體思想就是當(dāng)你的觸摸的是主界面的子View時(shí)候,該子View的左邊left小于0的時(shí)候,說(shuō)明這是已經(jīng)跑出左邊界,則強(qiáng)制等于0,同理右邊大于主界面的可以拖拽范圍的時(shí)候,則強(qiáng)制等于最大拖拽范圍;然后是onViewPositionChanged(View changedView, int left, int top, int dx, int dy),該方法做子View的伴隨移動(dòng),當(dāng)我們移動(dòng)側(cè)滑菜單子View的時(shí)候,希望可以拖動(dòng)側(cè)滑菜單也能讓主界面的子View伴隨側(cè)滑菜單一起移動(dòng),這樣才能顯示出側(cè)滑效果,要不然只有移動(dòng)主界面才能側(cè)滑就不夠生動(dòng);該方法的思想為當(dāng)時(shí)移動(dòng)側(cè)滑菜單的時(shí)候,側(cè)滑菜單固定,并在同第一個(gè)方法的限制方位邏輯下讓主界面子View的位置伴隨側(cè)滑面板一起移動(dòng),說(shuō)了一大段,還是上代碼實(shí)在,將這兩個(gè)方法改造成:
private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() {
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if(child==mainMenu){
if(left<0) left=0;//限制左邊界
if(left>dragRange)left= (int) dragRange;//限制右邊界
}
return left;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if(changedView==leftMenu){
//固定側(cè)滑菜單
leftMenu.layout(0,0,mainMenu.getMeasuredWidth(),mainMenu.getMeasuredHeight());
int newLeft=mainMenu.getLeft()+dx;
if(newLeft<0) newLeft=0;//限制左邊界
if(newLeft>dragRange) newLeft= (int) dragRange;//限制右邊界
//兩個(gè)菜單一起伴隨滑動(dòng)
mainMenu.layout(newLeft,mainMenu.getTop()+dy,mainMenu.getRight()+dx,mainMenu.getBottom()+dy);
}
};
- 到此該側(cè)滑菜單控件已經(jīng)初步成型,但是這個(gè)側(cè)滑菜單還是太生硬了,拉出或者關(guān)閉側(cè)滑菜單不順滑,這時(shí)候可以使用Scroller來(lái)是移動(dòng)順滑,但是ViewDragHelper就是這么強(qiáng)大,他已經(jīng)將Scroller集成好,我么直接使用就可以,這時(shí)思想為當(dāng)主界面的移動(dòng)范圍小于拖拽的范圍的二分之一,則側(cè)滑菜單自動(dòng)關(guān)閉,大于二分之一,則自動(dòng)打開(kāi),而這些都是手指抬起后的動(dòng)作,所以在 onViewReleased(View releasedChild, float xvel, float yvel)方法做處理:
private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() {
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if(mainMenu.getLeft()<dragRange/2){//在拖拽范圍的左邊,關(guān)閉
close();
}else {//在拖拽范圍的右邊,打開(kāi)
open();
}
}
};
//側(cè)滑面板打開(kāi)
public void open() {
viewDragHelper.smoothSlideViewTo(mainMenu, (int) dragRange,mainMenu.getTop());
ViewCompat.postInvalidateOnAnimation(MySlideMenu.this);//刷新
}
//側(cè)滑面板關(guān)閉
public void close() {
viewDragHelper.smoothSlideViewTo(mainMenu,0,mainMenu.getTop());
ViewCompat.postInvalidateOnAnimation(MySlideMenu.this);//刷新
}
@Override
public void computeScroll() {
if(viewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(MySlideMenu.this);//刷新
}
}
- 到此,側(cè)滑面板大概樣子已經(jīng)初步完成,但是為了向抽屜更加形象,則可以在拖拽的過(guò)程中加入動(dòng)畫(huà)效果,我們可以這樣,可以計(jì)算出拖拽過(guò)程的程度除與最大拖拽長(zhǎng)度,就可以得出百分比,根據(jù)這個(gè)百分比來(lái)執(zhí)行伴隨動(dòng)畫(huà),對(duì),就是這樣,因?yàn)閛nViewPositionChanged(View changedView, int left, int top, int dx, int dy)是在View位置變化的時(shí)候執(zhí)行的方法,所以繼續(xù)對(duì)其改造,并加入放大,透明,遮罩等動(dòng)畫(huà):
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if(changedView==leftMenu){
//固定側(cè)滑菜單
leftMenu.layout(0,0,mainMenu.getMeasuredWidth(),mainMenu.getMeasuredHeight());
int newLeft=mainMenu.getLeft()+dx;
if(newLeft<0) newLeft=0;//限制左邊界
if(newLeft>dragRange) newLeft= (int) dragRange;//限制右邊界
//兩個(gè)菜單一起伴隨滑動(dòng)
mainMenu.layout(newLeft,mainMenu.getTop()+dy,mainMenu.getRight()+dx,mainMenu.getBottom()+dy);
}
//計(jì)算滑動(dòng)百分比
float fraction=mainMenu.getLeft()/dragRange;
//執(zhí)行伴隨動(dòng)畫(huà)
executeAnim(fraction);
}
private void executeAnim(float fraction) {
//移動(dòng)側(cè)邊欄
ViewHelper.setTranslationX(leftMenu,intEvaluator.evaluate(fraction,-leftMenu.getMeasuredWidth()/2,0));
//放大側(cè)邊欄
ViewHelper.setScaleX(leftMenu,floatEvaluator.evaluate(fraction,0.5f,1f));
ViewHelper.setScaleY(leftMenu,floatEvaluator.evaluate(fraction,0.5f,1f));
//改變側(cè)邊欄的透明度
ViewHelper.setAlpha(leftMenu,floatEvaluator.evaluate(fraction,0.3f,1f));
//給側(cè)邊欄背景添加黑色遮罩效果
getBackground().setColorFilter((Integer) ColorUtil.evaluateColor(fraction, Color.BLACK,Color.TRANSPARENT), PorterDuff.Mode.SRC_OVER);
}
- ViewHelper是在一個(gè)版本兼容包中,找不到可以下載我的源碼拿(源碼地址在最下面):

- 接下里就是設(shè)置外部監(jiān)聽(tīng)回調(diào),回調(diào)說(shuō)白了就是空間內(nèi)部發(fā)生的事情需要讓使用者知道,比如你是老板,吩咐員工去外地辦事,員工在外地辦好事打電話給你,就相當(dāng)于回調(diào)(感覺(jué)這個(gè)例子很摳腳);回調(diào)接口定義步驟一般為:
- 1.定義一個(gè)回調(diào)接口,在接口中定義為實(shí)現(xiàn)的邏輯方法
- 2.傳遞一個(gè)實(shí)現(xiàn)了此接口類(lèi)的對(duì)象,并且實(shí)現(xiàn)上述接口中未實(shí)現(xiàn)的方法
- 3.在需要告知外部的地方調(diào)用接口中需要告知的方法
/**
* 設(shè)置外部監(jiān)聽(tīng)回調(diào)
*/
private onDragStateChangeListener listener;
public void setOnDragStateChangeListener(onDragStateChangeListener listener){
this.listener=listener;
}
public interface onDragStateChangeListener{
/**
* 側(cè)滑菜單打開(kāi)
*/
void onOpen();
/**
* 側(cè)滑菜單處于關(guān)閉
*/
void onClose();
/**
*正在拖拽,將此時(shí)的百分比隨時(shí)暴露給調(diào)用者
*/
void Draging(float fraction);
}
- 定義好回調(diào)接口,則在需要告知外部的地方使用邏輯方法,也就是在滑動(dòng)拖拽的過(guò)程中根據(jù)拖拽的百分比來(lái)確定側(cè)滑菜單是打開(kāi)或者關(guān)閉,這時(shí)候就可以枚舉出兩個(gè)打開(kāi)或者關(guān)閉的狀態(tài),方便操作;所以,再次改造onViewPositionChanged(View changedView, int left, int top, int dx, int dy)方法:
private DragState currentState=DragState.STATE_CLOSE;//默認(rèn)是關(guān)閉狀態(tài)
/**
* 枚舉側(cè)滑菜單的開(kāi)關(guān)狀態(tài)
*/
public enum DragState{
STATE_OPEN,STATE_CLOSE
}
/**
* 獲取側(cè)滑菜單狀態(tài)
*/
public DragState getDragState(){
return currentState;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if(changedView==leftMenu){
//固定側(cè)滑菜單
leftMenu.layout(0,0,mainMenu.getMeasuredWidth(),mainMenu.getMeasuredHeight());
int newLeft=mainMenu.getLeft()+dx;
if(newLeft<0) newLeft=0;//限制左邊界
if(newLeft>dragRange) newLeft= (int) dragRange;//限制右邊界
//兩個(gè)菜單一起伴隨滑動(dòng)
mainMenu.layout(newLeft,mainMenu.getTop()+dy,mainMenu.getRight()+dx,mainMenu.getBottom()+dy);
}
//計(jì)算滑動(dòng)百分比
float fraction=mainMenu.getLeft()/dragRange;
//執(zhí)行伴隨動(dòng)畫(huà)
executeAnim(fraction);
//根據(jù)百分比來(lái)值來(lái)確定側(cè)滑菜單是打開(kāi)還是關(guān)閉
if(fraction==0&¤tState!=DragState.STATE_CLOSE){//如果百分比是0,且當(dāng)前狀態(tài)不是關(guān)閉
currentState=DragState.STATE_CLOSE;
//調(diào)用回調(diào)方法
listener.onClose();
}else if(fraction==1&¤tState!=DragState.STATE_OPEN){
currentState=DragState.STATE_OPEN;
//調(diào)用回調(diào)方法
listener.onOpen();
}
if(listener!=null){
//只要listener存在,就將百分比暴露出去
listener.Draging(fraction);
}
}
打這里,側(cè)滑菜單控件類(lèi)的大部分已經(jīng)完成,但是,還差一個(gè)小問(wèn)題,那就是這時(shí)候的側(cè)滑菜單拖動(dòng)打開(kāi)或者關(guān)閉一定要大于或者小于拖動(dòng)范圍的二分之一才能打開(kāi)或者關(guān)閉,而成熟應(yīng)用的的側(cè)滑菜單都是手指一劃就可以打開(kāi)或者關(guān)閉,其實(shí)這是根據(jù)手指滑動(dòng)的速度來(lái)做,上面介紹方法的時(shí)候已介紹過(guò)onViewReleased(View releasedChild, float xvel, float yvel)方法可以獲取X軸和Y軸的速度,所以將其改造為:
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if(mainMenu.getLeft()<dragRange/2){//在拖拽范圍的左邊,關(guān)閉
close();
}else {//在拖拽范圍的右邊,打開(kāi)
open();
}
//當(dāng)用戶稍微滑動(dòng)一下,根據(jù)X軸方向的速度來(lái)打開(kāi)或者關(guān)閉側(cè)滑菜單
if(xvel>200&¤tState!=DragState.STATE_OPEN){
open();
}else if(xvel<-200&¤tState!=DragState.STATE_CLOSE){
close();
}
}
到此,整個(gè)自定義控件側(cè)滑菜單類(lèi)已經(jīng)完成,但是還有一些小瑕疵,當(dāng)側(cè)滑面?zhèn)然姘迨谴蜷_(kāi)狀態(tài)下,發(fā)現(xiàn)主界面的ListView還是可以滑動(dòng),也就是說(shuō)側(cè)滑菜單打開(kāi)的時(shí)候主界面的點(diǎn)擊事件沒(méi)有被攔截,而主界面子View我使用根布局是LinerLayout,所有可以自定義一個(gè)LinerLayout,讓其在側(cè)滑菜單是打開(kāi)的狀態(tài)下攔截事件并消費(fèi)掉就可以了(事件分發(fā)攔截機(jī)制這里不多說(shuō)),并且在打開(kāi)的狀態(tài)下點(diǎn)擊主界面就可以關(guān)閉側(cè)滑菜單,邏輯很簡(jiǎn)單:
/**
* Created by 毛麒添 on 2017/2/24 0024.
* 當(dāng)自定的側(cè)滑菜單打開(kāi)的時(shí)候,右側(cè)的主界面菜單不應(yīng)該能滑動(dòng),
* 自定義一個(gè)LinearLayout攔截并消費(fèi)該觸摸事件
*/
public class MyLinerLayout extends LinearLayout {
private MySlideMenu mySlideMenu;
public MyLinerLayout(Context context) {
super(context);
}
public MyLinerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLinerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setSlideMenu(MySlideMenu mySlideMenu){
this.mySlideMenu=mySlideMenu;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(mySlideMenu!=null&& mySlideMenu.getDragState()== MySlideMenu.DragState.STATE_OPEN){
//如果該側(cè)滑面板是打開(kāi),則攔截消費(fèi)觸摸事件
return true;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(mySlideMenu!=null&& mySlideMenu.getDragState()== MySlideMenu.DragState.STATE_OPEN){
if(event.getAction()==MotionEvent.ACTION_UP){//在側(cè)滑面板打開(kāi)的狀態(tài)時(shí)候點(diǎn)一下主界面應(yīng)該關(guān)閉側(cè)滑面板
mySlideMenu.close();
}
//如果該側(cè)滑面板是打開(kāi),則攔截消費(fèi)觸摸事件
return true;
}
return super.onTouchEvent(event);
}
}
好了,扯了一大堆,總算了是把這個(gè)自定義控件完成了,布局和MainActivity比較簡(jiǎn)單,這里就不貼了。
整體一步一步走下來(lái),還是能對(duì)技術(shù)有不少提升的。如果有錯(cuò)誤,希望大家可以給我提出來(lái),大家一起學(xué)習(xí)進(jìn)步。