編輯:關於Android編程
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很大程度上決定一個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確定
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或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(); } }
Moco介紹在開發過程中,經常會使用到一些http網絡接口,而這部分功能通常是由第三方開發團隊或者是後端同事進行開發的,在我們開發時不能給我們提供服務,更有甚者,要集成的
介紹 做項目到一定龐大的時候就會發現方法數太多,安裝包根本就裝不上去了,這個也不足為奇,我們都知道當方法數目超過65536這個數目限制的時候,擋在2.x的系統上面就會出現
Android WebView In NestedScrollView 加載騰訊新聞頁面 點擊彈出層 bug 小記。目的是: CoordinatorLayou+AppBa
今天我們實現一個直接繼承於View的全新控件。大家都知道音樂播放器吧,在點擊一首歌進行播放時,通常會有一塊區域用於顯示音頻條,我們今天就來學習下,播放器音頻條的實現。首先