前面的话
开源项目Android-SlideExpandableListView是一个简单的介绍列表项动画展示的小型项目,分析这个项目可以对自定义框架及列表类动画实现有个比较清晰的认识,工作中中时常根据需求扩展定义自己的适配器,虽然具体需求不同,但架构类似,本文把最近关于该开源项目的研究心得整理分享,共同学习~
项目简介
github地址https://github.com/tjerkw/Android-SlideExpandableListView
这是个入门级的列表项动画展示框架,实现效果如下
当点击more按钮时,对应列表项展示下拉动画,将含有两个按钮的扩展布局显示或隐藏,其自定义的动画效果比android自带的expandlistview要好一些。更详细的项目简介请参考http://a.code4app.com/android/SlideExpandableListView/524507cd6803faeb7e000000。顺带推荐code4App这个网站,本来是专为ios开发者的优秀开源项目分享平台,口号就是避免重复造轮子,现在有个android版本的http://a.code4app.com/,感兴趣的同学可自行检索。
代码实现
这个项目的实现思路分成以下几步
1) 封装一个基本的列表适配器
2) 为封装适配器添加新功能(如动画显示/收缩) --》CSDN博客编辑器从此往下无法高亮及设置标题。
3) 使用动画展示或隐藏列表项下拉布局
4) 用户设置扩展布局中的控件监听器(即含有两个按钮的初始隐藏布局)
5) 扩展使用2中的适配器,可自定义扩展布局及控件id,并为其绑定方法。
1) 封装一个基本的列表适配器。
封装适配器WrapperListAdapterImpl.java (本文列出代码为核心实现代码,利于文章整理讲解,非项目全部代码,如需阅读全部代码可在github上自行下载,下同)
<span style="font-family:KaiTi_GB2312;font-size:18px;">
public abstract class WrapperListAdapterImpl extends BaseAdapter implements WrapperListAdapter { protected ListAdapter wrapped; public WrapperListAdapterImpl(ListAdapter wrapped) { this.wrapped = wrapped; } @Override public ListAdapter getWrappedAdapter() { return wrapped; } @Override public boolean areAllItemsEnabled() { return wrapped.areAllItemsEnabled(); } @Override public boolean isEnabled(int i) { return wrapped.isEnabled(i); } @Override public void registerDataSetObserver(DataSetObserver dataSetObserver) { wrapped.registerDataSetObserver(dataSetObserver); } @Override public void unregisterDataSetObserver(DataSetObserver dataSetObserver) { wrapped.unregisterDataSetObserver(dataSetObserver); } @Override public int getCount() { return wrapped.getCount(); } @Override public Object getItem(int i) { return wrapped.getItem(i); } @Override public long getItemId(int i) { return wrapped.getItemId(i); } @Override public boolean hasStableIds() { return wrapped.hasStableIds(); } @Override public View getView(int position, View view, ViewGroup viewGroup) { return wrapped.getView(position, view, viewGroup); } @Override public int getItemViewType(int i) { return wrapped.getItemViewType(i); } @Override public int getViewTypeCount() { return wrapped.getViewTypeCount(); } @Override public boolean isEmpty() { return wrapped.isEmpty(); } }</span>
这个封装过程是进行适配器功能扩展最基本的一步,WrapperListAdapterImpl 封装了一个ListAdapter并将其作为自己的一个实例域wrapped,而重写的每个方法仅仅是简单使用委托的方式,让被委托对象wrapped来实现,这种封装形式在Android框架层源码中也经常被用到(如ContextWrapper),这样做的好处是能够让子类扩展被封装对象的某些特性而不会引起其它变化,以本文为例,单独使用WrapperListAdapterImpl没有任何意义,但如果我们想创建一个SlideAdapter,这个适配器会扩展普通ListAdapter的功能,实现滑动动画效果,那么WrapperListAdapterImpl就是很好的抽象基类了。
2) 为封装适配器添加新功能(如动画显示/收缩) AbstractSlideExpandableListAdapter.java
//这个是前述封装适配器后做的最重要的扩展,通过重写getView方法,定义需要的动画展示行为 @Override public View getView(int position, View view, ViewGroup viewGroup) { view = wrapped.getView(position, view, viewGroup); //使用被封装适配器的getView方法获得列表项视图, //被封装的原始适配器提供view,这可供扩展新的特性,如本文中的动画 enableFor(view, position); //扩展动画功能 return view; } public void enableFor(View parent, int position) { View more = getExpandToggleButton(parent); //获得more点击按钮 View itemToolbar = getExpandableView(parent); //获得动画显示/隐藏的布局 itemToolbar.measure(parent.getWidth(), parent.getHeight()); enableFor(more, itemToolbar, position); // 增加动画展示功能 } private void enableFor(final View button, final View target, final int position) { if(target == lastOpen && position!=lastOpenPosition) { // lastOpen is recycled, so its reference is false lastOpen = null; } if(position == lastOpenPosition) { // re reference to the last view // so when can animate it when collapsed lastOpen = target; } int height = viewHeights.get(position, -1); if(height == -1) { viewHeights.put(position, target.getMeasuredHeight()); updateExpandable(target,position); } else { updateExpandable(target, position); } //为more按钮设置监听方法,动画显示/隐藏扩展布局 button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View view) { Animation a = target.getAnimation(); if (a != null && a.hasStarted() && !a.hasEnded()) { a.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { view.performClick(); } @Override public void onAnimationRepeat(Animation animation) { } }); } else { target.setAnimation(null); int type = target.getVisibility() == View.VISIBLE ? ExpandCollapseAnimation.COLLAPSE : ExpandCollapseAnimation.EXPAND; // remember the state if (type == ExpandCollapseAnimation.EXPAND) { openItems.set(position, true); } else { openItems.set(position, false); } // check if we need to collapse a different view if (type == ExpandCollapseAnimation.EXPAND) { if (lastOpenPosition != -1 && lastOpenPosition != position) { if (lastOpen != null) { animateView(lastOpen, ExpandCollapseAnimation.COLLAPSE); } openItems.set(lastOpenPosition, false); } lastOpen = target; lastOpenPosition = position; } else if (lastOpenPosition == position) { lastOpenPosition = -1; } animateView(target, type); //具体的执行显示/隐藏动画的方法 } } }); } private void updateExpandable(View target, int position) { final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)target.getLayoutParams(); if(openItems.get(position)) { target.setVisibility(View.VISIBLE); params.bottomMargin = 0; } else { target.setVisibility(View.GONE); params.bottomMargin = 0-viewHeights.get(position); } } /** * 执行隐藏/显示动画的具体实现,依赖于ExpandCollapseAnimation动画工具类 * Performs either COLLAPSE or EXPAND animation on the target view * @param target the view to animate * @param type the animation type, either ExpandCollapseAnimation.COLLAPSE * or ExpandCollapseAnimation.EXPAND */ private void animateView(final View target, final int type) { Animation anim = new ExpandCollapseAnimation( target, type ); anim.setDuration(getAnimationDuration()); target.startAnimation(anim); }</span>
3)使用动画展示或隐藏列表项下拉布局ExpandCollapseAnimation.java
这个是扩展/缩放动画的工具类,上文已经看到,animateView方法通过设置这个动画类来完成需要的操作。
<span style="font-family:KaiTi_GB2312;font-size:18px;">
public class ExpandCollapseAnimation extends Animation { private View mAnimatedView; private int mEndHeight; private int mType; public final static int COLLAPSE = 1; public final static int EXPAND = 0; private LinearLayout.LayoutParams mLayoutParams; /** * Initializes expand collapse animation, has two types, collapse (1) and expand (0). * @param view The view to animate * @param type The type of animation: 0 will expand from gone and 0 size to visible and layout size defined in xml. * 1 will collapse view and set to gone */ public ExpandCollapseAnimation(View view, int type) { mAnimatedView = view; mEndHeight = mAnimatedView.getMeasuredHeight(); mLayoutParams = ((LinearLayout.LayoutParams) view.getLayoutParams()); mType = type; if(mType == EXPAND) { mLayoutParams.bottomMargin = -mEndHeight; } else { mLayoutParams.bottomMargin = 0; } view.setVisibility(View.VISIBLE); } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { super.applyTransformation(interpolatedTime, t); if (interpolatedTime < 1.0f) { if(mType == EXPAND) { mLayoutParams.bottomMargin = -mEndHeight + (int) (mEndHeight * interpolatedTime); } else { mLayoutParams.bottomMargin = - (int) (mEndHeight * interpolatedTime); } Log.d("ExpandCollapseAnimation", "anim height " + mLayoutParams.bottomMargin); mAnimatedView.requestLayout(); } else { if(mType == EXPAND) { mLayoutParams.bottomMargin = 0; mAnimatedView.requestLayout(); } else { mLayoutParams.bottomMargin = -mEndHeight; mAnimatedView.setVisibility(View.GONE); mAnimatedView.requestLayout(); } } } } </span>
这个动画类实现了该开源项目的具体效果,根据当前是扩展还是缩放类型设置bottomMargin,动画时间已经在AbstractSlideExpandableListAdapter中定义,这个开源项目将动画实现放到一个单一的类中,符合单一职责原则,如果想要定义其它的动画效果或者形式,只需要修改这个动画类即可。
4) 用户设置扩展布局中的控件监听器 ExampleActivity.java
<span style="font-family:KaiTi_GB2312;font-size:18px;">
ActionSlideExpandableListView list = (ActionSlideExpandableListView)this.findViewById(R.id.list); // fill the list with data list.setAdapter(buildDummyData()); // listen for events in the two buttons for every list item. // the 'position' var will tell which list item is clicked list.setItemActionListener(new ActionSlideExpandableListView.OnActionClickListener() { @Override public void onClick(View listView, View buttonview, int position) { /** * Normally you would put a switch * statement here, and depending on * view.getId() you would perform a * different action. */ String actionName = ""; if(buttonview.getId()==R.id.buttonA) { actionName = "buttonA"; } else { actionName = "ButtonB"; } /** * For testing sake we just show a toast */ Toast.makeText( ExampleActivity.this, "Clicked Action: "+actionName+" in list item "+position, Toast.LENGTH_SHORT ).show(); } // note that we also add 1 or more ids to the setItemActionListener // this is needed in order for the listview to discover the buttons }, R.id.buttonA, R.id.buttonB);
这个开源框架运行用户来定义扩展界面的两个按钮需要执行什么样的功能,正如所有框架层的控件一样,通过提供接口及绑定方式,用户来设置控件监听器,框架层要做的就是在扩展布局中的按钮被点击时,调用这些监听器中的方法即可。如下:
<span style="font-family:KaiTi_GB2312;font-size:18px;">
public void setAdapter(ListAdapter adapter) { super.setAdapter(new WrapperListAdapterImpl(adapter) { @Override public View getView(final int position, View view, ViewGroup viewGroup) { final View listView = wrapped.getView(position, view, viewGroup);//得到封装的适配的列表项视图 // add the action listeners if(buttonIds != null && listView!=null) { for(int id : buttonIds) { View buttonView = listView.findViewById(id); if(buttonView!=null) { buttonView.findViewById(id).setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { if(listener!=null) { listener.onClick(listView, view, position); //当扩展视图上的按钮被点击时,调用用户 //定义的监听器方法 } } }); } } } return listView; } }); }</span>
ActionSlideExpandableListView用了一种比较巧妙的方式设置监听器,通过装饰者模式将普通的listadapter装饰成点击扩展区按钮就会调用用户监听器方法的adapter,这其中新建了一个封装类并重写getView,和添加动画效果职责一样,这次添加的职责是监听器调用,看看这像哪种模式?
5)扩展使用2中的适配器,可自定义扩展布局及控件id,并为其绑定方法。
SlideExpandableListAdapter是这个开源框架提供的一个自定义列表适配器,如前文所述,这个适配器扩展普通列表适配器并重写了getView方法,引入了动画相关类及控制参数,实现了点击more按钮时动画显示/隐藏扩展布局的功能。这个是整个开源项目想要实现的,作为框架库,这个可以运行用户自定义列表项布局和绑定扩展布局中的控件方法。
自定义列表项布局比较容易实现,因为SlideExpandableListAdapter已经实现了基本的动画功能,用户只需要自定义布局xml文件以及触发按钮(more按钮)及扩展视图的id即可。
<span style="font-family:KaiTi_GB2312;font-size:18px;">
ListView list = ... your list view ListAdapter adapter = ... your list adapter // now simply wrap the adapter // and indicate the ids of your toggle button // and expandable view list.setAdapter( new SlideExpandableListAdapter( adapter, R.id.expandable_toggle_button, R.id.expandable ) );</span>
绑定扩展布局中的控件方法这个暂时需要使用框架中提供的ActionSlideExpandableListView,这个自定义列表实现了用户绑定功能,只需要提供扩展视图中的控件id及对于绑定方法即可,如4中的设置方式,只是需要把R.id.buttonA, R.id.buttonB换成是自定义中的控件id即可。
项目结构
该开源项目中适配器的结构简图如下:
1)使用装饰者模式实现扩展动画功能。WrapperListAdapterImpl将普通适配器封装起来,其子类AbstractSlideExpandableListAdapter通过重写getView方法引入动画缩放效果并实现了具体的业务逻辑(如当展示第二项时前一个打开项会动画隐藏),可以说AbstractSlideExpandableListAdapter较普通的列表适配器添加了动画展示的功能,而两者又都是ListAdapter类型,这就解释了装饰者模式的特定,动态的为类对象添加职责,在exampleActivity中也可以看到,list.setAdapter(buildDummyData());列表传递了一个普通的ListAdapter,但通过listview的层层继承,这个普通的adapter已经被装饰成一个SlideExpandableListAdapter类型的适配器,这就告诉我们:想要一个新的自定义适配器控件,装饰原有普通控件是个简洁有效的方式。
2)使用模板方法模式完成视图检索。AbstractSlideExpandableListAdapter中有两个方法被定义成抽象方法,分别是getExpandToggleButton :获取动画触发按钮 和 getExpandableView :获取扩展视图,这个符合模板方法模式的思想,父类中的一些操作因具体情况不同留给子类去实现,稳定的算法骨架则在父类中,这两个方法在父类的enableFor方法中被顺序调用,子类SlideExpandableListAdapter实现具体的方式,通过子类来具体实现获得触发按钮和扩展视图,不同的子类有不同的实现方式,如果不需要使用该触发按钮,可以将返回视图设为空,则相当于禁止动画效果展示。
3)被封装的普通列表适配器有何作用。研究整个代码结构可以发现,被封装的普通适配器以实例域wrapped的方式,仅仅提供了自己的getview方法,也就是说普通适配器仅仅起到了提供原始列表项视图对象的作用,扩展的适配器则实现了对于该视图的绑定和增加动画特效的操作,普通适配器提供的视图是包含隐藏的扩展布局的,这个在普通适配器中已经配置好,AbstractSlideExpandableListAdapter所要做的是增加动画等相关特效而已。
除了上文提到的适配器架构,该开源项目的作者还是自定义了一些列的listview,如下图
这种自定义的listview结构体现了单一职责原则,SlideExpandableListView较普通的listview并没有太大区别,只是增加了通过点击item项来触发动画的功能,ActionSlideExpandableListView则更加纯粹,提供了接口并调用的方式,可以让用户为扩展界面的控件自由设置监听器,虽然这符合了单一职责原则,但是个人感觉这种隔离并不是非常必要,过度的分离反而容易造成逻辑上的混乱,对于一个自定义的SlideExpandableListView,直接为其添加监听器相关代码也是可以考虑的,一个自定义的列表视图有自己的监听器设置方式,并不一定要将其职责分离到子类,个人意见供参考。
适用场景扩展
这个小型开源项目比较好的展示了自定义适配器的框架结构和使用方式,场景可扩展至
1) 需要使用列表并自定义动画特效的场景
2) 需要使用适配器视图并自定义特效的场景
3) 需修改部分源Android框架控件并自定义特效功能的场景
4) 其它由MVC方式构建视图并可自定义扩展C部分的场景
小结
研究此类短小精悍结构清晰的开源项目有利于掌握框架设计技巧及模式识别的相关原则,SlideExpandableListView 这个项目不仅仅用到了上述提到的众多模式和原则,他在如何搭建自己的框架结构方面提供了参考,感谢作者tjerkw的分享精神及专业工作,有兴趣的朋友去点个star吧~。