Android中标签容器控件的实例详解
前言
在一些app中我们可以看到一些存放标签的容器控件,和我们平时使用的一些布局方式有些不同,它们一般都可以自动适应屏幕的宽度进行布局,根据对自定义控件的一些理解,今天写一个简单的标签容器控件,给大家参考学习。
下面这个是我在手机上截取的一个实例,是在miui8系统上截取的
这个是我实现的效果图
原理介绍
根据对整个控件的效果分析,大致可以将控件分别从以下这几个角度进行分析:
1.首先涉及到自定义的viewgroup,因为现有的控件没法满足我们的布局效果,就涉及到要重写onmeasure和onlayout,这里需要注意的问题是自定义view的时候,我们需要考虑到view的padding属性,而在自定义viewgroup中我们需要在onlayout中考虑child控件的margin属性否则子类设置这个属性将会失效。整个view的绘制流程是这样的:
最顶层的viewroot执行performtraversals然后分别开始对各个view进行层级的测量、布局、绘制,整个流程是一层一层进行的,也就是说父视图测量时会调用子视图的测量方法,子视图调孙视图方法,一直测量到叶子节点,performtraversals这个函数翻译过来很直白,执行遍历,就说明了这种层级关系。
2.该控件形式上和listview的形式比较相近,所以在这里我也模仿listview的adapter模式实现了对控件内容的操作,这里对listview的setadapter和adapter的notifydatasetchanged方法做个简单的解释:
在listview调用setadapter后,listview会去注册一个observer对象到这个adapter上,然后当我们在改变设置到adapter上的数据发改变时,我们会调用adapter的notifydatasetchanged方法,这个方法就会通知所有监听了该adapter数据改变时的observer对象,这就是典型的监听者模式,这时由于listview中的内部成员对象监听了该事件,就可以知道数据源发生了改变,我们需要对真个控件重新进行绘制了,下面来一些相关的源码。
adapter的notifydatasetchanged
public void notifydatasetchanged() { mdatasetobservable.notifychanged(); }
listview的setadapter方法
@override public void setadapter(listadapter adapter) { /** *每次设置新的适配的时候,如果现在有的话会做一个解除监听的操作 */ if (madapter != null && mdatasetobserver != null) { madapter.unregisterdatasetobserver(mdatasetobserver); } resetlist(); mrecycler.clear(); /** 省略部分代码..... */ if (madapter != null) { mareallitemsselectable = madapter.areallitemsenabled(); molditemcount = mitemcount; mitemcount = madapter.getcount(); checkfocus(); /** *在这里对adapter设置了监听, *使用的是adapterdatasetobserver类的对象,该对象定义在listview的父类adapterview中 */ mdatasetobserver = new adapterdatasetobserver(); madapter.registerdatasetobserver(mdatasetobserver); /** 省略 */ } else { /** 省略 */ } requestlayout(); }
adapterview中的内部类adapterdatasetobserver
class adapterdatasetobserver extends datasetobserver { private parcelable minstancestate = null; @override public void onchanged() { /* ***代码略*** */ checkfocus(); requestlayout(); } @override public void oninvalidated() { /* ***代码略*** */ checkfocus(); requestlayout(); } public void clearsavedstate() { minstancestate = null; } }
一段伪代码表示
listview{ observer observer{ onchange(){ change; } } setadapter(adapter adapter){ adapter.register(observer); } } adapter{ list<observer> mobservable; register(observer){ mobservable.add(observer); } notifydatasetchanged(){ for(i-->mobserverable.size()){ mobserverable.get(i).onchange } } }
实现过程
获取viewitem的接口
package humoursz.gridtag.test.adapter; import android.view.view; import java.util.list; /** * created by zhangzhiquan on 2016/7/19. */ public interface gridetagbaseadapter { list<view> getviews(); }
抽象适配器absgridtagsadapter
package humoursz.gridtag.test.adapter; import android.database.datasetobservable; import android.database.datasetobserver; /** * created by zhangzhiquan on 2016/7/19. */ public abstract class absgridtagsadapter implements gridetagbaseadapter { datasetobservable mobservable = new datasetobservable(); public void notification(){ mobservable.notifychanged(); } public void registerobserve(datasetobserver observer){ mobservable.registerobserver(observer); } public void unregisterobserve(datasetobserver observer){ mobservable.unregisterobserver(observer); } }
此效果中的需要的适配器,实现了getview接口,主要是模仿了listview的baseadapter
package humoursz.gridtag.test.adapter; import android.content.context; import android.view.layoutinflater; import android.view.view; import android.widget.textview; import java.util.arraylist; import java.util.list; import humoursz.gridtag.test.r; import humoursz.gridtag.test.util.uiutil; import humoursz.gridtag.test.widget.gridtagview; /** * created by zhangzhiquan on 2016/7/19. */ public class mygridtagadapter extends absgridtagsadapter { private context mcontext; private list<string> mtags; public mygridtagadapter(context context, list<string> tags) { mcontext = context; mtags = tags; } @override public list<view> getviews() { list<view> list = new arraylist<>(); for (int i = 0; i < mtags.size(); i++) { textview tv = (textview) layoutinflater.from(mcontext) .inflate(r.layout.grid_tag_item_text, null); tv.settext(mtags.get(i)); gridtagview.layoutparams lp = new gridtagview .layoutparams(gridtagview.layoutparams.wrap_content ,gridtagview.layoutparams.wrap_content); lp.margin(uiutil.dp2px(mcontext, 5)); tv.setlayoutparams(lp); list.add(tv); } return list; } }
最后是主角gridtagsview控件
package humoursz.gridtag.test.widget; import android.content.context; import android.database.datasetobserver; import android.util.attributeset; import android.util.log; import android.view.view; import android.view.viewgroup; import java.util.list; import humoursz.gridtag.test.adapter.absgridtagsadapter; /** * created by zhangzhiquan on 2016/7/18. */ public class gridtagview extends viewgroup { private int mlines = 1; private int mwidthsize = 0; private absgridtagsadapter madapter; private gtobserver mobserver = new gtobserver(); public gridtagview(context context) { this(context, null); } public gridtagview(context context, attributeset attrs) { this(context, attrs, 0); } public gridtagview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); } public void setadapter(absgridtagsadapter adapter) { if (madapter != null) { madapter.unregisterobserve(mobserver); } madapter = adapter; madapter.registerobserve(mobserver); madapter.notification(); } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { int widthsize = measurespec.getsize(widthmeasurespec); int heightsize = measurespec.getsize(heightmeasurespec); int curwidthsize = 0; int childheight = 0; mlines = 1; for (int i = 0; i < getchildcount(); ++i) { view child = getchildat(i); measurechild(child, widthmeasurespec, heightmeasurespec); curwidthsize += getchildrealwidthsize(child); if (curwidthsize > widthsize) { /** * 计算一共需要多少行,用于计算控件的高度 * 计算方法是,如果当前控件放下后宽度超过 * 容器本身的高度,就放到下一行 */ curwidthsize = getchildrealwidthsize(child); mlines++; } if (childheight == 0) { /** * 在第一次计算时拿到字视图的高度作为计算基础 */ childheight = getchildrealheightsize(child); } } mwidthsize = widthsize; setmeasureddimension(widthsize, childheight == 0 ? heightsize : childheight * mlines); } @override protected void onlayout(boolean changed, int l, int t, int r, int b) { if (getchildcount() == 0) return; int childcount = getchildcount(); layoutparams lp = getchildlayoutparams(getchildat(0)); /** * 初始的左边界在自身的padding left和child的margin后 * 初始的上边界原理相同 */ int left = getpaddingleft() + lp.leftmargin; int top = getpaddingtop() + lp.topmargin; int curleft = left; for (int i = 0; i < childcount; ++i) { view child = getchildat(i); int right = curleft + getchildrealwidthsize(child); /** * 计算如果放下当前试图后整个一行到右侧的距离 * 如果超过控件宽那就放到下一行,并且左边距还原,上边距等于下一行的开始 */ if (right > mwidthsize) { top += getchildrealheightsize(child); curleft = left; } child.layout(curleft, top, curleft + child.getmeasuredwidth(), top + child.getmeasuredheight()); /** * 下一个控件的左边开始距离是上一个控件的右边 */ curleft += getchildrealwidthsize(child); } } /** * 获取childview实际占用宽度 * @param child * @return 控件实际占用的宽度,需要算上margin否则margin不生效 */ private int getchildrealwidthsize(view child) { layoutparams lp = getchildlayoutparams(child); int size = child.getmeasuredwidth() + lp.leftmargin + lp.rightmargin; return size; } /** * 获取childview实际占用高度 * @param child * @return 实际占用高度需要考虑上下margin */ private int getchildrealheightsize(view child) { layoutparams lp = getchildlayoutparams(child); int size = child.getmeasuredheight() + lp.topmargin + lp.bottommargin; return size; } /** * 获取layoutparams属性 * @param child * @return */ private layoutparams getchildlayoutparams(view child) { layoutparams lp; if (child.getlayoutparams() instanceof layoutparams) { lp = (layoutparams) child.getlayoutparams(); } else { lp = (layoutparams) generatelayoutparams(child.getlayoutparams()); } return lp; } @override public viewgroup.layoutparams generatelayoutparams(attributeset attr) { return new layoutparams(getcontext(), attr); } @override protected viewgroup.layoutparams generatelayoutparams(viewgroup.layoutparams p) { return new layoutparams(p); } public static class layoutparams extends marginlayoutparams { public layoutparams(context c, attributeset attrs) { super(c, attrs); } public layoutparams(int width, int height) { super(width, height); } public layoutparams(marginlayoutparams source) { super(source); } public layoutparams(viewgroup.layoutparams source) { super(source); } public void marginleft(int left) { this.leftmargin = left; } public void marginright(int r) { this.rightmargin = r; } public void margintop(int t) { this.topmargin = t; } public void marginbottom(int b) { this.bottommargin = b; } public void margin(int m){ this.leftmargin = m; this.rightmargin = m; this.topmargin = m; this.bottommargin = m; } } private class gtobserver extends datasetobserver { @override public void onchanged() { removeallviews(); list<view> list = madapter.getviews(); for (int i = 0; i < list.size(); i++) { addview(list.get(i)); } } @override public void oninvalidated() { log.d("mrz","fd"); } } }
mainactivity
package humoursz.gridtag.test; import android.support.v7.app.appcompatactivity; import android.os.bundle; import android.view.view; import java.util.list; import humoursz.gridtag.test.adapter.mygridtagadapter; import humoursz.gridtag.test.util.listutil; import humoursz.gridtag.test.widget.gridtagview; public class mainactivity extends appcompatactivity { mygridtagadapter adapter; gridtagview mgridtag; list<string> mlist; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); mgridtag = (gridtagview)findviewbyid(r.id.grid_tags); mlist = listutil.getgridtagslist(20); adapter = new mygridtagadapter(this,mlist); mgridtag.setadapter(adapter); } public void onclick(view v){ mlist.removeall(mlist); mlist.addall(listutil.getgridtagslist(20)); adapter.notification(); } }
xml 文件
<?xml version="1.0" encoding="utf-8"?> <relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="humoursz.gridtag.test.mainactivity"> <humoursz.gridtag.test.widget.gridtagview android:id="@+id/grid_tags" android:layout_width="match_parent" android:layout_height="wrap_content"> </humoursz.gridtag.test.widget.gridtagview> <button android:layout_centerinparent="true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onclick="onclick" android:text="换一批"/> </relativelayout>
以上就是android中标签容器控件的全部实现过程,这样一个简单的控件就写好了,主要需要注意measure和layout否则很多效果都会失效,安卓中的linearlayout之类的控件实际实现起来要复杂的很多,因为支持的属性实在的太多了,多动手实践可以帮助理解,希望本文能帮助到在android开发中的大家。
上一篇: java 算法之希尔排序详解及实现代码