Android自定義控件總結


自定義控件分類:
1、使用系統控件,實現自定義的效果
2、自己定義一個類繼承View ,如textView、ImageView等,通過重寫相關的方法來實現新的效果
3、自己定義一個類繼承ViewGroup,實現相應的效果
繼承view類或viewgroup類,來創建所需要的控件。一般來講,通過繼承已有的控件來自定義控件要簡單一點。
 
介紹下實現一個自定義view的基本流程
1.明確需求,確定你想實現的效果。
2.確定是使用組合控件的形式還是全新自定義的形式,組合控件即使用多個系統控件來合成一個新控件,你比如titilebar,這種形式相對簡單。
3.如果是完全自定義一個view的話,你首先需要考慮繼承哪個類,是View呢,還是ImageView等子類。
4.根據需要去復寫View#onDraw、View#onMeasure、View#onLayout方法。 
5.根據需要去復寫dispatchTouchEvent、onTouchEvent方法。 
6.根據需要為你的自定義view提供自定義屬性,即編寫attr.xml,然后在代碼中通過TypedArray等類獲取到自定義屬性值。 
7.需要處理滑動沖突、像素轉換等問題。

 

繪制流程
onMeasure測量view的大小,設置自己顯示在屏幕上的寬高。
onLayout確定view的位置,父view 會根據子view的需求,和自身的情況,來綜合確定子view的位置(確定他的大小)。
onDraw(Canvas)繪制 view 的內容。
在主線程中 拿到view調用Invalide()方法,刷新當前視圖,導致執行onDraw執行,如果是在子線程用postinvalidate,或者不需要一直刷新用postinvalidateDelayed(300),每隔300毫秒刷新一次。
如果希望視圖的繪制流程(三步)可以完完整整地重新走一遍,就不能使用invalidate()方法,而應該調用requestLayout()了。

 

事件沖突
當點擊事件發生時,事件最先傳遞給Activity,Activity會首先將事件將被所屬的Window進行處理,即調用superDispatchTouchEvent()方法。

通過觀察superDispatchTouchEvent()方法的調用鏈,我們可以發現事件的傳遞順序:

  • PhoneWinodw.superDispatchTouchEvent()
  • DecorView.dispatchTouchEvent(event)
  • ViewGroup.dispatchTouchEvent(event)

事件一層層傳遞到了ViewGroup里。

當事件出現時,先從頂級開始往下傳遞,每到一個子view,看他的onInterceptTouchEvent 方法是否攔截,ontouch是否消費方法,如果沒有繼續向下dispatchTouchEvent分發事件,都不處理向上傳,當回到頂級,若頂層(activity)也不對此事件進行處理,此事件相當於消失了(無效果)。
View沒有onInterceptTouchEvent()方法,一但有點擊事件傳遞給它,它的ouTouchEvent()方法就會被調用。

當事件發現沖突的時候,處理的原則就是事件分發機制,有倆種方法:
  1. 外部處理,重寫父view的onInterceptTouchEvent ,MotionEvent的事件全部返回false,不攔截;
  2. 內部處理。重寫子view的dispatchTouchEvent,通過requestDisallowInterceptTouchEvent方法(這個方法可以在子元素中干預父元素的事件分發過程),請求父控件不攔截自己的事件,true是不攔截,false是攔截。
 
Activity/Window/View三者的差別,Activity 如何顯示到屏幕上
ActivityManager :用於維護與管理 Activity 的啟動與銷毀
WindowManagerService:用來創建、管理和銷毀Window。
Activity像一個工匠(控制單元),Window像窗戶(承載模型),View像窗花(顯示視圖) LayoutInflater像剪刀,Xml配置像窗花圖紙。
  • 在Activity中執行setContentView方法后會執行PhoneWindow的setContentView,在該方法中會生成DecorView 組件作為應用窗口的頂層視圖。
  • DecorView 是PhoneWindow的內部類,繼承至FrameLayout,DecorView 會添加一個id為content的FrameLayout作為根布局,Activity的xml文件會通過LayoutInflater的inflate方法解析成View樹添加到id為content的FrameLayout中。
  • ViewRoot不是View,它的實現類是ViewRootImpl,ViewRoot是DecorView的“管理者”。它是DecorView和WindowManager之間的紐帶。
  • 畢竟“管理者”,所以View的繪制流程是從ViewRoot的performTraversals方法開始的。所以performTraversals方法依次調用performMeasure,performLayout和performDraw三個方法。然后各自經歷measure、layout、draw三個流程最終顯示在用戶面前,用戶在點擊屏幕時,點擊事件隨着Activity傳入Window,最終由ViewGroup/View進行分發處理。
 
ActivityThread,Ams,Wms的工作原理
ActivityThread: 運行在應用進程的主線程上,響應 ActivityMananger、Service 啟動、暫停Activity,廣播接收等消息。 
Ams:統一調度各應用程序的Activity、內存管理、進程管理。

 

自定義控件有幾個重要方法:
 1、實現構造方法 。(三個構造方法)
第二個是創建布局文件調用的構造函數
 2、onMeasure測量view的大小。 設置自己顯示在屏幕上的寬高。
 

MeasureSpec有SpecMode和SpecSize倆個屬性。對於普通view,其MeasureSpec是由父容器的MeasureSpec和自身的layoutparams共同決定的,那么針對不同的父容器和view不同layoutparams,view可以有多種不同的MeasureSpec。
SpecMode有三類。
unspecified:父View不對子View做任何限制,需要多大給多大,一般不關心這個模式
exactly:view的大小就是SpecSize指定的大小。相當於mach_parents和具體數值
at_most:父容器指定了一個specsize,view不能大於這個值。具體的值看view,相當於wrap_content
日常開發中我們接觸最多的不是MeasureSpec而是LayoutParams,在View測量的時候,LayoutParams會和父View的MeasureSpec相結合被換算成View的MeasureSpec,進而決定View的大小。
  • 對於頂級View(DecorView)其MeasureSpec由窗口的尺寸和自身的LayoutParams共同確定的。
  • 對於普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同確定的。
重寫onMeasure為了測量view的大小, 設置自己顯示在屏幕上的寬高。
如果寫的自定義View是直接繼承View的,而且寫了super.measure(),則會默認給這個View設置了一個測量寬和高,這個寬高是多少?
 //如果View沒有設置背景,那么返回android:minWidth這個屬性的值,這個值可以為0
 //如果View設置了背景,那么返回android:minWidth和背景最小寬度兩者中的最大值。
如果寫的自定義View是繼承現有控件的,而且寫了super.measure(),則會默認使用那個現有控件的測量寬高,你可以在這個已經測量好的寬高上做修改,當然也可以全部重新測過再改掉。
如果我們的View直接繼承ImageView,ImageView已經運行了一大堆已經寫好的代碼測出了相應的寬高。我們可以在它基礎上更改即可。比如我們的Image2View是一個自定義的正方形的ImageView:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //這里已經幫我們測好了ImageView的規則下的寬高,並且通過了setMeasuredDimension方法賦值進去了。
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    //我們這里通過getMeasuredWidth/Height放來獲取已經賦值過的測量的寬和高
    //然后在ImageView幫我們測量好的寬高中,取小的值作為正方形的邊。
    //然后重新調用setMeasuredDimension賦值進去覆蓋ImageView的賦值。
    //我們從頭到位都沒有進行復雜測量的操作,全靠ImageView。哈哈
    int width = getMeasuredWidth();
    int height = getMeasuredHeight();
    if (width < height) {
        setMeasuredDimension(width, width);
    } else {
        setMeasuredDimension(height, height);
    }
}

  

 
  • setMeasuredDimension后才能getmeasure寬高,super里做了這步,因為這方法是用來設置view測量的寬和高。
  • 如果需要重新測量或者動態改變自定義控件大小那就需要根據自己需求重寫規則makeMeasureSpec,簡單說就是規則改變了就需要重寫規則。
  • 重寫onMeasure方法的目的是為了能夠給view一個warp_content屬性下的默認大小,因為不重寫onMeasure,那么系統就不知道該使用默認多大的尺寸。如果不處理,那wrap_content就相當於match_parent。所以自定義控件需要支持warp_content屬性就重寫onMeasure。那如何重寫呢?
  • 可以自己嘗試一下自定義一個View,然后不重寫onMeasure()方法,你會發現只有設置match_parent和wrap_content效果是一樣的,事實上TextView、ImageView 等系統組件都在wrap_content上有自己的處理。
    @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec);
    
          //指定一組默認寬高,至於具體的值是多少,這就要看你希望在wrap_cotent模式下
          //控件的大小應該設置多大了
          int mWidth = 200;
          int mHeight = 200;
    
          int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
          int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    
          int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
          int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    
          if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
              setMeasuredDimension(mWidth, mHeight);
          } else if (widthSpecMode == MeasureSpec.AT_MOST) {
              setMeasuredDimension(mWidth, heightSpecSize);
          } else if (heightSpecMode == MeasureSpec.AT_MOST) {
              setMeasuredDimension(widthSpecSize, mHeight);
          }
      }
    

      

 
3、onLayout設置自己顯示在屏幕上的位置(只有在自定義ViewGroup中才用到),這個坐標是相對於當前視圖的父視圖而言的。view自身有一些建議權,決定權在 父view手中。 
調用場景:在view需要給其孩子設置尺寸和位置時被調用。子view,包括孩子在內,必須重寫onLayout(boolean, int, int, int, int)方法,並且調用各自的layout(int, int, int, int)方法。
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		
		for (int i = 0; i < getChildCount(); i++) {
			View view = getChildAt(i); // 取得下標為I的子view

			/**
			 * 父view 會根據子view的需求,和自身的情況,來綜合確定子view的位置,(確定他的大小)
			 */
			//指定子view的位置  ,  左,上,右,下,是指在viewGround坐標系中的位置
			view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight());	

		}	
	}

  

 4、onDraw(Canvas)繪制 view 的內容。控制顯示在屏幕上的樣子(自定義viewgroup時不需要這個)
/*
* backgroundBitmap 要繪制的圖片
* left 圖片的左邊界
* top 圖片的上邊界
* paint 繪制圖片要使用的畫筆
*/
canvas.drawBitmap(backgroundBitmap, 0, 0, paint);


View和ViewGroup的區別
  1. ViewGroup需要控制子view如何擺放的時候需要實現onLayout。
  2. View沒有子view,所以不需要onLayout方法,需要的話實現onDraw
  3. 繼承系統已有控件或容器,比如FrameLayou,它會幫我們去實現onMeasure方法中,不需要去實現onMeasure, 如果繼承View或者ViewGroup的話需要warp_content屬性的話需要實現onMeasure方法
  4. 自定義ViewGroup大多時候是控制子view如何擺放,並且做相應的變化(滑動頁面、切換頁面等)。自定義view主要是通過onDraw畫出一些形狀,然后通過觸摸事件去決定如何變化
 
scrollTo()和scrollBy()
scrollTo:將當前視圖的基准點移動到某個點(坐標點);
ScrollBy移動當前view內容 移動一段距離。
 
 
getHeight()和getMeasuredHeight()的區別:
有倆種方法可以獲得控件的寬高
  • getMeasuredHeight(): 控件實際的大小
獲取測量完的高度,只要在onMeasure方法執行完,就可以用它獲取到寬高,在自定義view內使用view.measure(0,0)方法可以主動通知系統去測量,然后就可以直接使用它獲取寬高。measure里調用的onmeasure
  • getHeight():控件顯示的大小,必須在onLayout方法執行完后,才能獲得寬高,這種方法不好,得等所以的都測量完才能獲得。獲取到的是屏幕上顯示的高度,getMeasuredHeight是實際高度。
view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
					@Override
					public void onGlobalLayout() {
					headerView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
					int headerViewHeight = headerView.getHeight();
					//直接可以獲取寬高
			}
		});

  

這倆個一般情況是一樣的,但是在viewgroup里getWidth是父類給子view分配的空間:右邊-左邊。系統可能需要多次measure才能確定最終的測量寬高,很可能是不准確的,好習慣是在onlayout里獲得測量寬高或最終寬高。
 
還有一種獲得控件寬高的方法:
onSizeChanged:當該組件的大小被改變時回調此方法
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		// 當尺寸有變化的時候調用
		mHeight = getMeasuredHeight();
		mWidth = getMeasuredWidth();
		// 移動的范圍
		mRange = (int) (mWidth * 0.6f);
		
	}

  

 
onFinishInflate
當xml被填充完畢時調用,在自定義viewgroup中,可以通過這個方法獲得子view對象
protected void onFinishInflate() {
		super.onFinishInflate();
		// 容錯性檢查 (至少有倆子View, 子View必須是ViewGroup的子類)
		if(getChildCount() < 2){
			throw new IllegalStateException("布局至少有倆孩子. Your ViewGroup must have 2 children at least.");
		}
		if(!(getChildAt(0) instanceof ViewGroup && getChildAt(1) instanceof ViewGroup)){
			throw new IllegalArgumentException("子View必須是ViewGroup的子類. Your children must be an instance of ViewGroup");
		}
		
		mLeftContent = (ViewGroup) getChildAt(0);
		mMainContent = (ViewGroup) getChildAt(1);
	}

  

 
其他概念
  • view的位置參數有left、right、top、bottom(可以getXX獲得),3.0后又增加了幾個參數:x、y、translationX和translationY,其中x和y是view左上角的坐標,而translationX和translationY是view左上角相對於父容器的偏移量。這些參數都是相對於父容器的坐標,並且translationX和translationY的默認值是0,他們的換算關系是:x=left+translationX   y=top+ translationY。需要注意的是,view在平移的過程中,top和left表示的是原始左上角的位置信息,其值並不會發生改變,此時發生改變的是x、y、translationX和translationY這四個參數
  • touchslop是系統所能識別出的被認為是滑動的最小距離,比如當倆次滑動事件的滑動距離小於這個值,我們就可以認為未達到滑動距離的臨界值
 
 



 
事件分發
View中 setOnTouchListener的onTouch,onTouchEvent,onClick的執行順序

追溯到View的dispatchTouchEvent源碼查看,有這么一段代碼

public boolean dispatchTouchEvent(MotionEvent event) {  
        if (!onFilterTouchEventForSecurity(event)) {  
            return false;  
        }  

        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        }  
        return onTouchEvent(event);  
    }
}

  

當以下三個條件任意一個不成立時,

  • mOnTouchListener不為null
  • view是enable的狀態
  • mOnTouchListener.onTouch(this, event)返回true,

函數會執行到onTouchEvent。在這里我們可以看到,首先執行的是mOnTouchListener.onTouch的方法,然后是onTouchEvent方法

繼續追溯源碼,到onTouchEvent()觀察,發現在處理ACTION_UP事件里有這么一段代碼

if (!post(mPerformClick)) {        performClick();    }

  

此時可知,onClick方法也在最后得到了執行

所以三者的順序是:
  1. setOnTouchListener() 的onTouch
  2. onTouchEvent()
  3. onClick()
view的事件分發:View為啥會有dispatchTouchEvent方法?
View可以注冊很多事件監聽器,事件的調度順序是onTouchListener> onTouchEvent>onLongClickListener> onClickListener
 
View的事件分發
 
當事件出現時,先從頂級父類開始往下傳遞,每到一個孩子,看他的onInterceptTouchEvent 方法是否攔截,ontouch是否消費方法,如果沒有繼續向下dispatchTouchEvent分發事件,都不處理回到頂級的父空間,若頂層(activity)也不對此事件進行處理,此事件相當於消失了(無效果)。
Touchevent 中,返回值是 true ,則說明消耗掉了這個事件,返回值是 false ,則沒有消耗掉,會繼續傳遞下去

1)dispatchTouchEvent:這個方法用來分發事件,如果攔截了交給ontouchevent處理,對應上面的和ontounch理解,否則傳給子view

2)onInterceptTouchEvent: 這個方法用來攔截事件,返回true表示攔截(不允許事件繼續向子view傳遞),false不攔截,如果自定義viewgroup里某個子view需要自己處理事件,就需要重寫改方法,讓他返回false。

3)onTouchEvent: 這個方法用來處理事件。Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View的。,子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。

 
onTouchEvent
一般自定義控件都需要去重寫onTouchEvent方法。
1.在down的時候去記錄坐標點
getX/getY獲取相對於當前View左上角的坐標,getRawX/getRawY獲取相對於屏幕左上角的坐標。
比如接觸到按鈕時,x,y是相對於該按鈕左上點的相對位置。而rawx,rawy始終是相對於屏幕的位置。

2.move的時候計算偏移量,並用scrollTo()或scrollBy()方法移動view。這倆個方法都是快速滑動,是瞬間移動的。

注意:滾動的並不是viewgroup內容本身,而是它的矩形邊框。

 

三種滑動的方法

  1. 使用scrollTo()或scrollBy()
  2. 動畫
  3. 實時改變layoutparams,重新布局

 

 
如果讓view在一段時間內移動到某個位置(不是快速滑動,彈性)方法:
 a.使用自定義動畫(讓view在一段時間內做某件事),extends Animation,
總要修改的是translationx.y這倆個值
(相對於父容器移動的距離)
 b.使用Scoller
         c.offsetTopAndBottom(offset)和offsetLeftAndRight(offset);,這個好理解,左右移動多少
模板(固定代碼):
 
	 * @param startX	開始時的X坐標
	 * @param startY	開始時的Y坐標
	 * @param disX		X方向 要移動的距離
	 * @param disY		Y方向 要移動的距離
myScroller.startScroll(getScrollX(),0,distance,0,Math.abs(distance));//持續的時間
/**
	 * Scroller不主動去調用這個方法
	 * 而invalidate()可以掉這個方法
	 * invalidate->draw->computeScroll
	 */
	@Override
	public void computeScroll() {
		super.computeScroll();
		if(scroller.computeScrollOffset()){//返回true,表示動畫沒結束
			scrollTo(scroller.getCurrX(), 0);
			invalidate();
		}
	}

  

scroller的工作原理:scroller本身並不能實現view的滑動,它需要配合view的的comouteScroll方法才能完成彈性滑動的效果,它不斷的讓view重繪,而每一次重繪距滑動起始時間會有有一個時間間隔,通過這個時間間隔srcoller就可以得出view當前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成view的滑動,就這樣,view的每一次重繪就會導致view進行小幅度的滑動,而多次的小幅度滑動就組成了彈性動畫。
 
3.在up的時候,判斷應顯示的頁面位置,並計算距離、滑動頁面。見下:

 


 

 
ontouch觸摸事件也可以交給其他工具類去實現
1.GestureDetector(手勢識別器)去處理,可以在onFling里處理快速滑動事件,同時在MotionEvent.ACTION_UP里處理沒有快速滑動的時候。有時候比ontounch更方便,比如處理onfling,onscroll(按下屏幕后拖動),長安,雙擊等事件。
mDectector.onTouchEvent(event);// 委托手勢識別器處理觸摸事件
...	
case MotionEvent.ACTION_UP:
			
			if(!isFling){//  在沒有發生快速滑動的時候,才執行按位置判斷currid
				int nextId = 0;
				if(event.getX()-firstX>getWidth()/2){ // 手指向右滑動,超過屏幕的1/2  當前的currid - 1
					nextId = currId-1;
				}else if(firstX - event.getX()>getWidth()/2){ // 手指向左滑動,超過屏幕的1/2  當前的currid + 1
					nextId = currId+1;
				}else{
					nextId = currId;
				}	
				moveToDest(nextId);
//				scrollTo(0, 0);
			}
		
			isFling = false;
			
			break;
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
					float distanceY) {
				//移動屏幕 	
				/**
				 * 移動當前view內容 移動一段距離
				 * disX	 X方向移的距離		為正是,圖片向左移動,為負時,圖片向右移動 
				 * disY  Y方向移動的距離
				 */
				scrollBy((int) distanceX, 0);
		
				return false;
			}
			
			@Override
			/**
			 * 發生快速滑動時的回調
			 */
			public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
					float velocityY) {
				isFling = true;
				if(velocityX>0 && currId>0){ // 快速向右滑動。當前子view的下標
					currId--;
				}else if(velocityX<0 && currId<getChildCount()-1){ // 快速向左滑動
					currId++;
				}
				
				moveToDest(currId);
				
				return false;
			}
			
			@Override
			public boolean onDown(MotionEvent e) {
				return false;
			}
		});

  

 
2. 交給ViewDragHelper去處理
用法:
	// a.初始化 (通過靜態方法) 
		mDragHelper = ViewDragHelper.create(this , mCallback);
	
// b.傳遞觸摸事件
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		// 傳遞給mDragHelper
		return mDragHelper.shouldInterceptTouchEvent(ev);
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		try {
			mDragHelper.processTouchEvent(event);
		} catch (Exception e) {
			e.printStackTrace();
		}
		// 返回true, 持續接受事件
		return true;
	}
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
		// c. 重寫事件
		
		// 1. 根據返回結果決定當前child是否可以拖拽
		// child 當前被拖拽的View
		// pointerId 區分多點觸摸的id
		@Override
		public boolean tryCaptureView(View child, int pointerId) {
			Log.d(TAG, "tryCaptureView: " + child);
			return true;
		};
		
		@Override
		public void onViewCaptured(View capturedChild, int activePointerId) {
			Log.d(TAG, "onViewCaptured: " + capturedChild);
			// 當capturedChild被捕獲時,調用.
			super.onViewCaptured(capturedChild, activePointerId);
		}
 
		@Override
		public int getViewHorizontalDragRange(View child) {
			// 返回拖拽的范圍, 不對拖拽進行真正的限制. 僅僅決定了動畫執行速度
			return mRange;
		}
		
		// 2. 根據建議值 修正將要移動到的(橫向)位置   (重要)
		// 此時沒有發生真正的移動
		public int clampViewPositionHorizontal(View child, int left, int dx) {
			// child: 當前拖拽的View
			// left 新的位置的建議值, dx 位置變化量
			// left = oldLeft + dx;
			Log.d(TAG, "clampViewPositionHorizontal: " 
					+ "oldLeft: " + child.getLeft() + " dx: " + dx + " left: " +left);
			
			if(child == mMainContent){
				left = fixLeft(left);
			}
			return left;
		}
 
		// 3. 當View位置改變的時候, 處理要做的事情 (更新狀態, 伴隨動畫, 重繪界面)
		// 此時,View已經發生了位置的改變
		@Override
		public void onViewPositionChanged(View changedView, int left, int top,
				int dx, int dy) {
			// changedView 改變位置的View
			// left 新的左邊值
			// dx 水平方向變化量
			super.onViewPositionChanged(changedView, left, top, dx, dy);
			Log.d(TAG, "onViewPositionChanged: " + "left: " + left + " dx: " + dx);
			
			int newLeft = left;
			if(changedView == mLeftContent){
				// 把當前變化量傳遞給mMainContent
				newLeft = mMainContent.getLeft() + dx;
			}
			
			// 進行修正
			newLeft = fixLeft(newLeft);
			
			if(changedView == mLeftContent) {
				// 當左面板移動之后, 再強制放回去.
				mLeftContent.layout(0, 0, 0 + mWidth, 0 + mHeight);
				mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
			}
			// 更新狀態,執行動畫
			dispatchDragEvent(newLeft);
			
			// 為了兼容低版本, 每次修改值之后, 進行重繪
			invalidate();
		}
 
		// 4. 當View被釋放的時候, 處理的事情(執行動畫)
		@Override
		public void onViewReleased(View releasedChild, float xvel, float yvel) {
			// View releasedChild 被釋放的子View 
			// float xvel 水平方向的速度, 向右為+
			// float yvel 豎直方向的速度, 向下為+
			Log.d(TAG, "onViewReleased: " + "xvel: " + xvel + " yvel: " + yvel);
			super.onViewReleased(releasedChild, xvel, yvel);
			
			// 判斷執行 關閉/開啟
			// 先考慮所有開啟的情況,剩下的就都是關閉的情況
			if(xvel == 0 && mMainContent.getLeft() > mRange / 2.0f){
				open();
			}else if (xvel > 0) {
				open();
			}else {
				close();
			}
			
		}
 
		@Override
		public void onViewDragStateChanged(int state) {
			// TODO Auto-generated method stub
			super.onViewDragStateChanged(state);
		}
/**
	 * 根據范圍修正左邊值
	 * @param left
	 * @return
	 */
	private int fixLeft(int left) {
		if(left < 0){
			return 0;
		}else if (left > mRange) {
			return mRange;
		}
		return left;
	}

  

 
在view移動的時候也可以用伴隨動畫:
 
	private void animViews(float percent) {
		//		> 1. 左面板: 縮放動畫, 平移動畫, 透明度動畫
					// 縮放動畫 0.0 -> 1.0 >>> 0.5f -> 1.0f  >>> 0.5f * percent + 0.5f
			//		mLeftContent.setScaleX(0.5f + 0.5f * percent);
			//		mLeftContent.setScaleY(0.5f + 0.5f * percent);
					ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
					ViewHelper.setScaleY(mLeftContent, 0.5f + 0.5f * percent);
					// 平移動畫: -mWidth / 2.0f -> 0.0f
					ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth / 2.0f, 0));
					// 透明度: 0.5 -> 1.0f
					ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.5f, 1.0f));
				
		//		> 2. 主面板: 縮放動畫
					// 1.0f -> 0.8f
					ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
					ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
					
		//		> 3. 背景動畫: 亮度變化 (顏色變化)
					getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK, Color.TRANSPARENT), Mode.SRC_OVER);

  

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM