Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android——自定義View(學習Android開發與藝術探索)

Android——自定義View(學習Android開發與藝術探索)

編輯:關於Android編程

ViewRoot和DecorView

ViewRoot對應於ViewRootImpl類,是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當Activity對象被創建完畢後,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView建立關聯。
View的繪制流程從ViewRoot的performTraversals開始,經過measure、layout和draw三個過程才可以把一個View繪制出來,其中measure用來測量View的寬高,layout用來確定View在父容器中的放置位置,而draw則負責將View繪制到屏幕上。
performTraversals會依次調用performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程。其中performMeasure中會調用measure方法,在measure方法中又會調用onMeasure方法,在onMeasure方法中則會對所有子元素進行measure過程,這樣就完成了一次measure過程;子元素會重復父容器的measure過程,如此反復完成了整個View數的遍歷。另外兩個過程類似,大致調用流程如下圖:

measure過程決定了View的寬/高,完成後可通過getMeasuredWidth/getMeasureHeight方法來獲取View測量後的寬/高。Layout過程決定了View的四個頂點的坐標和實際View的寬高,完成後可通過getTop、getBotton、getLeft和getRight拿到View的四個定點坐標。Draw過程決定了View的顯示,完成後View的內容才能呈現到屏幕上。

如下圖,DecorView作為頂級View,一般情況下它內部包含了一個豎直方向的LinearLayout,裡面分為兩個部分(具體情況和Android版本和主題有關),上面是標題欄,下面是內容欄。在Activity通過setContextView所設置的布局文件其實就是被加載到內容欄之中的。
DecorView其實是一個FrameLayout,View層的事件都先經過DecorView,然後才傳給我們的View。
\

理解MeasureSpec

MeasureSpec很大程度上決定一個View的尺寸規格,測量過程中,系統會將View的layoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,再根據這個measureSpec來測量出View的寬/高。
MeasureSpec代表一個32位的int值,高2位為SpecMode,低30位為SpecSize,SpecMode是指測量模式,SpecSize是指在某種測量模式下的規格大小。

MpecMode有三類;
1.UNSPECIFIED 父容器不對View進行任何限制,要多大給多大,一般用於系統內部
2.EXACTLY 父容器檢測到View所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值,對應LayoutParams中的match_parent和具體數值這兩種模式。
3.AT_MOST 父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,不同View實現不同,對應LayoutParams中的wrap_content。

當View采用固定寬/高的時候,不管父容器的MeasureSpec的是什麼,View的MeasureSpec都是精確模式兵其大小遵循Layoutparams的大小。 當View的寬/高是match_parent時,如果他的父容器的模式是精確模式,那View也是精確模式並且大小是父容器的剩余空間;如果父容器是最大模式,那麼View也是最大模式並且起大小不會超過父容器的剩余空間。 當View的寬/高是wrap_content時,不管父容器的模式是精確還是最大化,View的模式總是最大化並且不能超過父容器的剩余空間。

MeasureSpec和LayoutParams的關系
在View測量的時候,系統會將LayoutParams在父容器的約束下轉化成對應的MeasureSpec,然後根據這個MeasureSpec來確定View測量後的高/寬
子元素的MeasureSpec主要是根據父容器的MeasureSpec和自身的LayoutParams確定


這裡寫圖片描述

View的工作流程

1. View的measure過程

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          
     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),      widthMeasureSpec),     
     getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}

setMeasuredDimension方法會設置View的寬/高的測量值
getDefaultSize方法返回的大小就是measureSpec中的specSize,也就是View測量後的大小,絕大部分情況和View的最終大小(layout階段確定)相同。
getSuggestedMinimumWidth方法,作為getDefaultSize的第一個參數(建議寬度)
直接繼承View的自定義控件,需要重寫onMeasure方法並且設置
wrap_content時的自身大小,否則在布局中使用了wrap_content相當於使用了match_parent。解決方法:在onMeasure時,給View指定一個內部寬/高,並在wrap_content時設置即可,其他情況沿用系統的測量值即可。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
    int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
    if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
        setMeasuredDimension(mWidth,mHeight);
}else if(widthSpecMode==MeasureSpec.AT_MOST){
        setMeasuredDimension(mWidth,widthSpecSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
        setMeasuredDimension(heightSpecSize,mHeight);
}
}

ViewGroup的measure過程:

對於ViewGroup來說,除了完成自己的measure過程之外,還會遍歷去調用所有子元素的measure方法,個個子元素再遞歸去執行這個過程,和View不同的是,ViewGroup是一個抽象類,沒有重寫View的onMeasure方法,提供了measureChildren方法。
measureChildren方法,遍歷獲取子元素,子元素調用measureChild方法
measureChild方法,取出子元素的LayoutParams,再通過getChildMeasureSpec方法來創建子元素的MeasureSpec,接著將MeasureSpec傳遞給View的measure方法進行測量。
ViewGroup沒有定義其測量的具體過程,因為不同的ViewGroup子類有不同的布局特征,所以其測量過程的onMeasure方法需要各個子類去具體實現。
measure完成之後,通過getMeasureWidth/Height方法就可以獲取View的測量寬/高,需要注意的是,在某些極端情況下,系統可能要多次measure才能確定最終的測量寬/高,比較好的習慣是在onLayout方法中去獲取測量寬/高或者最終寬/高。
如何在Activity中獲取View的寬/高信息
因為View的measure過程和Activity的生命周期不是同步進行,如果View還沒有測量完畢,那麼獲取到的寬/高就是0;所以在Activity的onCreate、onStart、onResume中均無法正確的獲取到View的寬/高信息。下面給出4種解決方法。
第一種:Activity/View#onWindowFocusChanged。
onWindowFocusChanged這個方法的含義是:VieW已經初始化完畢了,寬高已經准備好了,需要注意:它會被調用多次,當Activity的窗口得到焦點和失去焦點 均會被調用。
第二種:view.post(runnable)。
通過post將一個runnable投遞到消息隊列的尾部,當Looper調用此runnable的時候,View也初始化好了。
第三種:ViewTreeObserver。
使用ViewTreeObserver的眾多回調可以完成這個功能,比如OnGlobalLayoutListener這個接口,當View樹的狀態發送改變或View樹內部的View的可見性發生改變時,onGlobalLayout方法會被回調。需要注意的是,伴隨著View樹狀態的改變,onGlobalLayout會被回調多次。
第四種:view.measure(int widthMeasureSpec,int heightMeasureSpec)。
(1). match_parent:
無法measure出具體的寬高,因為不知道父容器的剩余空間,無法測量出View的大小
(2). 具體的數值(dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
(3). wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
2. View的layout過程
在View的默認實現中,View的測量寬/高和最終寬/高是相等的,測量寬/高形成於View的measure過程,而最終寬/高形成於View的layout過程。
3. View的draw過程
將View繪制到屏幕上,大概的幾個步驟:
1.繪制背景background.draw(canvas)
2.繪制自己(onDraw)
3.繪制children(dispatchDraw)
4.繪制裝飾(onDrawScrollBars)
View的繪制過程是通過dispatchDraw來實現的,它會遍歷所有子元素的draw方法。
如果一個View不需要繪制任何內容,那麼設置setWillNotDraw為true後,系統會進行相應的優化;ViewGroup默認為true,如果我們的自定義ViewGroup需要通過onDraw來繪制內容的時候,需要顯示的關閉它。

自定義View

直接繼承View或ViewGroup的控件, 需要在onMeasure中對wrap_content做特殊處理。
直接繼承View的控件,如果不在draw方法中處理padding,那麼padding屬性就無法起作用。直接繼承ViewGroup的控件也需要在onMeasure和onLayout中考慮padding和子元素margin的影響,不然padding和子元素的margin無效。
View內部提供了post系列的方法,完全可以替代Handler的作用。
View中有線程和動畫,需要在View的onDetachedFromWindow中停止。
兩個例子:
繼承View重寫onDraw方法:

public class CircleView  extends View{
private int mColor=Color.RED;
    private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);//抗鋸齒
public CircleView(Context context) {
super(context);
}

public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray a=context.obtainStyledAttributes(attrs,R.styleable.CircleView);//獲取自定義屬性  
mColor=a.getColor(R.styleable.CircleView_circle_color,Color.RED);
a.recycle();
init();
}
private void init(){
paint.setColor(mColor);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//處理View使其支持wra_content
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
        int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
        if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(200,200);
}else if(widthSpecMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(200,widthSpecSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(heightSpecSize,200);
}
    }

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
        final int paddingLeft=getPaddingLeft();
        final int paddingRight=getPaddingRight();
        final int paddingTop=getPaddingTop();
        final int paddingBottom=getPaddingBottom();
        int width=getWidth()-paddingLeft-paddingRight;
        int height=getHeight()-paddingBottom-paddingTop;
        int radius=Math.min(width,height)/2;
canvas.drawCircle(width/2+paddingLeft,height/2+paddingTop,radius,paint);
}
}

繼承ViewGroup派生出特殊的Layout
HorizontalScrollView:內部的子元素可以進行水平滑動,子元素中可豎直滑動。

public class HorizontalScrollView extends ViewGroup{
private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

//分別記錄上次滑動的坐標
private int mLastX=0;
    private int mLastY=0;
//分別記錄上次滑動的坐標(onInterceptTouchEvent)
private int mLastXIntercept=0;
    private int mLastYIntercept=0;

    private Scroller mScroller;
    private VelocityTracker   mVelocityTracker;

    public HorizontalScrollView(Context context) {
super(context);
init();
}

public HorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init(){
if(mScroller==null){
mScroller=new Scroller(getContext());
mVelocityTracker=VelocityTracker.obtain();
}
    }

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted=false;
        int x= (int) ev.getX();
        int y= (int)ev.getY();

        switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:{
                intercepted=false;
                if(!mScroller.isFinished()){
mScroller.abortAnimation();
intercepted=true;
}
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX=x-mLastXIntercept;
                int deltaY=y-mLastXIntercept;
                if(Math.abs(deltaX)>Math.abs(deltaY)){
                    intercepted=true;
}else{
                    intercepted=false;
}
break;
}
case MotionEvent.ACTION_UP:{
                intercepted=false;
                break;
}
default:break;
}
mLastX=x;
mLastY=y;
mLastXIntercept=x;
mLastYIntercept=y;
        return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
        int x= (int) event.getX();
        int y= (int)event.getY();

        switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
            }
case MotionEvent.ACTION_MOVE:{
int deltaX=x-mLastX;
                int deltaY=y-mLastY;
scrollBy(-deltaX,0);
}
case MotionEvent.ACTION_UP:{
int scrollX=getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity=mVelocityTracker.getXVelocity();
                if(Math.abs(xVelocity)>50){
mChildIndex=xVelocity>0? mChildIndex-1:mChildIndex+1;
}else{
mChildIndex=(scrollX+mChildWidth/2)/mChildWidth;
}
mChildIndex=Math.max(0,Math.min(mChildIndex,mChildIndex-1));
                int dx=mChildIndex*mChildWidth-scrollX;
smoothScrollBy(dx,0);
mVelocityTracker.clear();
                break;
}
default:break;

}
mLastY=y;
mLastX=x;
        return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth=0;
        int measuredHeight=0;
        final int childCount=getChildCount();
measureChildren(widthMeasureSpec,heightMeasureSpec);

        int widthSpaceSize=MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
        if(childCount==0){
            setMeasuredDimension(0,0);
}else if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
final View childView=getChildAt(0);
measuredWidth=childView.getMeasuredWidth()*childCount;
measuredHeight=childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth,measuredHeight);
}else if(widthSpecMode==MeasureSpec.AT_MOST){
final View childView=getChildAt(0);
measuredWidth=childView.getMeasuredWidth()*childCount;
setMeasuredDimension(measuredWidth,heightSpecSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
final View childView=getChildAt(0);
measuredHeight=childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize,measuredHeight);
}
    }

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft=0;
        final int childCount=getChildCount();
        for(int i=0;i<childCount;i++){
final View view=getChildAt(i);
            if(view.getVisibility()!=View.GONE){
final int childWidth=view.getMeasuredWidth();
mChildWidth=childWidth;
view.layout(childLeft,0,childLeft+mChildWidth,getMeasuredHeight());
childLeft+=childWidth;
}
        }
    }
private void smoothScrollBy(int dx,int dy){
mScroller.startScroll(getScrollX(),0,dx,0,500);
invalidate();
}

@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
    }

@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
        super.onDetachedFromWindow();
}
}
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved