欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-03-06 09:35:19
前言 在一些app中我们可以看到一些存放标签的容器控件,和我们平时使用的一些布局方式有些不同,它们一般都可以自动适应屏幕的宽度进行布局,根据对自定义控件的一些理解,今天写...

前言

在一些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>

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