Android學(xué)習(xí)筆記之View

導(dǎo)圖

一、View事件體系

1.什么是 View 和 View的位置坐標(biāo)

  • View是什么:

View 是一種界面層的控件的一種抽象,一組 View 則稱為 ViewGroup,同時(shí) ViewGroup 繼承了 View。意味著 View 可以是單個(gè)控件也可以是多個(gè)控件組成的組控件,通過這種關(guān)系形成了 View 樹的結(jié)構(gòu)。

  • View的位置坐標(biāo)
  • Android坐標(biāo)系:以屏幕的左上角為坐標(biāo)原點(diǎn),向右為x軸增大方向,向下為y軸增大方向。
  • View的寬高和坐標(biāo)關(guān)系:width = right - left,height = bottom - top。
  • 從android3.0開始,View增加了額外幾個(gè)參數(shù):x,y,translationX、translationY。其中x和y是View左上角的坐標(biāo),translationX和translationY是新View左上角相對(duì)于父容器的偏移量,它們默認(rèn)值是0。
  • 存在關(guān)系:x = left + translationX,y = top + translationY
  • 由此可見,x和left不同體現(xiàn)在:left是View的初始坐標(biāo),在繪制完畢后就不會(huì)再改變;而x是View偏移后的實(shí)時(shí)坐標(biāo),是實(shí)際坐標(biāo)。y和top的區(qū)別同理。

2.MotionEvent和TouchTop

  • MotionEvent:在手指接觸屏幕后所產(chǎn)生的一系列事件,任何事件列都是以DOWN事件開始,UP事件結(jié)束,中間有無數(shù)的MOVE事件。
    • ACTION_DOWM:手指剛接觸屏幕。
    • ACTION_MOVE:手指在屏幕上移動(dòng)。
    • ACTION_UP:手指在屏幕上松開的一瞬間。
MotionEvent點(diǎn)擊view位置示意圖

通過MotionEvent 對(duì)象可以得到觸摸位置的x、y坐標(biāo)。其中通過getX()、getY()可獲取相對(duì)于當(dāng)前view左上角的x、y坐標(biāo);通過getRawX()、getRawY()可獲取相對(duì)于手機(jī)屏幕左上角的x,y坐標(biāo)。


  • TouchTop: 系統(tǒng)所能識(shí)別的被認(rèn)為是滑動(dòng)的最小距離。即當(dāng)手指在屏幕上滑動(dòng)時(shí),如果兩次滑動(dòng)之間的距離小于這個(gè)常量,那么系統(tǒng)就不認(rèn)為你是在進(jìn)行滑動(dòng)操作。獲取這個(gè)常量:

該常量和設(shè)備有關(guān),可用它來判斷用戶的滑動(dòng)是否達(dá)到閾值,獲取方法:
ViewConfiguration.get(getContext()).getScaledTouchSlop()。

3.VelocityTracker 和 GestureDetector

  • VelocityTracker:速度追蹤,用于追蹤手指在滑動(dòng)過程中的速度,包括水平和豎直方向的速度。

A、首先在view的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度:

//實(shí)例化一個(gè)VelocityTracker 對(duì)象
VelocityTracker velocityTracker = VelocityTracker.obtain();
//添加追蹤事件
velocityTracker.addMovement(event);

B、接著在 ACTION_UP 事件中獲取當(dāng)前的速度。注意這里計(jì)算的是1000ms時(shí)間間隔移動(dòng)的像素值,假設(shè)像素是100,即速度是每秒100像素。另外,手指逆著坐標(biāo)系的正方向滑動(dòng),所產(chǎn)生的速度為負(fù)值 ,順著正反向滑動(dòng),所產(chǎn)生的速度為正值。

//獲取速度前先計(jì)算速度,這里計(jì)算的是在1000ms內(nèi)
velocityTracker .computeCurrentVelocity(1000);
//得到的是1000ms內(nèi)手指在水平方向從左向右滑過的像素?cái)?shù),即水平速度
float xVelocity = velocityTracker .getXVelocity();
//得到的是1000ms內(nèi)手指在水平方向從上向下滑過的像素?cái)?shù),垂直速度
float yVelocity = velocityTracker .getYVelocity();

C、最后,當(dāng)不需要使用它的時(shí)候,需要調(diào)用clear方法來重置并回收內(nèi)存:

velocityTracker.clear();
velocityTracker.recycle();

  • GestureDetector:手勢(shì)檢測,用于輔助檢測用戶的單擊、滑動(dòng)、長按、雙擊等行為。

A、使用過程:創(chuàng)建一個(gè)GestureDetecor對(duì)象并實(shí)現(xiàn)OnGestureListener接口,根據(jù)需要實(shí)現(xiàn)單擊等方法:

GestureDetector mGestureDetector = new GestureDetector(this);//實(shí)例化一個(gè)GestureDetector對(duì)象
mGestureDetector.setIsLongpressEnabled(false);// 解決長按屏幕后無法拖動(dòng)的現(xiàn)象

B、接著,接管目標(biāo)view的onTouchEvent方法,在待監(jiān)聽view的onTouchEvent方法中添加如下實(shí)現(xiàn):

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

C、然后,就可以有選擇的實(shí)現(xiàn)OnGestureListener和OnDoubleTapListener中的方法了。

建議:如果只是監(jiān)聽滑動(dòng)操作,建議在onTouchEvent中實(shí)現(xiàn);如果要監(jiān)聽雙擊這種行為,則使用GestureDetector 。


二、View的滑動(dòng)

滑動(dòng)方式1:通過View本身提供的scrollTo/scrollBy方法

  • scrollBy 是內(nèi)部調(diào)用了 scrollTo 的,它是基于當(dāng)前位置的相對(duì)滑動(dòng);而scrollTo是絕對(duì)滑動(dòng),因此如果利用相同輸入?yún)?shù)多次調(diào)用scrollTo()方法,由于View初始位置是不變只會(huì)出現(xiàn)一次View滾動(dòng)的效果而不是多次。
  • 注意:兩者都只能對(duì)view內(nèi)容進(jìn)行滑動(dòng),而不能使view本身滑動(dòng)。
image
  • mScrollX和mScrollY分別表示View在X、Y方向的滾動(dòng)距離。
  • mScrollX:View的左邊緣減去View的內(nèi)容的左邊緣;
  • mScrollY:View的上邊緣減去View的內(nèi)容的上邊緣。從右向左滑動(dòng),mScrollX為正值,反之為負(fù)值;從下往上滑動(dòng),mScrollY為正值,反之為負(fù)值。

滑動(dòng)方式2:通過動(dòng)畫給View施加平移效果

  • View動(dòng)畫:layout文件 < set > 標(biāo)簽下的 < translate fromXDelta和 toXDelta > 來平移。
  • 屬性動(dòng)畫:ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

滑動(dòng)方式3:通過改變View的LayoutParams使得View重新布局

比如將一個(gè)View向右移動(dòng)100像素,向右,只需要把它的marginLeft參數(shù)增大即可,代碼見下:

MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 請(qǐng)求重新對(duì)View進(jìn)行measure、layout

三種方法比較

  • scrollTo/scrollBy:操作簡單,適合對(duì)view內(nèi)容滑動(dòng),非平滑。
  • 動(dòng)畫:操作簡單,主要適用于沒有交互的view和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果。
  • 改變LayoutParams:操作稍微復(fù)雜,適用于有交互的view,非平滑。

三、彈性滑動(dòng)

彈性滑動(dòng)方式1:使用 Scroller

  • 與scrollTo/scrollBy不同:scrollTo/scrollBy過程是瞬間完成的,非平滑;而Scroller則有過渡滑動(dòng)的效果。
  • 注意:Scoller本身無法讓View彈性滑動(dòng),它需要和 View 的 computerScroller 方法配合使用。
Scroller scroller = new Scroller(mContext); //實(shí)例化一個(gè)Scroller對(duì)象

private void smoothScrollTo(int dstX, int dstY) {
  int scrollX = getScrollX();//View的左邊緣到其內(nèi)容左邊緣的距離
  int scrollY = getScrollY();//View的上邊緣到其內(nèi)容上邊緣的距離
  int deltaX = dstX - scrollX;//x方向滑動(dòng)的位移量
  int deltaY = dstY - scrollY;//y方向滑動(dòng)的位移量
  scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //開始滑動(dòng)
  invalidate(); //刷新界面
}

@Override//計(jì)算一段時(shí)間間隔內(nèi)偏移的距離,并返回是否滾動(dòng)結(jié)束的標(biāo)記
public void computeScroll() {
  if (scroller.computeScrollOffset()) { 
    scrollTo(scroller.getCurrX(), scroller.getCurY());
    postInvalidate();//通過不斷的重繪不斷的調(diào)用computeScroll方法
  }
}

具體實(shí)現(xiàn):在 MotionEvent.ACTION_UP 事件觸發(fā)時(shí)調(diào)用 startScroll 方法->馬上調(diào)用invalidate/postInvalidate 方法->會(huì)請(qǐng)求 View 重繪,導(dǎo)致 View.draw 方法被執(zhí)行->會(huì)調(diào)用 View.computeScroll 方法,此方法是空實(shí)現(xiàn),需要自己處理邏輯。具體邏輯是:先判斷 computeScrollOffset,若為 true(表示滾動(dòng)未結(jié)束),則執(zhí)行 scrollTo 方法,它會(huì)再次調(diào)用 postInvalidate,如此反復(fù)執(zhí)行,直到返回值為 false。

image

原理:Scroll 的 computeScrollOffset() 根據(jù)時(shí)間的流逝動(dòng)態(tài)百分比計(jì)算一小段時(shí)間里View滑動(dòng)的距離,并得到當(dāng)前View位置,再通過scrollTo繼續(xù)滑動(dòng)。即把一次滑動(dòng)拆分成無數(shù)次小距離滑動(dòng)從而實(shí)現(xiàn)彈性滑動(dòng)。

彈性滑動(dòng)方式2:通過動(dòng)畫

動(dòng)畫本身就是一種漸近的過程,故可通過動(dòng)畫來實(shí)現(xiàn)彈性滑動(dòng)。

//在100ms內(nèi)使得View從原始位置向右平移100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

彈性滑動(dòng)方式3:使用延時(shí)策略

通過發(fā)送一系列延時(shí)信息從而達(dá)到一種漸近式的效果,具體可以通過HandlerView的postDelayed方法,也可使用線程的sleep方法。


四、View的事件分發(fā)機(jī)制

  • 事件分發(fā)的本質(zhì):其實(shí)就是對(duì) MotionEvent 事件分發(fā)的過程。即當(dāng)一個(gè) MotionEvent 產(chǎn)生了以后,系統(tǒng)需要將這個(gè)點(diǎn)擊事件傳遞到一個(gè)具體的View上。

  • 點(diǎn)擊事件的傳遞順序:Activity(Window) -> ViewGroup -> View

  • 其中三個(gè)主要方法:

    • dispatchTouchEvent:進(jìn)行事件的分發(fā)(傳遞)。
    • onInterceptTouchEvent:對(duì)事件進(jìn)行攔截。
    • onTouchEvent:進(jìn)行事件處理。
事件分發(fā)流程
  1. 事件分發(fā)是逐級(jí)下發(fā)的,目的是將事件傳遞給一個(gè)View。當(dāng)最后一個(gè)View 沒有消費(fèi)事件,這個(gè)事件會(huì)依次返轉(zhuǎn),最后回到最高位的Activity,如果這樣都沒消費(fèi)的話才拋棄。(責(zé)任鏈)
  2. 在ViewGroup事件分發(fā)中,View本身是不存在分發(fā),所以也沒有攔截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中進(jìn)行處理消費(fèi)或者不消費(fèi)。
  3. 當(dāng)一個(gè) View 需要處理事件時(shí),如果設(shè)置了 OnTouchListener,那么 OnTouchListener 的 onTouch 方法會(huì)回調(diào),如果返回 true,那么 onTouchEvent 方法將不會(huì)調(diào)用(同時(shí)onClick事件是在onTouchEvent中調(diào)用)所以三者優(yōu)先級(jí)是onTouch->onTouchEvent->onClick(onClickListener)。

View的事件分發(fā)機(jī)制

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) 
          == ENABLED && mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}
  • ①、在View的dispatchTouchEvent方法中首先會(huì)執(zhí)行onTouch這個(gè)回調(diào)函數(shù),但是執(zhí)行onTouch這個(gè)回調(diào)函數(shù)有兩個(gè)前提條件、第一個(gè)提前提條件是該控件注冊(cè)了觸摸監(jiān)聽,第二個(gè)條件是該控件是的狀態(tài)是Enable的。
  • ②、onTouch回調(diào)函數(shù)會(huì)有一個(gè)返回值,如果返回為true的話就代表本次觸摸事件被消耗掉了,執(zhí)行接觸;返回值為false的話會(huì)繼續(xù)執(zhí)行onTouchEvent方法。
  • ③、onTouchEvent方法就是真正執(zhí)行點(diǎn)擊事件的地方,也就是我們重寫的onClick方法。
  • ④、onTouchEvent方法中能夠捕獲一次點(diǎn)擊事件當(dāng)中ACTION_DOWN、ACTION_ MOVE、ACTION_UP這三個(gè)動(dòng)作,當(dāng)觸摸動(dòng)作為ACTION_UP會(huì)調(diào)用onClick方法。

ViewGroup的事件分發(fā)機(jī)制

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    if(onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else{
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}
  • ①、首先會(huì)去調(diào)用ViewGroup的dispatchTouchEvent方法,其中會(huì)有onInterceptTouchEvent方法對(duì)事件傳遞進(jìn)行攔截,如果返回值為true的話就表示事件不往子View中傳遞,如果為false的話就表示不對(duì)事件傳遞進(jìn)行攔截,事件會(huì)往子View中傳遞。

五、View滑動(dòng)沖突

1.常見的滑動(dòng)沖突場景

  1. 外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向不一致(左右滑動(dòng):Fragment,上下滑動(dòng): ListView)。
  2. 外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向一致,(ScrollView 中包含 ListView );
  3. 上面兩種情況的嵌套;

2.滑動(dòng)沖突的處理規(guī)則

  1. 滑動(dòng)路徑和水平方向所形成的夾角
  2. 水平方向和豎直方向上的距離差。
  3. 水平和豎直方向的速度差
  4. 從業(yè)務(wù)的需求上得出相應(yīng)的處理規(guī)則。

3.滑動(dòng)沖突的解決方式

第一種:外部攔截法

  • 含義:指點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。

  • 方法:需要重寫父容器的 onInterceptTouchEvent 方法,在內(nèi)部做出相應(yīng)的攔截。

  • 過程:在 onInterceptTouchEvent 方法中,首先在 ACTION_DOWN 這個(gè)事件中,父容器必須返回 false,即不攔截 ACTION_DOWN 事件,因?yàn)橐坏└溉萜鲾r截了 ACTION_DOWN,那么后續(xù)的 ACTION_MOVE / ACTION_UP 都會(huì)直接交給父容器處理;其次是 ACTION_MOVE,根據(jù)需求來決定是否要攔截;最后 ACTION_UP 事件,這里必須要返回 false,一旦攔截子View的onClick事件將不會(huì)觸發(fā)。

public boolean onInterceptTouchEvent (MotionEvent event){
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
         intercepted = false;
         break;
      case MotionEvent.ACTION_MOVE:
         if (父容器需要當(dāng)前事件) {
             intercepted = true;
         } else {
             intercepted = flase;
         }
         break;
   }
      case MotionEvent.ACTION_UP:
         intercepted = false;
         break;
      default : break;
   }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
   }

第二種:內(nèi)部攔截法

  • 含義:指父容器不攔截任何事件,而將所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進(jìn)行處理。

  • 方法:需要配合requestDisallowInterceptTouchEvent方法。

  • 過程:父元素需要默認(rèn)攔截除 ACTION_DOWN 以外的事,這樣子元素調(diào)用 parent.requestDisallowInterceptTouchEvent(false) 方法時(shí),父元素才能繼續(xù)攔截需要的事件。(ACTION_DOWN 事件不受 requestDisallowInterceptTouchEvent 方法影響,所以一旦父元素?cái)r截 ACTION_DOWN 事件,那么所有元素都無法傳遞到子元素去)。

public boolean dispatchTouchEvent ( MotionEvent event ) {
  int x = (int) event.getX();
  int y = (int) event.getY();

  switch (event.getAction) {
      case MotionEvent.ACTION_DOWN:
         parent.requestDisallowInterceptTouchEvent(true);//為true表示禁止父容器攔截
         break;
      case MotionEvent.ACTION_MOVE:
         int deltaX = x - mLastX;
         int deltaY = y - mLastY;
         if (父容器需要此類點(diǎn)擊事件) {
             parent.requestDisallowInterceptTouchEvent(false);
         }
         break;
      case MotionEvent.ACTION_UP:
         break;
      default :
         break;        
 }

  mLastX = x;
  mLastY = y;
  return super.dispatchTouchEvent(event);
}

六、View工作原理

1.View的繪制流程

  1. 整個(gè) View 樹的繪圖流程是在 ViewRoot 類的 performTraversals() 方法展開的。
  2. performTraversals() 依次調(diào)用 performMeasure()performLayout()performDraw()三個(gè)方法,分別完成頂級(jí)View的繪制。
  3. 其中,performMeasure() 會(huì)調(diào)用 measure(),measure() 中又調(diào)用onMeasure(),實(shí)現(xiàn)對(duì)其所有子元素的 measure 過程,這樣就完成了一次 measure 過程;接著子元素會(huì)重復(fù)父容器的 measure 過程,如此反復(fù)至完成整個(gè) View 樹的遍歷。
  4. performLayout 和 performDraw 的傳遞流程也是類似,唯一不同的是 performDraw 都傳遞過程是用 draw 方法中的 dispatchDraw 來實(shí)現(xiàn)。
  • measure 過程決定了View的寬高,Measure完成以后,可以通過getMeasuredWidthgetMeasureHeight方法來獲取到View的寬/高
  • Layout 過程決定了View的四個(gè)頂點(diǎn)的坐標(biāo)和View的實(shí)際寬高,完成以后通過getTop、getBottom、getLeft、getRight來拿到四個(gè)頂點(diǎn)的位置,并通過getWidth和getHeight方法來拿到View的最終寬高。
  • Draw 過程決定了View的顯示,只有draw方法完成后View的內(nèi)容才能呈現(xiàn)在屏幕上。
image

2.measure方法

2.1 MeasureSpec

  • 作用:通過寬測量值widthMeasureSpec和高測量值heightMeasureSpec 決定View的大小。

  • 組成:32 位的 int 值,高 2 位 代表 SpecMode測量模式,低 30 位代表 SpecSize 規(guī)格大小。

  • 三種測量模式:

    • UNSPECIFIED:不確定,父容器不對(duì) View 進(jìn)行任何限制,要多大給多大,一般用于系統(tǒng)內(nèi)部。
    • EXACTLY:父容器檢測到 View 所需要的精確大小,這時(shí)候 View 的最終大小就是 SpecSize 所指定的值,對(duì)應(yīng) LayoutParams 中的“match_parent”和“具體數(shù)值”這兩種模式。
    • AT_MOST:父容器指定了最大的尺寸 SpecSize,View 的大小不能大于這個(gè)值,對(duì)應(yīng) LayoutParams 中的 wrap_content。
  • 決定因素:值由子View的布局參數(shù)LayoutParams和父容器的MeasureSpec值共同決定。

  • LayoutParams布局參數(shù):具體數(shù)值、match_parent和wrap_content。

2.2 measure測量過程圖

  • ①View的measure:View的measure由measure方法來完成,而它是一個(gè)final類型的方法,意味這不能重寫,在View的measure中去調(diào)用View的onMeasure方法,因此只需要在onMeasure中實(shí)現(xiàn)即可。
image

從getDefaultSize()中可以看出,直接繼承View的自定義View需要重寫onMeasure()并設(shè)置wrap_content時(shí)的自身大小,否則效果相當(dāng)于macth_parent。

  • ②ViewGroup的measure
    • measure():基本測量邏輯的判斷,調(diào)用onMeasure進(jìn)行下一步測量;
    • onMeasure()(需要重寫):計(jì)算View寬/高,并存儲(chǔ)。
    • measureChildren():遍歷子View并調(diào)用measureChild()進(jìn)行子View下一步測量。
    • measureChild():計(jì)算單個(gè)子View的MeasureSpec;調(diào)用每個(gè)子View的measure()進(jìn)行下一步測量。
    • getChildMeasureSpec():計(jì)算子View的MeasureSpec參數(shù)。
    • ......遍歷子View測量(View的measure過程)........
    • setMeasureDimension:存儲(chǔ)測量后的子View寬高。
image
image

3. layout方法

  • 作用:確定 View 的最終寬高和四個(gè)頂點(diǎn)的位置。
  • 大致流程:從頂級(jí)View開始依次調(diào)用 layout(),其中子 View 的 layout() 會(huì)調(diào)用 setFrame() 來設(shè)定自己的四個(gè)頂點(diǎn)(mLeft、mRight、mTop、mBottom),接著調(diào)用 onLayout() 來確定其坐標(biāo),注意該方法是空方法,因?yàn)椴煌?ViewGroup 對(duì)其子 View 的布局是不相同的。
image
image

4. draw方法

  • 繪制順序:
    1. 繪制背景:background.draw(canvas)
    2. 繪制自己:onDraw(canvas)
    3. 繪制children:dispatchDraw(canvas)
    4. 繪制裝飾:onDrawScrollBars(canvas)

ViewGroup 通常情況下不需要繪制,因?yàn)楸旧頉]有需要繪制的東西,如果不是指定了ViewGroup的背景顏色,那么連 ViewGroup 都不會(huì)調(diào)用。ViewGroup 會(huì)使用dispatchDraw方法來繪制其子View。


七、自定義View

1. 自定義 View 的分類

  • 繼承 View 重寫 onDraw 方法:主要實(shí)現(xiàn)一些不規(guī)則的效果,重寫onDraw方法,需要支持wrap_content并且需要自己處理padding。(重寫view實(shí)現(xiàn)新的控件)
  • 繼承 ViewGroup 派生特殊的 Layout:實(shí)現(xiàn)自定義布局,除了LinearLayout、RelativeLayout等這幾種系統(tǒng)之外的布局。稍微復(fù)雜需要合適地處理ViewGroup的測量、布局這兩個(gè)過程,并同時(shí)處理子元素的測量和布局的過程。
  • 繼承特定的 View,比如 TextView:擴(kuò)展某種已有View的功能,比如TextView。容易實(shí)現(xiàn),不需要自己支持wrap_content和padding。(對(duì)現(xiàn)有的控件進(jìn)行拓展)
  • 繼承特定的 ViewGroup(比如 LinearLayout):與第二種的區(qū)別在于不用自己處理ViewGroup的測量和布局,第二種更接近底層。(通過組合來創(chuàng)建復(fù)合控件)

2. 自定義 View 須知

  • 讓 View 支持 wrap_content;
  • 如果有必要,讓你的 View 支持 padding;
  • 盡量不要在 View 中使用 Handler,沒有必要,View 內(nèi)部提供了 post 系列的方法;
  • View 中如果有線程或者動(dòng)畫,需要繼續(xù)停止,參考View#onDetachedFromWindow;
  • View 帶有滑動(dòng)嵌套情形時(shí),需要處理好滑動(dòng)沖突;

3. 自定義 View 的思想

  • 首先要掌握基本功,比如 View 的彈性滑動(dòng)、滑動(dòng)沖突、繪制原理等,這些都是自定義 View 所必須的。
  • 熟練掌握基本功后,在面對(duì)新的自定義 View 時(shí),要能夠?qū)ζ浞诸惒⑦x擇合適的實(shí)現(xiàn)思路。
  • 另外平時(shí)還要多積累一些自定義 View 相關(guān)的經(jīng)驗(yàn),并逐漸做到融會(huì)貫通。

4. 為什么自定義控件

  • 特定的顯示風(fēng)格。
  • 處理特有的用戶風(fēng)格
  • 優(yōu)化布局
  • 封裝

5.如何自定義控件

  1. 自定義屬性的聲明與獲取。
  2. 測量onMeasure。
  3. 布局onLayout(viewgroup)
  4. 繪制onDraw
  5. onTouchEvent。
  6. onInterceptTouchEvent(ViewGroup)。

6. 自定義屬性聲明與獲取

  1. 分析需要的自定義屬性。
  2. 在res/values/attrs.xml定義聲明
  3. 在layout.xml方法中進(jìn)行使用。
  4. 在View的構(gòu)造方法中進(jìn)行獲取。

7. 自定義View的三要素

  1. Canvas:畫布,用于繪制View所要顯示的內(nèi)容,一般來自onDraw()函數(shù)的傳入。
  2. Paint :畫筆,用于繪制View所需要繪制的內(nèi)容,相當(dāng)于筆,在其內(nèi)部可以設(shè)置顏色,粗細(xì),是否實(shí)心等信息,一般通過new的方式獲取該類對(duì)象;
  3. Point:點(diǎn),用于確定View所需要繪制的內(nèi)容大小及位置等相關(guān)信息,當(dāng)然這里的Point不具有實(shí)際意義,也有可能是線段,矩形等,只不過大多數(shù)是通過點(diǎn)的組合關(guān)系確定而已;

問題1:onTouch()、onTouchEvent()和onClick()關(guān)系?

優(yōu)先度onTouch()>onTouchEvent()>onClick()。因此onTouchListener的onTouch()方法會(huì)先觸發(fā);如果onTouch()返回false才會(huì)接著觸發(fā)onTouchEvent(),同樣的,內(nèi)置諸如onClick()事件的實(shí)現(xiàn)等等都基于onTouchEvent();如果onTouch()返回true,這些事件將不會(huì)被觸發(fā)。

問題2:SurfaceView和View的區(qū)別?

SurfaceView是從View基類中派生出來的顯示類,它和View的區(qū)別有:

  • View需要在UI線程對(duì)畫面進(jìn)行刷新,而SurfaceView可在子線程進(jìn)行頁面的刷新
  • View適用于主動(dòng)更新的情況,而SurfaceView適用于被動(dòng)更新,如頻繁刷新,這是因?yàn)槿绻褂肰iew頻繁刷新會(huì)阻塞主線程,導(dǎo)致界面卡頓
  • SurfaceView在底層已實(shí)現(xiàn)雙緩沖機(jī)制,而View沒有,因此SurfaceView更適用于需要頻繁刷新、刷新時(shí)數(shù)據(jù)處理量很大的頁面

問題3:invalidate()和postInvalidate()的區(qū)別?

invalidate()與postInvalidate()都用于刷新View,主要區(qū)別是invalidate()在主線程中調(diào)用,若在子線程中需要配合handler使用;而postInvalidate()可在子線程中直接調(diào)用。

問題4: requestLayout()和invalidate()的區(qū)別?

  1. 調(diào)用invalidate()只會(huì)執(zhí)行onDraw方法;調(diào)用requestLayout()只會(huì)執(zhí)行onMeasure方法和onLayout方法,并不會(huì)執(zhí)行onDraw方法。

  2. 所以當(dāng)我們進(jìn)行View更新時(shí),若僅View的顯示內(nèi)容發(fā)生改變,則只需調(diào)用invalidate方法;若View寬高和位置發(fā)生改變,則調(diào)用requestLayout方法;若兩者均發(fā)生改變,則需先調(diào)用requestLayout()再調(diào)用invalidate()。

View的生命周期

問題5:Android中真實(shí)寬高getWidth和getMeasuredWidth的區(qū)別:哪個(gè)計(jì)算的是真實(shí)的寬?

getWidth():得到的是View在父Layout中布局好后的寬度值,如果沒有父布局,那么默認(rèn)的父布局就是整個(gè)屏幕。
getMeasuredWidth():得到的是最近一次調(diào)用measure()方法測量后得到的是View的寬度,它僅僅用在測量和Layout的計(jì)算中。所以此方法得到的是View的內(nèi)容占據(jù)的實(shí)際寬度。
總結(jié):
getWidth(): View在設(shè)定好布局后整個(gè)View的寬度。
getMeasuredWidth(): 對(duì)View上的內(nèi)容進(jìn)行測量后得到的View內(nèi)容占據(jù)的寬度,前提是你必須在父布局的onLayout()方法或者此View的onDraw()方法里調(diào)用measure(0,0);否則你得到的結(jié)果和getWidth()得到的結(jié)果是一樣的。

問題6:為什么Viewgroup的Measure過程和View的過程不一樣,還要自己重寫onMeasure()方法?

因?yàn)椴煌腣iewGroup子類(LinearLayout、RelativeLayout / 自定義ViewGroup子類等)具備不同的布局特性,這導(dǎo)致他們子View的測量方法各有不同。

總結(jié)一句話:View的measure過程的onMeasure()具有統(tǒng)一實(shí)現(xiàn),而ViewGroup則沒有。

本文參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容