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

作者:humorousz 时间:2023-05-09 00:45:53 

前言

在一些APP中我们可以看到一些存放标签的容器控件,和我们平时使用的一些布局方式有些不同,它们一般都可以自动适应屏幕的宽度进行布局,根据对自定义控件的一些理解,今天写一个简单的标签容器控件,给大家参考学习。

下面这个是我在手机上截取的一个实例,是在MIUI8系统上截取的

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

这个是我实现的效果图

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

原理介绍

根据对整个控件的效果分析,大致可以将控件分别从以下这几个角度进行分析:

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>

来源:http://blog.csdn.net/humorousz/article/details/51982885

标签:android,标签流
0
投稿

猜你喜欢

  • springboot+thymeleaf 文件上传功能的实现代码

    2023-11-25 05:08:59
  • Android系统服务概览

    2022-02-25 06:31:34
  • Spring中Bean的加载与SpringBoot的初始化流程详解

    2022-12-18 05:02:18
  • Android手机卫士之设置密码对话框

    2021-08-03 07:24:27
  • java导出csv格式文件的方法

    2022-07-24 20:26:36
  • 简单学习Java抽象类要点及实例

    2021-10-11 09:08:12
  • BeanDefinition基础信息讲解

    2022-03-23 23:48:37
  • 解决java 查看JDK中底层源码的实现方法

    2022-03-13 20:40:14
  • Java import导入及访问控制权限修饰符原理解析

    2023-08-17 21:42:59
  • MyBatis传入集合 list 数组 map参数的写法

    2022-12-02 20:21:45
  • 如何使用MybatisPlus快速进行增删改查详解

    2023-11-03 06:58:13
  • 浅谈Java中hashCode的正确求值方法

    2021-11-24 11:40:53
  • JAVA Spring中让人头痛的JAVA大事务问题要如何解决你知道吗

    2023-01-14 04:58:04
  • Java基于Runtime调用外部程序出现阻塞的解决方法

    2023-11-09 04:24:23
  • Android底部菜单栏(RadioGroup+Fragment)美化

    2023-01-06 05:09:56
  • 使用 CliWrap 让C#中的命令行交互(推荐)

    2023-04-19 01:31:51
  • SpringBoot整合RabbitMQ实现六种工作模式的示例

    2021-10-17 06:21:08
  • Java中的Unsafe在安全领域的使用总结和复现(实例详解)

    2023-07-30 10:58:42
  • Java聊天室之实现获取Socket功能

    2023-09-19 03:57:10
  • Java ArrayList扩容问题实例详解

    2022-05-08 08:57:57
  • asp之家 软件编程 m.aspxhome.com