自定義控件其實很簡單7/12
來源:程序員人生 發布時間:2015-01-26 09:18:50 閱讀次數:2539次
尊重原創轉載請注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!
炮兵
鎮樓
要在數量上統計中國菜的品種,在地域上毫無爭議地劃分菜系,在今天,是1件幾近不可能完成的事……Cut……
……抱歉……忘吃藥了,再來1遍。如果非要對自定義控件的流程進行1個簡單的劃分,我會嘗試將其分為3大部份:控件的繪制、控件的丈量和控件的交互行動。前面我們用了6節的篇幅和1個翻頁的例子來對控件的繪制有了1個全新的認識但是我們所做出的所有例子都是不完善的,為何這么說呢,還是先來看個sample:
/**
*
* @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;
}
}
這個例子呢非常簡單,我們用它來摹擬類似ImageView的效果顯示1張圖片,在MainActivity中我們獲得該控件并為其設置Bitmap:
/**
* 主界面
*
* @author Aige {@link http://blog.csdn.net/aigestudio}
* @since 2014/11/17
*/
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);
}
}
此時運行效果以下:

很簡單對吧,可是上面的代碼實際上是有個問題的,至于甚么問題?我們待會再說,就看你通過前面我們的學習能不能發現了。這1節我們重點是控件的丈量,大家不知道注意沒有,這個系列文章的命名我用了“控件”而非“View”其實目的就是說明我們的控件不但包括View也應當包括ViewGroup,固然你也能夠以官方的方式將其分為控件和布局,不過我更喜歡View和ViewGroup,好了空話不說,我們先來看看View的丈量方式,上面的代碼中MainActivity對應的布局文件以下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.ImgView
android:id="@+id/main_pv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
既然我們的自定義View也算1個控件那末我們也能夠像平時做布局那樣往我們的LinearLayout中添加各種各樣的其他控件對吧:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.ImgView
android:id="@+id/main_pv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</LinearLayout>
但是運行后你卻發現我們的Button和TextView卻沒有顯示在屏幕上,這時候你可能會說那固然咯,由于我們的ImgViewlayout_width和layout_height均為match_parent,可是即使你將其改成wrap_content:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.ImgView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!-- ……省略1些代碼…… -->
</LinearLayout>
結果也1樣,這時候你肯定很困惑,不解的主要緣由是沒有弄懂View的丈量機制,在前面的幾節中我們或多或少有提到控件的丈量,也曾說過Android提供給我們能夠操縱控件丈量的方法是onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
默許情況下onMeasure方法中只是簡單地將簽名列表中的兩個int型參數回傳給父類的onMeasure方法,然后由父類的方法去計算出終究的丈量值。但是,這里有個問題非常重要,就是onMeasure簽名列表中的這兩個參數是從何而來,這里可以告知大家的是,這兩個參數是由view的父容器,代碼中也就是我們的LinearLayout傳遞進來的,很多初學Android的朋友會將位于xml布局文件頂真個控件稱之為根布局,比如這里我們的LinearLayout,而事實上在Android的GUI框架中,這個LinearLayout還稱不上根布局,我們知道1個Activity可以對應1個View(也能夠是ViewGroup),很多情況下我們會通過Activity的setContentView方法去設置我們的View:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
setContentView在Activity內的實現也非常簡單,就是調用getWindow方法獲得1個Window類型的對象并調用其setContentView方法:
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
而這個Window對象
public Window getWindow() {
return mWindow;
}
其本質也就是1個PhoneWindow,在Activity的attach方法中通過makeNewWindow生成:
final void attach(Context context, ActivityThread aThread,
// 此處省去1些代碼……
mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
// 此處省去巨量代碼……
}
在PolicyManager中通過反射的方式獲得com.android.internal.policy.impl.Policy的1個實例:
public final class PolicyManager {
private static final String POLICY_IMPL_CLASS_NAME =
"com.android.internal.policy.impl.Policy";
private static final IPolicy sPolicy;
static {
try {
Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
sPolicy = (IPolicy)policyClass.newInstance();
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
} catch (InstantiationException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
}
}
// 省去構造方法……
public static Window makeNewWindow(Context context) {
return sPolicy.makeNewWindow(context);
}
// 省去無關代碼……
}
并通過其內部的makeNewWindow實現返回1個PhoneWindow對象:
public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
PhoneWindow是Window的1個子類,其對Window中定義的大量抽象方法作了具體的實現,比如我們的setContentView方法在Window中僅做了1個抽象方法定義:
public abstract class Window {
// 省去不可估計的代碼……
public abstract void setContentView(int layoutResID);
public abstract void setContentView(View view);
public abstract void setContentView(View view, ViewGroup.LayoutParams params);
// 省去數以億計的代碼……
}
其在PhoneWindow中都有具體的實現:
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// 省去草泥馬個代碼……
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mContentParent.addView(view, params);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
// 省去法克魷個代碼……
}
固然如果你要是使用了TV的SDK那末這里就不是PhoneWindow而是TVWindow了,至因而不是呢?留給大家去驗證。到這里我們都還沒完,在PhoneWindow的setContentView方法中先會去判斷mContentParent這個援用是不是為空,如果為空則表示我們是第1次生成那末調用installDecor方法去生成1些具體的對象否則清空該mContentParent下的所有子元素(注意mContentParent是1個ViewGroup)并通過LayoutInflater將xml布局轉換為View
Tree添加至mContentParent中(這里根據setContentView(int layoutResID)方法分析,其他重載方法類似),installDecor方法做的事相對多但不復雜,首先是對DecorView類型的mDecor成員變量賦值繼而將其注入generateLayout方法生成我們的mContentParent:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
// 省省省……
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// 省省省……
}
// 省省省……
}
generateLayout方法中做的事就多了,簡直可以跟performTraversals拼,這里不貼代碼了簡單分析1下,generateLayout方法中主要根據當前我們的Style類型為當前Window選擇不同的布局文件,看到這里,想必大家也該意想到,這才是我們的“根布局”,其會指定1個用來寄存我們自定義布局文件(也就是我們口頭上常說的根布局比如我們例子中的LinearLayout)的ViewGroup,1般情況下這個ViewGroup的重擔由FrameLayout來承當,這也是為何我們在獲得我們xml布局文件中的頂層布局時調用其getParent()方法會返回FrameLayout對象的緣由,其id為android:id="@android:id/content":
protected ViewGroup generateLayout(DecorView decor) {
// 省去巨量代碼……
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// 省去1些代碼……
}
在這個Window布局文件被肯定后,mDecor則會將該布局所生成的對應View添加進來并獲得id為content的View將其賦給mContentParent,至此mContentParent和mDecor均已生成,而我們xml布局文件中的布局則會被添加至mContentParent。對應關系類似下圖:

說了大半天才理清這個小關系,但是我們還沒說到重點…………………………就是widthMeasureSpec和heightMeasureSpec究竟是從哪來的……………………如果我們不做上面的1個分析,很多童鞋壓根無從下手,有了上面1個分析,我們知道我們界面的真正根視圖應當是DecorView,那末我們的widthMeasureSpec和heightMeasureSpec應當從這里或更上1層PhoneWindow傳遞進來對吧,但是DecorView是FrameLayout的1個實例,在FrameLayout的onMeasure中我們確切有對子元素的丈量,但是問題是FrameLayout:onMeasure方法中的widthMeasureSpec和heightMeasureSpec又是從何而來呢?追溯上去我們又回到了View…………………………………………………………不了解Android
GUI框架的童鞋邁出的第1步就被無情地煽了回去。其實在Android中我們可以在很多方面看到類似MVC架構的影子,比如最最多見的就是我們的xml界面布局――Activity等組件――model數據之間的關系,而在全部GUI的框架中,我們也能夠對其做出類似的計劃,View在設計進程中就注定了其只會對顯示數據進行處理比如我們的丈量布局和繪制還有動畫等等,而承當Controller控制器重擔的是誰呢?在Android中這1功能由ViewRootImpl承當,我們在前面提到過這個類,其負責的東西很多,比如我們窗口的顯示、用戶的輸入輸出固然還有關于處理我們繪制流程的方法:
private void performTraversals() {
// ………………啦啦啦啦………………
}
performTraversals方法是處理繪制流程的1個開始,內部邏輯相當相當多&復雜,雖然沒有View類復雜……但是讓我選的話我寧愿看全部View類也不愿看performTraversals方法那邪惡的邏輯…………
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈