首页 > 系统 > Android > 正文

Android中标签容器控件的实例详解

2019-12-12 05:54:53
字体:
来源:转载
供稿:网友

前言

在一些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中标签容器控件的全部实现过程,这样一个简单的控件就写好了,主要需要注意measurelayout否则很多效果都会失效,安卓中的LinearLayout之类的控件实际实现起来要复杂的很多,因为支持的属性实在的太多了,多动手实践可以帮助理解,希望本文能帮助到在Android开发中的大家。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表