前言
關(guān)于view的使用以及列表式加載view的簡單介紹隐圾。
-
View與ViewGroup:
View作為一個(gè)視圖類的基類牢酵,給人的直觀感受就是一個(gè)視圖,即一個(gè)控件或者一個(gè)Layout形成的范圍區(qū)域默蚌。View的層次結(jié)構(gòu)類似于一個(gè)完整的數(shù)據(jù)結(jié)構(gòu)體系母剥,從根部至ViewGroup一直向下遍歷,在其遞歸的過程中茵烈,會發(fā)生對View的測量百匆、布局和繪制的過程,即View的三個(gè)基本方法onMeasure( )呜投,onLayout( )加匈,onDraw( )存璃,這在自定義View中會經(jīng)常用到。而其中又包含了多種回調(diào)監(jiān)聽方法雕拼,如點(diǎn)擊纵东,觸摸,滑動啥寇,位置偎球,尺寸等狀態(tài)變化。而其中的一點(diǎn)細(xì)節(jié)辑甜,比如繪制的過程衰絮, 重繪時(shí)的invalidate()方法,以及ViewGroup與其子View的關(guān)系等在這不做詳細(xì)說明栈戳。
</br>
-
布局填充器LayoutInflater的使用說明:
LayoutInflater可以獲得布局文件的對象并且實(shí)例化岂傲,常見于一些Adapter、自定義View中子檀,其實(shí)現(xiàn)方法有別于簡單的findViewById( )镊掖,是將初始化具體子View的實(shí)現(xiàn)變?yōu)榱藢?shí)例化具體子布局的實(shí)現(xiàn)。通常見于一些沒有被載入的界面褂痰,即不在Activity中且需要調(diào)用布局文件的情景亩进,因?yàn)锳ctivity的setContentView( )方法通過getWindow( )的實(shí)現(xiàn)已經(jīng)完成了對Layout的展示:
setContentView(R.layout.activity_main);
針對于LayoutInflater的實(shí)現(xiàn)方法,其本質(zhì)都是一致的缩歪,即通過resoure id和ViewGroup使用getSystemService( )得到該布局的對象归薛,以下大致介紹其三種實(shí)現(xiàn)方法:
LayoutInflater inflater = getLayoutInflater();
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater inflater = LayoutInflater.from(context);
-
ListView的基本使用
ListView是包含多個(gè)子Item的列表式View,通過與Adapter關(guān)聯(lián)的適配器模式從而實(shí)現(xiàn)了經(jīng)典的MVC模式匪蝙,而其通過在Model中自定義實(shí)現(xiàn)方法來操作數(shù)據(jù)源主籍,可關(guān)聯(lián)于基本的View-Model的概念。
針對于List的數(shù)據(jù)結(jié)構(gòu)逛球,ListView提供了幾種常用的基本適配器千元,如ArrayAdapter,SimpleAdapter和BaseAdapter颤绕,其中的一些重寫方法getCount( )幸海,getItem( ),getItemId( )在使用時(shí)很方便去調(diào)用奥务,而針對于getView( )物独,在對ListView優(yōu)化時(shí),通過視圖的緩存機(jī)制氯葬,為了避免多次的findViewById而使用了ViewHolder挡篓,具體細(xì)節(jié)在這里也不做詳細(xì)說明。
</br>
自定義View & addView
關(guān)于自定義View的簡單實(shí)現(xiàn)帚称,以及在實(shí)例中對該View調(diào)用時(shí)的一些細(xì)節(jié)處理瞻凤。
-
簡單定義的Item
有時(shí)可能由于需求的不同憨攒,如果單純的使用ListView實(shí)現(xiàn)多種樣式的Item時(shí)需要使用ViewHolderType處理,而ListView會對高度有一個(gè)默認(rèn)設(shè)置阀参,即在內(nèi)部實(shí)現(xiàn)滾動效果肝集,容易與外部布局的ScrollView的滑動事件沖突,在根據(jù)實(shí)際情況可自行選擇蛛壳。在這里介紹一下通過addView方法實(shí)現(xiàn)的類似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這個(gè)簡單的View間接地繼承了View衙荐,其中沒有對其繪制做處理捞挥,如有感興趣的同學(xué)可以重寫其onMeasure( ),onLayout( )忧吟,onDraw( )進(jìn)行重繪砌函。邏輯簡單的話實(shí)現(xiàn)起來并不復(fù)雜,主要涉及到一些子View和父Layout的Height/Weight的計(jì)算和關(guān)系處理溜族,以及一些回調(diào)方法的使用讹俊。
-
項(xiàng)目中的實(shí)踐
通過addView實(shí)現(xiàn)統(tǒng)一化處理的效果,類似于一個(gè)自定義的include.xml煌抒,和ListView仍劈,GridView等都有相似之處,當(dāng)然也可以對每次add的View進(jìn)行差異化處理寡壮,完全可以手動實(shí)現(xiàn)贩疙。
在實(shí)際項(xiàng)目中,對View的處理除了初始化時(shí)况既,一般都是由網(wǎng)絡(luò)請求后返回的data對其設(shè)置屬性这溅。舉個(gè)例子,onResponse( )在成功時(shí)返回一個(gè)list棒仍,這時(shí)我們?nèi)粝雽⑦@個(gè)list的每個(gè)元素依次添加到該自定義View中【在list.size( )較小時(shí)較為適用】芍躏,可以先對list進(jìn)行遍歷,然后依次將CampaignListItem進(jìn)行實(shí)例化降狠,再通過自定義View中定義的這些方法對list的子元素屬性進(jìn)行設(shè)置,從而達(dá)到了類似于ListView的getView中通過ViewHolder對象對Item進(jìn)行設(shè)置的效果庇楞。
思路大致是這樣榜配,下面從代碼層面深入解析一下:
1、調(diào)用該自定義View的構(gòu)造方法CampaignListItem(Context context, AttributeSet attrs)吕晌,傳入Context對象和定義的屬性變量(attrs在此未作處理)蛋褥,new CampaignListItem( )。
CampaignListItem campaignListItem = new CampaignListItem(getContext(), null);
2睛驳、通過其對象campaignListItem調(diào)用自定義的方法對其設(shè)置烙心。
campaignListItem.setCampaignIcon(R.drawable.ic_launcher);
campaignListItem.setCampaignAttend(R.drawable.ic_launcher);
campaignListItem.setCampaignNew(R.drawable.ic_launcher);
campaignListItem.setCampaignTitle(R.string.text);
3膜廊、也可以對其設(shè)置監(jiān)聽,如對點(diǎn)擊事件實(shí)現(xiàn)onClick( )的處理淫茵。
campaignListPlatformItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO
}
});
4爪瓜、這里可能會遇到一個(gè)問題,即在依次循環(huán)中匙瘪,campaignListItem會自動變成最后遍歷出的那個(gè)View铆铆,而此時(shí)如在onClick( )中處理campaignListItem,則會統(tǒng)一為對最后View的處理丹喻。針對這個(gè)問題薄货,解決方法有多種,如定義一個(gè)回調(diào)的Listener碍论,通過實(shí)現(xiàn)該接口做出處理等谅猾,這里介紹一種簡單的系統(tǒng)方法:setId( )/getId( ),setTag( )/getTag( )鳍悠。
setId( )實(shí)現(xiàn)較為簡單税娜,參數(shù)設(shè)為一個(gè)int型的變量即可,如循環(huán)次數(shù) i贼涩。而setTag( )的實(shí)現(xiàn)需在資源文件中進(jìn)行預(yù)定義巧涧,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>
這樣便可在代碼中進(jìn)行設(shè)置,并可在該匿名內(nèi)部類中調(diào)用遥倦,從而實(shí)現(xiàn)對每個(gè)Item獨(dú)立setTag( )谤绳,依次實(shí)現(xiàn)監(jiān)聽:
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、循環(huán)內(nèi)袒哥,父View(rootView)對循環(huán)遍歷的子Item進(jìn)行addView( )缩筛。
rootView.addView(campaignListPlatformItem);
6、刷新問題的處理堡称,結(jié)合removeView( )的實(shí)現(xiàn)瞎抛。因?yàn)橥ㄟ^addView( )添加的子Item不像Adapter那樣可以動態(tài)刷新:notifyDataSetChanged( );當(dāng)然,你可以自己寫出一個(gè)動態(tài)刷新的方法却紧,或者更簡單一點(diǎn)桐臊,刷新時(shí)請求完網(wǎng)絡(luò),判斷出哪條數(shù)據(jù)需要變化晓殊,則對其手動重設(shè)即可断凶,這時(shí)便可調(diào)用getId( )來實(shí)現(xiàn),需要注意的是將某條Item移除后巫俺,還得重新將這條Item添加到同一位置认烁。
rootView.removeViewAt(campaignListItem.getId());
// TODO
rootView.addView(campaignListItem, campaignListItem.getId());
當(dāng)然,如果在數(shù)據(jù)量不大的情況下,即不影響性能時(shí)却嗡,可考慮全部重新加載的方法舶沛,避免了錯(cuò)位等問題:即先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>
關(guān)于addView( )和removeView( )的實(shí)現(xiàn)原理
這里只從源碼的角度下找出幾個(gè)重點(diǎn)柱彻,有興趣的同學(xué)可以自行深入研究下,畢竟關(guān)于View這塊的水還是很深的餐胀。
-
addView( )
/** * @param child the child view to add */ public void addView(View child) { addView(child, -1); }
由注釋中可大致理解這里只是將這個(gè)View進(jìn)行add哟楷,還得繼續(xù)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調(diào)用getLayoutParams( )方法初始化一個(gè)LayoutParams對象否灾,從而可在添加進(jìn)行父布局時(shí)對布局的參數(shù)(比例卖擅、位置、大小等)進(jìn)行控制墨技,如通過index進(jìn)行添加時(shí)的定位惩阶。繼續(xù)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);
}
通過扒了下源代碼和查找了網(wǎng)上的一些資料,這里簡單說一下invalidate( )方法的實(shí)現(xiàn)原理扣汪。invalidate( )的主要作用是請求View樹進(jìn)行重繪断楷。其原理比較復(fù)雜,大致是通過一個(gè)循環(huán)將子視圖的dirty區(qū)域與父視圖做運(yùn)算來確定最終要重繪的dirty區(qū)域崭别,直至ViewRoot的parent為null時(shí)為止冬筒,并保存在ViewRoot中的mDirty變量中,最后調(diào)用scheduleTraversals發(fā)起一個(gè)異步的重繪請求消息茅主,從而最終調(diào)用performTraversals( )執(zhí)行重繪舞痰。(附此段落參考鏈接)
由于下面的源碼較多,這里就不一一復(fù)述诀姚,值得一提的是requestLayout( )和invalidate(true)都是對View進(jìn)行一個(gè)重繪的方法响牛,invalidate( )是View本身調(diào)用迫使view重畫,類似一個(gè)強(qiáng)制的調(diào)用赫段,而requestLayout則是當(dāng)view確定自身已經(jīng)不再適合現(xiàn)有的區(qū)域時(shí)呀打,該view本身調(diào)用這個(gè)方法要求parent view重新調(diào)用他的onMeasure( ),onLayout( )來對重新設(shè)置自己位置糯笙,是一個(gè)自適應(yīng)的調(diào)用方法贬丛,如LayoutParams已經(jīng)變化但還沒有更新View時(shí)調(diào)用。
-
removeView( )
了解完addView( )的原理后再來看removeView( )就簡單了炬丸,其實(shí)現(xiàn)方法大體類似,就不在此詳述了。查看源碼可看到其幾種實(shí)現(xiàn)方法:
/**
* 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>
尾聲
再來說兩句......
-
注意事項(xiàng)
1稠炬、關(guān)于調(diào)用getChildCount( )這里需結(jié)合實(shí)際情況對父View是否為空或者是否不含子View進(jìn)行判斷焕阿,在ListView中調(diào)用這個(gè)方法經(jīng)常會出現(xiàn)為0的情況,可以稍留心一下首启。
2暮屡、關(guān)于View的繪制,有一些回調(diào)的監(jiān)聽方法毅桃,可以反饋出繪制是否完成褒纲,繪制的位置等,結(jié)合第幾條Item的id钥飞,params等可以自己實(shí)現(xiàn)一個(gè)動態(tài)的更新方法莺掠。
3、關(guān)于ListView優(yōu)化時(shí)使用的ViewHolder读宙,也是通過convertView的setTag( )/getTag( )進(jìn)行處理彻秆,可以參考結(jié)合ListView較為成熟的處理方法。
4结闸、關(guān)于invalidate( )的底層代碼有興趣的可以深入研究唇兑。但是在實(shí)際繪制時(shí)經(jīng)常會出現(xiàn)一些View沒有對應(yīng)上的問題,即在遍歷時(shí)的addView( )沒有處理好桦锄。
5扎附、關(guān)于自定義View的實(shí)現(xiàn),不僅限于本文中提到的對原Item進(jìn)行屬性設(shè)置的方法结耀,可以多元化的自定義View留夜,喔,你的能量超乎你想像饼记。
-
Github地址:
-
雜談一下:
后續(xù)會不定期更新一點(diǎn)實(shí)踐中的心得香伴,可能敘述的不夠完善,希望大家可以一起交流~~~
</br>