尊重原創轉載請註明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!
炮兵鎮樓
上一節我們細緻地、猥瑣地、小心翼翼地、猶如絲滑般撫摸、啊不,是講解了如何去測量一個佈局控件,再次強調,如我之前多次強調那樣,控件的測量必須要邏輯縝密嚴謹,儘量少地避免出現較大的邏輯錯誤。在整個系列撰寫的過程中,有N^N個朋友曾多次不間斷地小窗我問View是否也有生命週期,我也多次細心地、耐心地打開小窗然後默默地關掉它,不是我不願回答而是問的人太多我們乾脆就在blog中詳細闡述下,即便你是第一天學習Android,你也一定會用到Activity,用到Activity你一定會接觸到onCreate方法,然後你會從各種途徑瞭解到類似這樣的方法還有7個,我們稱之爲Activity生命週期:
- /**
- * 主界面
- *
- * @author Aige {@link http://blog.csdn.net/aigestudio}
- * @since 2014/11/17
- */
- public class MainActivity extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
- @Override
- protected void onStart() {
- super.onStart();
- }
- @Override
- protected void onResume() {
- super.onResume();
- }
- @Override
- protected void onPause() {
- super.onPause();
- }
- @Override
- protected void onStop() {
- super.onStop();
- }
- @Override
- protected void onRestart() {
- super.onRestart();
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- }
- }
如上所示的這些方法,除了提到的“生命週期”方法外還有一些事件的回調,多說無益,我們還是來看看這些方法會在View的什麼時候被調用,老樣子我們新建一個繼承於View的子類並重寫這些方法:
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/1/27
- *
- */
- public class LifeCycleView extends View {
- private static final String TAG = "AigeStudio:LifeCycleView";
- public LifeCycleView(Context context) {
- super(context);
- Log.d(TAG, "Construction with single parameter");
- }
- public LifeCycleView(Context context, AttributeSet attrs) {
- super(context, attrs);
- Log.d(TAG, "Construction with two parameters");
- }
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- Log.d(TAG, "onFinishInflate");
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- Log.d(TAG, "onMeasure");
- }
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- Log.d(TAG, "onLayout");
- }
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- Log.d(TAG, "onSizeChanged");
- }
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- Log.d(TAG, "onDraw");
- }
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- Log.d(TAG, "onAttachedToWindow");
- }
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- Log.d(TAG, "onDetachedFromWindow");
- }
- @Override
- protected void onWindowVisibilityChanged(int visibility) {
- super.onWindowVisibilityChanged(visibility);
- Log.d(TAG, "onWindowVisibilityChanged");
- }
- }
首先是調用了構造方法,這是不用猜都該知道的,然後呢調用了onFinishInflate方法,這個方法當xml佈局中我們的View被解析完成後則會調用,具體的實現在LayoutInflater的rInflate方法中:
- public abstract class LayoutInflater {
- void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
- boolean finishInflate) throws XmlPullParserException, IOException {
- // 省去無數代碼…………
- if (finishInflate) parent.onFinishInflate();
- }
- }
- public class MainActivity extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(new LifeCycleView(this));
- }
- }
緊接着調用的是onAttachedToWindow方法,此時表示我們的View已被創建並添加到了窗口Window中,該方法後緊接着一般會調用onWindowVisibilityChanged方法,只要我們當前的Window窗口中View的可見狀態發生改變都會被觸發,這時View是被顯示了,隨後就會開始調用onMeasure方法對View進行測量,如果測量結果被確定則會先調用onSizeChanged方法通知View尺寸大小發生了改變,緊跟着便會調用onLayout方法對子元素進行定位佈局,然後再次調用onMeasure方法對View進行二次測量,如果測量值與上一次相同則不再調用onSizeChanged方法,接着再次調用onLayout方法,如果測量過程結束,則會調用onDraw方法繪製View。我們看到,onMeasure和onLayout方法被調用了兩次,很多童鞋會很糾結爲何onMeasure方法回被多次調用,其實沒必要過於糾結這個問題,onMeasure的調用取決於控件的父容器以及View
Tree的結構,不同的父容器有不同的測量邏輯,比如上一節自定義控件其實很簡單2/3中,我們在SquareLayout測量子元素時就採取了二次測量,在API 19的時候Android對測量邏輯做了進一步的優化,比如在19之前只會對最後一次的測量結果進行Cache而在19開始則會對每一次測量結果都進行Cache,如果相同的代碼相同佈局相同的邏輯在19和19之前你有可能會看到不一樣的測量次數結果,所以沒必要去糾結這個問題,一般情況下只要你邏輯正確onMeasure都會得到正確的調用。
上面這些方法都很好理解,我們主要關心的是其調用流程,雖然上面我們通過LogCat的輸出大致瞭解了一下其執行順序,但是如果你好奇心足夠重,一定會想真是這樣的麼?在自定義控件其實很簡單7/12中我曾留下一個疑問:
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/1/12
- *
- */
- public class ImgView extends View {
- private Bitmap mBitmap;// 位圖對象
- public ImgView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 繪製位圖
- canvas.drawBitmap(mBitmap, 0, 0, null);
- }
- /**
- * 設置位圖
- *
- * @param bitmap
- * 位圖對象
- */
- public void setBitmap(Bitmap bitmap) {
- this.mBitmap = bitmap;
- }
- }
- public class MainActivity extends Activity {
- private ImgView mImgView;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- mImgView = (ImgView) findViewById(R.id.main_pv);
- Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lovestory);
- mImgView.setBitmap(bitmap);
- }
- }
- 處理控件動畫的階段
- 處理測量的階段
- 處理繪製的階段
Android的Animation動畫體系龐大不在本系列的講解範疇內,暫時Skip,測量和繪製的主要過程由我們之前所講的三個方法onMeasure、onLayout和onDraw所控制,這三個方法呢在framework中又主要由measure、layout、draw以及其派生方法所控制,在View中形成這樣一個體系:
再次注意:View的測量過程是由多個方法調用共同構成,measure和onMeasure僅僅代表該過程中的兩個方法而已。
如果控件繼承於ViewGroup實現的是一個佈局容器,那麼會多出一個dispatchDraw方法:
dispatchDraw方法本質上實現的是父容器對子元素的繪製分發,雖然邏輯不盡相同但是作用類似於draw,在高仿網易評論列表效果之界面生成中我們曾利用該方法在繪製子元素前繪製蓋樓背景,具體不再多說了。在我們調用setContentView方法後,如果你傳入的是一個資源文件ID,此時framework會使用LayoutInflater去解析佈局文件,當解析到我們自定義控件LifeCycleView的標籤時,通過反射獲取一個對應的LifeCycleView類實例,此時構造方法被調用,爾後開始解析LifeCycleView標籤下的各類屬性並存值,LifeCycleView標籤下的所有屬性(如果是個容器的話也會層層解析)解析完成後調用onFinishInflate方法表示當前LifeCycleView所有的(注意不是整個佈局哦僅僅是該View對應標籤)xml解析完畢,之後嘗試將View添加至當前Activity所在的Window,然後將處理UI事件的Msg壓入Message
Queue開始至上而下地對整個View Tree進行測量,假設我們有如下的View Tree結構:
那麼我們的測量總是從根部RelativeLayout開始逐層往下進行調用,在Android翻頁效果原理實現之引入折線中我們曾在講滑動時對Message Queue作過一個簡單的淺析,當Msg壓入Queue並最終得到處理的這段過程並不是立即的,也就是說其中會有一定的延時,這相對於我們在setContentView後立即setBitmap來說時間要長很多很多,這也是爲什麼我們在onMeasure中獲取Bitmap不爲null的原因,具體的源碼邏輯實現會在《深入剖析Android
GUI框架》深度講解,本系列除了後面要涉及到的事件分發外不會再涉及過多的源碼畢竟與基礎篇的定位不符,好了,這裏我再留一個問題,setBitmap和onMeasure、onLayout等這些回調方法之間是異步呢還是同步呢?其實答案很明顯了……OK,不說了,既然我們知道這樣直接setBitmap是不對的(即便可行)那麼我們該如何改進呢?答案很簡單,Andorid提供給我們極其簡便的方法,我們只需在設置Bitmap後調用requestLayout方法和invalidate即可:
- public void setBitmap(Bitmap bitmap) {
- this.mBitmap = bitmap;
- requestLayout();
- invalidate();
- }
requestLayout方法的意義在於如果你的操作有可能會讓控件的尺寸或位置發生改變那麼就可以調用該方法請求佈局,調用該方法後framework會嘗試調用measure對控件重新測量:
而invalidate方法呢我們則用的多了不再多說:
但是要注意的一點是,requestLayout方法和invalidate方法並非都必需調用的,比如我們有一個更改字體顏色的方法:
- public void setTextColor(int color) {
- mPaint.setColor(color);
- invalidate();
- }
- public void setTextSize(int size) {
- mPaint.setTextSize(size);
- requestLayout();
- invalidate();
- }
當時我們是直接extends View去做的,繪製了文本、繪製了Bitmap還有在此之前對其進行測量、定位等等,即便我們考慮周詳,但是也極難將一個裝載文本和圖片的控件做成一個TextView和ImageView的複合體,更難以像TextView和ImageView那樣提供儘可能多的接口方法,誒!等等!既然我們的這個圖標控件看上去就是個TextView和ImageView雜交的後代,那麼我們是否可以簡單地將這兩種控件組合起來變成一個新的控件呢?答案是肯定的撒!而且比起我們直接extends view來說簡單很多很多很多,首先我們先定義一個佈局,這個佈局裏面呢只包含一個ImageView和一個TextView,大體來說樣式跟上面我們自定義的類似:
- <!-- http://blog.csdn.net/aigestudio -->
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:gravity="center"
- android:orientation="vertical" >
- <ImageView
- android:id="@+id/view_complex_image_iv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:src="@drawable/logo" />
- <TextView
- android:textSize="40sp"
- android:textStyle="bold"
- android:id="@+id/view_complex_title_tv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
- </LinearLayout>
是不是跟我們自定義的一樣呢?如我所說,僅僅是一個ImageView和TextView的組合而已,接下來我們要做的則是將這個xml佈局文件“集成”到我們的自定義控件中去,方法也很簡單,在自定義控件的構造方法裏引入該佈局文件並將其作爲控件的佈局則可:
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/2/6
- *
- */
- public class ComplexView extends FrameLayout {
- private ImageView ivIcon;// 複合控件中的ImageView
- private TextView tvTitle;// 複合控件中的TextView
- public ComplexView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 加載佈局文件
- ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(
- R.layout.view_complex, this);
- // 獲取控件
- ivIcon = (ImageView) findViewById(R.id.view_complex_image_iv);
- tvTitle = (TextView) findViewById(R.id.view_complex_title_tv);
- }
- }
- xxxxxxxxxxxxxxxxxxxxx.inflate(R.layout.view_complex, this);
而後我們只需直接使用這個ComplexView符合控件即可:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:background="#ffffff"
- android:layout_height="match_parent" >
- <com.aigestudio.customviewdemo.views.ComplexView
- android:id="@+id/main_tv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- </LinearLayout>
非常完美,不需要我們去處理測繪邏輯,所有的這些都由Android自帶的控件自行去計算,我們只是簡單地將它們組合在一起了而已,所以說,每當Android提供的控件不能滿足你的需求時,首先你應該想想是否可以在現有控件的基礎上修改一下來達到你的目的,而不是盲目地直接重寫View或ViewGroup類,你可以提供不同的接口方法來修改你複合控件中的各類元素,比如下面我們提供一個setImageIcon方法來爲複合控件中的ImageView設置圖片:
- public void setImageIcon(int resId) {
- ivIcon.setImageResource(resId);
- }
- public TextView getTitle() {
- return tvTitle;
- }
- /**
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/2/6
- *
- */
- public class ComplexView extends LinearLayout {
- private ImageView ivIcon;// 複合控件中的ImageView
- private TextView tvTitle;// 複合控件中的TextView
- public ComplexView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 設置線性佈局排列方式
- setOrientation(LinearLayout.VERTICAL);
- // 設置線性佈局子元素對齊方式
- setGravity(Gravity.CENTER);
- // 實例化子元素
- ivIcon = new ImageView(context);
- ivIcon.setImageResource(R.drawable.logo);
- tvTitle = new TextView(context);
- tvTitle.setText("AigeStudio");
- tvTitle.setTextSize(MeasureUtil.dp2px(context, 30));
- tvTitle.setTypeface(Typeface.DEFAULT_BOLD);
- // 將子元素添加到複合控件
- addView(ivIcon, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT));
- addView(tvTitle, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT));
- }
- }
效果單一乏味不好看,而我想要的效果很簡單也與之類似,通過不斷點擊控件往復切換控件的兩種狀態即可:
達到類似效果有多種方法,最簡單的是更改checkBox,最複雜的是繼承View自己寫一個,而上面我們瞭解過複合控件,那麼我們能不能馬上學以致用使用一個複合控件來達到該效果呢?答案是肯定的!細心觀察可以看得出上面的效果無非就是兩張不同的圖片來回顯示/隱藏地切換而已,更直白地說就是兩個ImageView不斷地顯示/隱藏切換對吧,Such easy,下面直接看全部代碼:
- /**
- * 自定義CheckBox
- *
- * @author AigeStudio {@link http://blog.csdn.net/aigestudio}
- * @since 2015/2/6
- *
- */
- public class CustomCheckBox extends FrameLayout {
- private ImageView ivCheckOn, ivCheckOff;// 兩種狀態的ImageView
- private CustomCheckBoxChangeListener customCheckBoxChangeListener;// 切換的監聽器
- private boolean isCheck;// 是否被選中的標誌值
- public CustomCheckBox(Context context) {
- this(context, null);
- }
- public CustomCheckBox(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
- public CustomCheckBox(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- // 設置佈局文件
- LayoutInflater.from(context).inflate(R.layout.view_custom_check_box, this);
- // 獲取控件元素
- ivCheckOn = (ImageView) findViewById(R.id.view_custom_check_box_on);
- ivCheckOff = (ImageView) findViewById(R.id.view_custom_check_box_off);
- // 設置兩個ImageView的點擊事件
- ivCheckOn.setOnClickListener(new ClickListener());
- ivCheckOff.setOnClickListener(new ClickListener());
- // 讀取xml中設置的資源屬性ID
- TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomCheckBox);
- int imageOnResId = array.getResourceId(R.styleable.CustomCheckBox_imageOn, -1);
- int imageOffResId = array.getResourceId(R.styleable.CustomCheckBox_imageOff, -1);
- // 設置顯示資源
- setOnImage(imageOnResId);
- setOffImage(imageOffResId);
- // 對象回收
- array.recycle();
- // 默認顯示的是沒被選中的狀態
- setCheckOff();
- }
- /**
- * 爲CustomCheckBox設置監聽器
- *
- * @param customCheckBoxChangeListener
- * 監聽器接口對象
- */
- public void setCustomCheckBoxChangeListener(
- CustomCheckBoxChangeListener customCheckBoxChangeListener) {
- this.customCheckBoxChangeListener = customCheckBoxChangeListener;
- }
- /**
- * 設置開啓狀態時CustomCheckBox的圖片
- *
- * @param resId
- * 圖片資源ID
- */
- public void setOnImage(int resId) {
- ivCheckOn.setImageResource(resId);
- }
- /**
- * 設置關閉狀態時CustomCheckBox的圖片
- *
- * @param resId
- * 圖片資源ID
- */
- public void setOffImage(int resId) {
- ivCheckOff.setImageResource(resId);
- }
- /**
- * 設置CustomCheckBox爲關閉狀態
- */
- public void setCheckOff() {
- isCheck = false;
- ivCheckOn.setVisibility(GONE);
- ivCheckOff.setVisibility(VISIBLE);
- }
- /**
- * 設置CustomCheckBox爲開啓狀態
- */
- public void setCheckOn() {
- isCheck = true;
- ivCheckOn.setVisibility(VISIBLE);
- ivCheckOff.setVisibility(GONE);
- }
- /**
- * 獲取CustomCheckBox的選擇狀態
- *
- * @return true CustomCheckBox已被選擇
- * @return false CustomCheckBox未被選擇
- */
- public boolean isCheck() {
- return isCheck;
- }
- /**
- * 狀態改變監聽接口
- */
- public interface CustomCheckBoxChangeListener {
- void customCheckBoxOn();
- void customCheckBoxOff();
- }
- /**
- * 自定義CustomCheckBox中控件的事件監聽器
- */
- private class ClickListener implements OnClickListener {
- @Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.view_custom_check_box_on:
- setCheckOff();
- customCheckBoxChangeListener.customCheckBoxOff();
- break;
- case R.id.view_custom_check_box_off:
- setCheckOn();
- customCheckBoxChangeListener.customCheckBoxOn();
- break;
- }
- }
- }
- }
- /**
- * 主界面
- *
- * @author Aige {@link http://blog.csdn.net/aigestudio}
- * @since 2014/11/17
- */
- public class MainActivity extends Activity {
- private CustomCheckBox ccbTest;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- ccbTest = (CustomCheckBox) findViewById(R.id.main_ccb);
- ccbTest.setCustomCheckBoxChangeListener(new CustomCheckBoxChangeListener() {
- @Override
- public void customCheckBoxOn() {
- Toast.makeText(MainActivity.this, "Check on", Toast.LENGTH_SHORT).show();
- }
- @Override
- public void customCheckBoxOff() {
- Toast.makeText(MainActivity.this, "Check off", Toast.LENGTH_SHORT).show();
- }
- });
- }
- }
CustomCheckBox中加載用到的xml佈局文件如下:
- <?xml version="1.0" encoding="utf-8"?>
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
- <ImageView
- android:id="@+id/view_custom_check_box_on"
- android:layout_width="match_parent"
- android:scaleType="fitCenter"
- android:layout_height="match_parent" />
- <ImageView
- android:id="@+id/view_custom_check_box_off"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="fitCenter" />
- </FrameLayout>