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>

推荐阅读更多精彩内容