前言
關于view的使用以及列表式加載view的簡單介紹。
-
View與ViewGroup:
View作為一個視圖類的基類,給人的直觀感受就是一個視圖,即一個控件或者一個Layout形成的范圍區域。View的層次結構類似于一個完整的數據結構體系,從根部至ViewGroup一直向下遍歷,在其遞歸的過程中,會發生對View的測量、布局和繪制的過程,即View的三個基本方法onMeasure( ),onLayout( ),onDraw( ),這在自定義View中會經常用到。而其中又包含了多種回調監聽方法,如點擊,觸摸,滑動,位置,尺寸等狀態變化。而其中的一點細節,比如繪制的過程, 重繪時的invalidate()方法,以及ViewGroup與其子View的關系等在這不做詳細說明。
</br>
-
布局填充器LayoutInflater的使用說明:
LayoutInflater可以獲得布局文件的對象并且實例化,常見于一些Adapter、自定義View中,其實現方法有別于簡單的findViewById( ),是將初始化具體子View的實現變為了實例化具體子布局的實現。通常見于一些沒有被載入的界面,即不在Activity中且需要調用布局文件的情景,因為Activity的setContentView( )方法通過getWindow( )的實現已經完成了對Layout的展示:
setContentView(R.layout.activity_main);
針對于LayoutInflater的實現方法,其本質都是一致的,即通過resoure id和ViewGroup使用getSystemService( )得到該布局的對象,以下大致介紹其三種實現方法:
LayoutInflater inflater = getLayoutInflater();
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater inflater = LayoutInflater.from(context);
-
ListView的基本使用
ListView是包含多個子Item的列表式View,通過與Adapter關聯的適配器模式從而實現了經典的MVC模式,而其通過在Model中自定義實現方法來操作數據源,可關聯于基本的View-Model的概念。
針對于List的數據結構,ListView提供了幾種常用的基本適配器,如ArrayAdapter,SimpleAdapter和BaseAdapter,其中的一些重寫方法getCount( ),getItem( ),getItemId( )在使用時很方便去調用,而針對于getView( ),在對ListView優化時,通過視圖的緩存機制,為了避免多次的findViewById而使用了ViewHolder,具體細節在這里也不做詳細說明。
</br>
自定義View & addView
關于自定義View的簡單實現,以及在實例中對該View調用時的一些細節處理。
-
簡單定義的Item
有時可能由于需求的不同,如果單純的使用ListView實現多種樣式的Item時需要使用ViewHolderType處理,而ListView會對高度有一個默認設置,即在內部實現滾動效果,容易與外部布局的ScrollView的滑動事件沖突,在根據實際情況可自行選擇。在這里介紹一下通過addView方法實現的類似ListView的列表式加載,且適用于處理ScrollView的滑動事件。請看代碼:
public class CampaignListItem extends LinearLayout {
private ImageView mIconIV;
private ImageView mAttendIV;
private ImageView mNewIV;
private TextView mTitleTV;
public CampaignListItem(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.include_campaignlist, this);
mIconIV = (ImageView) findViewById(R.id.iv_attendcampaignitemicon);
mAttendIV = (ImageView) findViewById(R.id.iv_attendcampaignitemstate);
mNewIV = (ImageView) findViewById(R.id.iv_attendcampaignitemnew);
mTitleTV = (TextView) findViewById(R.id.tv_attendcampaignitemtitle);
}
public void setCampaignIcon(int resource) {
mIconIV.setImageResource(resource);
mIconIV.setVisibility(View.VISIBLE);
}
public void setCampaignAttend(int resource) {
mAttendIV.setImageResource(resource);
mAttendIV.setVisibility(View.VISIBLE);
}
public void setCampaignNew(int resource) {
mNewIV.setImageResource(resource);
mNewIV.setVisibility(View.VISIBLE);
}
public void setCampaignTitle(String title) {
mTitleTV.setText(title);
}
}
CampaignListItem這個簡單的View間接地繼承了View,其中沒有對其繪制做處理,如有感興趣的同學可以重寫其onMeasure( ),onLayout( ),onDraw( )進行重繪。邏輯簡單的話實現起來并不復雜,主要涉及到一些子View和父Layout的Height/Weight的計算和關系處理,以及一些回調方法的使用。
-
項目中的實踐
通過addView實現統一化處理的效果,類似于一個自定義的include.xml,和ListView,GridView等都有相似之處,當然也可以對每次add的View進行差異化處理,完全可以手動實現。
在實際項目中,對View的處理除了初始化時,一般都是由網絡請求后返回的data對其設置屬性。舉個例子,onResponse( )在成功時返回一個list,這時我們若想將這個list的每個元素依次添加到該自定義View中【在list.size( )較小時較為適用】,可以先對list進行遍歷,然后依次將CampaignListItem進行實例化,再通過自定義View中定義的這些方法對list的子元素屬性進行設置,從而達到了類似于ListView的getView中通過ViewHolder對象對Item進行設置的效果。
思路大致是這樣,下面從代碼層面深入解析一下:
1、調用該自定義View的構造方法CampaignListItem(Context context, AttributeSet attrs),傳入Context對象和定義的屬性變量(attrs在此未作處理),new CampaignListItem( )。
CampaignListItem campaignListItem = new CampaignListItem(getContext(), null);
2、通過其對象campaignListItem調用自定義的方法對其設置。
campaignListItem.setCampaignIcon(R.drawable.ic_launcher);
campaignListItem.setCampaignAttend(R.drawable.ic_launcher);
campaignListItem.setCampaignNew(R.drawable.ic_launcher);
campaignListItem.setCampaignTitle(R.string.text);
3、也可以對其設置監聽,如對點擊事件實現onClick( )的處理。
campaignListPlatformItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO
}
});
4、這里可能會遇到一個問題,即在依次循環中,campaignListItem會自動變成最后遍歷出的那個View,而此時如在onClick( )中處理campaignListItem,則會統一為對最后View的處理。針對這個問題,解決方法有多種,如定義一個回調的Listener,通過實現該接口做出處理等,這里介紹一種簡單的系統方法:setId( )/getId( ),setTag( )/getTag( )。
setId( )實現較為簡單,參數設為一個int型的變量即可,如循環次數 i。而setTag( )的實現需在資源文件中進行預定義,res/values/ids.xml中:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="campaignlist_type"></item>
<item type="id" name="campaignlist_state"></item>
</resources>
這樣便可在代碼中進行設置,并可在該匿名內部類中調用,從而實現對每個Item獨立setTag( ),依次實現監聽:
private String mType;
private String mState;
// TODO......
campaignListItem.setId(i);
campaignListItem.setTag(R.id.campaignlist_type, list.getType());
campaignListItem.setTag(R.id.campaignlist_state, list.getState());
campaignListPlatformItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mType = campaignListPlatformItem.getTag(R.id.campaignlist_type).toString();
mState = campaignListPlatformItem.getTag(R.id.campaignlist_state).toString();
}
});
5、循環內,父View(rootView)對循環遍歷的子Item進行addView( )。
rootView.addView(campaignListPlatformItem);
6、刷新問題的處理,結合removeView( )的實現。因為通過addView( )添加的子Item不像Adapter那樣可以動態刷新:notifyDataSetChanged( );當然,你可以自己寫出一個動態刷新的方法,或者更簡單一點,刷新時請求完網絡,判斷出哪條數據需要變化,則對其手動重設即可,這時便可調用getId( )來實現,需要注意的是將某條Item移除后,還得重新將這條Item添加到同一位置。
rootView.removeViewAt(campaignListItem.getId());
// TODO
rootView.addView(campaignListItem, campaignListItem.getId());
當然,如果在數據量不大的情況下,即不影響性能時,可考慮全部重新加載的方法,避免了錯位等問題:即先removeAll,然后再重新addAll。
private int mSize;
// TODO......
private void removeCampaign() {
this.mSize = rootView.getChildCount();
for( int i = 0; i < mSize; i++) {
// 每次remove()后,下一條Item都會前移一位,所以每次都removeViewAt(0)即可。
this.rootView.removeViewAt(0);
}
}
// 重新addView();
</br>
關于addView( )和removeView( )的實現原理
這里只從源碼的角度下找出幾個重點,有興趣的同學可以自行深入研究下,畢竟關于View這塊的水還是很深的。
-
addView( )
/** * @param child the child view to add */ public void addView(View child) { addView(child, -1); }
由注釋中可大致理解這里只是將這個View進行add,還得繼續Ctrl + Click:
/**
* @param child the child view to add
* @param index the position at which to add the child
/
public void addView(View child, int index) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
由上可見,這里是將待添加的View調用getLayoutParams( )方法初始化一個LayoutParams對象,從而可在添加進行父布局時對布局的參數(比例、位置、大小等)進行控制,如通過index進行添加時的定位。繼續Ctrl + Click:
/*
* @param child the child view to add
* @param index the position at which to add the child
* @param params the layout parameters to set on the child
*/
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
通過扒了下源代碼和查找了網上的一些資料,這里簡單說一下invalidate( )方法的實現原理。invalidate( )的主要作用是請求View樹進行重繪。其原理比較復雜,大致是通過一個循環將子視圖的dirty區域與父視圖做運算來確定最終要重繪的dirty區域,直至ViewRoot的parent為null時為止,并保存在ViewRoot中的mDirty變量中,最后調用scheduleTraversals發起一個異步的重繪請求消息,從而最終調用performTraversals( )執行重繪。(附此段落參考鏈接)
由于下面的源碼較多,這里就不一一復述,值得一提的是requestLayout( )和invalidate(true)都是對View進行一個重繪的方法,invalidate( )是View本身調用迫使view重畫,類似一個強制的調用,而requestLayout則是當view確定自身已經不再適合現有的區域時,該view本身調用這個方法要求parent view重新調用他的onMeasure( ),onLayout( )來對重新設置自己位置,是一個自適應的調用方法,如LayoutParams已經變化但還沒有更新View時調用。
-
removeView( )
了解完addView( )的原理后再來看removeView( )就簡單了,其實現方法大體類似,就不在此詳述了。查看源碼可看到其幾種實現方法:
/**
* do not invoke this method from
* draw(android.graphics.Canvas)
* onDraw(android.graphics.Canvas)
* dispatchDraw(android.graphics.Canvas) or any related method
*/
public void removeView(View view) {
removeViewInternal(view);
requestLayout();
invalidate(true);
}
/**
* Removes a view during layout.
* This is useful if in your onLayout() method, you need to remove more views
* @param view the view to remove from the group
*/
public void removeViewInLayout(View view) {
removeViewInternal(view);
}
/**
* Removes a range of views during layout.
* This is useful if in your onLayout() method, you need to remove more views
* @param start the index of the first view to remove from the group
* @param count the number of views to remove from the group
*/
public void removeViewsInLayout(int start, int count) {
removeViewsInternal(start, count);
}
/**
* Removes the view at the specified position in the group
* @param index the position in the group of the view to remove
*/
public void removeViewAt(int index) {
removeViewInternal(index, getChildAt(index));
requestLayout();
invalidate(true);
}
/**
* Removes the specified range of views from the group
* @param start the first position in the group of the range of views to remove
* @param count the number of views to remove
*/
public void removeViews(int start, int count) {
removeViewsInternal(start, count);
requestLayout();
invalidate(true);
}
private void removeViewInternal(View view) {
final int index = indexOfChild(view);
if (index >= 0) {
removeViewInternal(index, view);
}
}
</br>
尾聲
再來說兩句......
-
注意事項
1、關于調用getChildCount( )這里需結合實際情況對父View是否為空或者是否不含子View進行判斷,在ListView中調用這個方法經常會出現為0的情況,可以稍留心一下。
2、關于View的繪制,有一些回調的監聽方法,可以反饋出繪制是否完成,繪制的位置等,結合第幾條Item的id,params等可以自己實現一個動態的更新方法。
3、關于ListView優化時使用的ViewHolder,也是通過convertView的setTag( )/getTag( )進行處理,可以參考結合ListView較為成熟的處理方法。
4、關于invalidate( )的底層代碼有興趣的可以深入研究。但是在實際繪制時經常會出現一些View沒有對應上的問題,即在遍歷時的addView( )沒有處理好。
5、關于自定義View的實現,不僅限于本文中提到的對原Item進行屬性設置的方法,可以多元化的自定義View,喔,你的能量超乎你想像。
-
Github地址:
-
雜談一下:
后續會不定期更新一點實踐中的心得,可能敘述的不夠完善,希望大家可以一起交流~~~
</br>