學習資料:
- Android開發藝術探索
- 愛哥自定義控件其實很簡單3/4
一般自定義一個View
有4種思路:
- 直接繼承
View
- 直接繼承
ViewGroup
- 繼承現有的
View
,例如ImageViwe
,SurfaceView
- 繼承現有的
ViewGroup
,例如LinearLayout
推薦優先考慮3
或者4
。原因也比較明顯,無論哪一種方式,需要注意的事項都不少。采用3
或者4
的話,現有控件的一些特性可以直接拿來用,而且也更加容易做適配
1.自定義View注意事項 <p>
之前有一個誤區,總覺得自定義View
就得是直接繼承View
,然后自己實現各種效果
1.1 讓View支持 wrap_content <p>
在之前的學習中,重寫onMeasure()
方法的目的就是為了支持wrap_content
。原因在學習View
和ViewGroup
中,也進行了嘗試說明,View
是的大小是要受自身和父容器ViewGroup
影響。想要讓wrap_content
擁有包裹內容
的特性,就需要重寫View
和ViewGroup
的onMeausre()
方法,針對要加載內容或者childView
的寬高來設置自定義View
的寬高。這也是為啥推薦方式3
或者4
的原因之一
1.2 根據需要,考慮Padding和Margin <p>
- 直接繼承的
View
,Margin
是不用考慮的,之前的學習提到過,這個屬性被封裝進了LayoutParamas
由父容器ViewGroup
來處理。
Padding
屬性需要根據需求在onMeasure()
和onDraw()
方法中處理 - 直接繼承的
ViewGroup
,在onMeadsure()
方法中,ViewGroup
自身的Margin
屬性是直接支持的。需要考慮的是封裝進了LayoutParamas
中的childView
的Margin
屬性信息。利用childView
拿到的LayoutParams
中的Margin
屬性信息,在測量時就對childView
的寬高進行處理。在onLayout()
確定childView
四個頂點位置時,將Margin
屬性信息也做處理。Padding
屬性的處理類似Margin
,也需要在onMeausre
和onLayout()
方法中,都進行處理
1.3 盡量不要在View中創建Handler <p>
View
自身提供了post
系列方法,里面封裝的有Handler
。除非需要明確使用Handlder
來發送消息
注意:
post(new Runnable(){....})
中的一系列操作依然是在主線程,最好不要進行耗時的操作。而執行網絡請求會直接造成應用崩潰,給出android.os.NetworkOnMainThreadException
異常
1.4 View中有線程后者動畫,考慮關閉時機 <p>
在Android 自定義View學習(九)——Bezier貝塞爾曲線學習中,處理過一次這樣的情況
主要使用到一個onDetachedFromWindow()
的方法。在View
所在的Activity
退出或者當前View
自身被remove
點時,View
的onDetachedFromWindow()
方法便會被回調。在這個方法內,將正在運行的動畫或者線程關閉掉,能夠減少內存泄露的出現
1.5 View有嵌套時,考慮點擊事件和滑動沖突
繼承之一些可以滑動的ViewGroup
后,常需要考慮滑動沖突。View
的事件體系,下篇開始學習。
以上都是看Android開發藝術探索
后做的總結
2. 直接繼承View
雖然之前學習過程中,用過了十幾次了,但每次都是按照固定套路來,并沒有進行一些思考。
- 構造方法又都有啥啥區別?
-
View
有沒有生命周期? 有 -
View
和Activity
的生命周期有啥聯系沒有? 有
這里之前搞錯了。說
View
和Activity
的生命周期沒有聯系。View
依賴于Activity
,不可能沒有聯系。感謝,Android之路
留言指出錯誤。但,具體啥聯系,暫時不打算學習,想留在后面具體學習Activity
時,再來補充
直接繼承View
后,代碼:
public class LifecycleView extends View {
private final String TAG = "LifecycleView";
public LifecycleView(Context context) {
super(context);
Log.e(TAG,"&&&第1個構造方法");
}
public LifecycleView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.e(TAG,"&&&第2個構造方法");
for(int i=0;i<attrs.getAttributeCount();i++){
Log.e(TAG, "&&&第2個構造方法"+attrs.getAttributeName(i)+"--->"+attrs.getAttributeValue(i));
}
}
public LifecycleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.e(TAG,"&&&第3個構造方法");
}
public LifecycleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
Log.e(TAG,"&&&第4個構造方法");
}
}
打印出的Log
信息:
根據Log
信息,很明顯得出,當在xml
布局文件中聲明了LifecycleView
,系統就會調用第2個構造方法,并且通過參數attrs
可以拿到布局中,LifecycleView
中的標簽屬性。布局中控件的屬性就封裝在AttributeSet
中
第1個構造方法是直接進行new LifecycleView(mContext)
時,會被調用,但很少這樣用
結論1:當在布局中使用自定義View時,調用第2個構造方法
可第3和第4構造方干嘛用的?
2.1 使用自定義屬性 <p>
第3個構造方法,需要一個declare-styleable
自定義屬性
關于自定義屬性可以看看鴻洋大神的Android 深入理解Android中的自定義屬性
先在style.xnl
布局文件中聲明一個
<declare-styleable name="CustomProperty">
<attr name="number" format="integer" />
<attr name="text" format="string" />
</declare-styleable>
定義好自定義屬性后,LifecycleView
中使用:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<com.szlk.customview.custom.LifecycleView
custom:text="英勇青銅5"
custom:number="521"
android:layout_width="100dp"
android:layout_height="100dp" />
</RelativeLayout>
修改LifecycleView
代碼:
public class LifecycleView extends View {
private final String TAG = "LifecycleView";
public LifecycleView(Context context) {
super(context);
Log.e(TAG,"&&&第1個構造方法");
}
public LifecycleView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.e(TAG,"&&&第2個構造方法");
for(int i=0;i<attrs.getAttributeCount();i++){
Log.e(TAG, "&&&&&&第2個構造方法"+attrs.getAttributeName(i)+"--->"+attrs.getAttributeValue(i));
}
}
public LifecycleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.e(TAG,"&&&第3個構造方法");
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomProperty);
String text = ta.getString(R.styleable.CustomProperty_text);
int textAttr = ta.getInteger(R.styleable.CustomProperty_number, -1);
Log.e(TAG,"&&&第3個構造方法"+text+"-----"+textAttr);
ta.recycle();
}
public LifecycleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
Log.e(TAG,"&&&第4個構造方法");
}
}
本以為會調用第3個構造方法,Log
信息卻顯示,調用的依然是第2個構造方法,但此時已經拿到了自定義的屬性
結論2:在布局文件中使用自定義屬性后,仍然調用的第2個構造方法
在別的博客看到,想要調用到第3和4個構造方法,只有在第2個構造方法使用this
進行調用
再次對代碼進行修改:
//構造方法1
this(context,null);
//構造方法2
this(context, attrs,0);
//構造方法3
this(context, attrs, defStyleAttr,0);
此時就調用了第4和第3個構造方法,但最終還是會調用第2個構造方法
至于為啥這樣設計構造方法,雖然看了幾篇博客,我暫時依然沒有想明白。難道是一開始系統默認會給一個屬性,利用第3和第4個構造方法可以更改系統默認屬性?
2.2 關于四個參數 <p>
第一個和第二個參數都比較容易理解。
-
第3個參數
int defStyleAttr
,當前Theme中的包含的一個指向style的引用默認值為0,是一個屬性資源。沒有使用第2個參數給自定義View設置declare-styleable資源集合時,默認從這個集合里面查找布局文件中配置屬性值。傳入0表示不向該defStyleAttr中查找默認值,只要在主題中對這個屬性賦值,該View就會自動應用這個屬性的值。
-
第4個參數
int defStyleRes
,也就是在布局文件中使用的style="@style/..."
的id
只有在第三個參數defStyleAttr為0,或者主題中沒有找到這個defStyleAttr屬性的賦值時,才可以啟用。
1.defStyleRes: 指向一個style引用.
2.defStyleRes的優先級低于defStyleAttr.
這里,看得有些懵,先記住結論
在布局xml中直接定義 > 在布局xml中通過style定義 > 自定義View所在的Activity的Theme中指定style引用 > 構造函數中defStyleRes指定的默認值
這里還找了另外兩篇:
Android View 四個構造函數詳解
Android中自定義樣式與View的構造函數中的第三個參數defStyle的意義
這里實在是沒有搞清楚優先級,先挖坑了 : )
2.3 View的類似生命周期的方法 <p>
雖然View
也有好多類似Activity
那種會在不同時期進行調用的方法,但實際上View
的生命周期就3個階段:
- 處理控件動畫階段
- 處理測量的階段
- 處理繪制的階段
重寫了幾個方法:
public class LifecycleView extends View {
private final String TAG = "LifecycleView";
public LifecycleView(Context context) {
this(context, null);
Log.e(TAG, "&&&第1個構造方法");
}
public LifecycleView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.e(TAG, "&&&第2個構造方法");
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Log.e(TAG, "&&&解析布局文件onFinishInflate()");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e(TAG, "&&&測量onMeasure()");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.e(TAG, "&&&布局onLayout()");
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.e(TAG, "&&&控件大小發生改變onSizeChanged()");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e(TAG, "&&&繪制onDraw()");
}
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
Log.e(TAG, "&&&控件焦點改變onFocusChanged()");
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
Log.e(TAG, "&&&獲取或者失去焦點onWindowFocusChanged()");
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.e(TAG, "&&&開始附加在屏幕上onAttachedToWindow()");
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.e(TAG, "&&&與屏幕失去連接onDetachedFromWindow()");
}
@Override
protected void onAnimationStart() {
super.onAnimationStart();
Log.e(TAG, "&&&動畫開始onAnimationStart()");
}
@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
Log.e(TAG, "&&&動畫結束onAnimationEnd()");
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
Log.e(TAG, "&&&可見性發生改變onWindowVisibilityChanged()");
}
}
加載LifecycleView
所在的Activity
時:
onFinishInflate()
這個方法進行解析View
的xml
文件,如果使用構造方法1
,就不會回調這個方法。測量方法和布局方法會被回調好幾次。而onDraw()
竟然也回調了兩次,一直覺得會被調用一次
點擊回退鍵,結束掉Activity
時:
關閉時倒是比較容易理解
3. 最后 <p>
關于構造方法3
和4
具體在哪種情景下會被調用,在哪個具體時機被調用沒有搞明白,暫時決定先擱下,下篇打算開始學習事件體系。有知道的,請告訴一下
繼承ViewGroup
的知識點,在上篇已經有所了解。而繼承現有的View
和ViewGroup
,和直接繼承View
和ViewGroup
大體思路是一致的,甚至還簡單一點
View
的事件分發,涉及到責任鏈模式
本人很菜,有錯誤,請指出
共勉 : )