Android View 之 addView與listView

前言

關于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地址:

https://github.com/Ivorfason

  • 雜談一下:

后續會不定期更新一點實踐中的心得,可能敘述的不夠完善,希望大家可以一起交流~~~
</br>

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容