从自定义TagLayout看自定义布局的一般步骤[手动加精]

2024-03-27 01:18

本文主要是介绍从自定义TagLayout看自定义布局的一般步骤[手动加精],希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

从自定义TagLayout看自定义布局的一般步骤[手动加精]

我们常用的布局有LinearLayout,FrameLayout,RelativeLayout,大多数情况下都能满足我们的需求,但是也有很多情况下这些布局不能满足我们的需求,无论我们怎么嵌套布局都没法实现我们想要的效果,这时候我们就需要用到自定义布局啦。如果你正准备学习自定义布局,或者你想彻底了解自定义布局的一般步骤,那么这篇文章一定很适合你。

预览

TagLayout预览图

TagLayout预览图2

我已经将它做成了gradle的依赖,你可以方便的引入android studio使用。
https://github.com/onlynight/TagLayout

概述

一般的,我们自定义布局的原则就是继承现有的布局,这样可以少写很多不必要的代码,但是现有的布局如果不能满足需求我们就要继承ViewGroup实现其中的所有流程了。下面我们先说明几个概念:

1. ViewGroup和View的关系与区别

ViewGroup继承自View但是它的和View的侧重点不同,View侧重的是onDraw绘制的内容,而ViewGroup侧重的是对其内部的子view的layout,也就是布局过程(onLayout)。当然ViewGroup同样也可以重写onDraw函数在其中绘制内容,但是一般的我们不会这么做,绘制特殊的图形一般采用自定义View而不是自定义ViewGroup。

2. ViewGroup、View、LayoutParams的关系

  • ViewGroup继承自View,所以ViewGroup实质上也是View
  • ViewGroup中可以包含多个View或者ViewGroup,从逻辑上来说他们ViewGroup和View共同构成了视图树,ViewGroup是父节点,View是子节点。View中不能包含View。
  • 有了布局就多出了一个概念,就是View在ViewGroup中的位置,andorid中为这个位置信息添加了个对象叫做LayoutParams。LayoutParams描述了ViewGroup中的子控件View的大小等属性。
  • LayoutParams针对不同的布局类型又有不同的子类,例如LinearLayout.LayoutParams中除了描述View的大小外还有一个weight属性,layout_weight相信大家都用过啦这里就不再赘述。自定义的ViewGroup如果需要ViewGroup.LayoutParams以外的属性,你需要重新构造一个你需要的布局参数,这个ViewGroup中所有的View都被设置上这个类型的布局参数。

3. 自定义ViewGroup的一般步骤

  1. onMeasure中测量自己以及子控件View的尺寸。
  2. onLayout中设置子控件View的位置。
  3. onDraw 如果又需要你可以在onDraw中绘制内容,但是自己布局我们一般都不会用到这个步骤。

4. MeasureSpec

MeasureSpec是android中一个特殊的值,我们看到onMeasure的参数即是MeasureSpec:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

我哦们将其打印出来后发现不是想象中的像素值,而是一个奇怪的数字,实际上这一个数值代表了两个属性,这个整形的高两位是描述的测量模式,低30位描述的才是控件尺寸的像素值。我们从这个函数中就可以看出端倪了:

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;@MeasureSpecMode
public static int getMode(int measureSpec) {//noinspection ResourceTypereturn (measureSpec & MODE_MASK);
}

MeasureSpec#getMode函数将measureSpec的后30位置为0,得到其高2位的值。

自定义TagLayout详细实现

1. 功能描述

我们想要实现的效果就是View横向排列,当宽度不够时向下换行;同时支持Tag item的margin属性;支持单选多选和不能选择三种选择模式。这就是我们想要的TagLayout的基本功能。

2. 选择继承的布局类

综合以上功能我们发现基本的布局已经不能满足我们的需求,所以需要继承ViewGroup实现以上功能。

3. 设置布局参数

功能中提到要实现布局的margin参数,要使用这个参数首先布局得支持这个参数,所有我们继承ViewGroup后要重写generateLayoutParams方法,并将其返回值设置位MarginLayoutParams:

/*** set the item layout parmas to {@link android.view.ViewGroup.MarginLayoutParams}.** @param attrs* @return*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {return new MarginLayoutParams(getContext(), attrs);
}

这样当调用ViewGroup的addView方法的时候就会通过这个方法获取布局参数,并给每一个子View设置MarginLayoutParams。在看MarginLayoutParams的构造参数,第一个为Context就不多说了,再看第二个参数AttributeSet,是不是觉得似曾相识?没错这个和View的构造函数中的AttributeSet是一样的,这个参数是LayoutInflator通过解析xml布局文件生成的控件属性参数,其中包含了layout_width,layout_height,layout_margin之类的系统为我们定义的属性,还有我们自己定义的属性。这样就为我们使用layout_margin参数做好了准备。

4. 向ViewGroup中添加控件View

从预览图的效果我们知道TagLayout中可以添加多个Tag View,但是每个Tag View都是相同的只是部分属性不同,这时候我们就要用到一种很方便的设计模式—适配器模式。相信大家也很常用ListView或者RecyclerView他们控件和数据绑定的方式就是适配器。这里我们也使用适配器,这样tag view可以灵活添加,tag的数量通过适配器就能轻松搞定。

我们来看一下setAdapter函数中我们做了些什么:

private BaseAdapter mAdapter;public void setAdapter(BaseAdapter adapter) {this.mAdapter = adapter;removeAllViews();if (mAdapter == null) {return;}if (!registeredDataObserver) {registeredDataObserver = true;mAdapter.registerDataSetObserver(dataSetObserver);}for (int i = 0; i < this.mAdapter.getCount(); i++) {View child = this.mAdapter.getView(i, null, this);addView(child);}setSelectMode(mMaxSelectCount);
}

注意这里我们使用Adapter#getView方法时传入第二个参数为null,我们知道ListView一种优化方式是使用ViewHolder,然后通过convertView的复用规则我们可以不用每次都inflat布局,这里我们并没有像ListView一样复用convertView,因为多数情况下convertView都不会被复用,所有的tag都是同时显示再屏幕上的,所以这里出入null即可。

实际上我们在setAdapter函数中最重要的有两点,第一将Adapter中的View添加到ViewGroup中去,第二为Adapter中的控件设置点击事件。一般的Adapter中的item点击事件都交由控件自身处理而不是由ViewGroup处理,这里我们使组控件为了监听他们的点击所有点击事件需要由ViewGroup接收并响应。需要注意的是我们不能在onMeasure,onLayout和onDraw中设置回掉事件,因为这些方法会被多次调用,这样设置回掉事件很耗时同时也有可能引发内存泄露,所以设置回调事件一般在初始化的时候进行。下面我们来看一下tag item的点击响应事件的处理:

5. 处理Tag选择模式

/*** set the select mode.** @param selectModeOrMaxSelectCount {@link TagLayout.SELECT_MODE_ALL} can select all.</p>*                                   {@link TagLayout.SELECT_MODE_NONE} can select none.</p>*                                   {@link TagLayout.SELECT_MODE_SINGLE} can select one.</p>*                                   or you can set the max select count you want.</p>*/
public void setSelectMode(int selectModeOrMaxSelectCount) {this.mMaxSelectCount = selectModeOrMaxSelectCount;if (mMaxSelectCount == 0) {for (int i = 0; i < this.mAdapter.getCount(); i++) {getChildAt(i).setOnClickListener(null);}clearSelect();} else {for (int i = 0; i < this.mAdapter.getCount(); i++) {final int index = i;final View child = getChildAt(i);child.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {//处理只单选的模式if (mMaxSelectCount == 1) {clearSelect();child.setSelected(!child.isSelected());} // 处理多选模式else if (mMaxSelectCount > 1) {List<Integer> selected = getSelected();// 超过最大选择数处理方式if (selected.size() >= mMaxSelectCount) {if (child.isSelected()) {child.setSelected(false);if (onTagItemSelectedListener != null) {onTagItemSelectedListener.onSelected(false, index, selected);}} else {if (onTagItemSelectedListener != null) {onTagItemSelectedListener.onCanNotSelectMore(false, index, selected);}}} // 正常多选处理方式else {child.setSelected(!child.isSelected());if (onTagItemSelectedListener != null) {onTagItemSelectedListener.onSelected(child.isSelected(), index, selected);}}} // 处理全选模式else if (mMaxSelectCount == -1) {child.setSelected(!child.isSelected());if (onTagItemSelectedListener != null) {List<Integer> selected = getSelected();onTagItemSelectedListener.onSelected(child.isSelected(), index, selected);}}}});}}
}

看这上面写的很复杂,实际上就是处理了几种选择模式:

  1. 不能选择的模式SELECT_TYPE_NONE

    禁止点击模式处理最简单,不设置点击回调即可。

  2. 单选模式SELECT_TYPE_SINGLE

    单选模式处理时,先清除所有被选择的tag然后把当前tag设为被选中即可。

  3. 全选模式SELECT_TYPE_ALL

    全选模式,就是可以选中所有tag,不做任何限制。

  4. 多选模式 大于1的任意数字

    多选模式处理时候比较复杂,如果没达到可选择的上限就继续选择,如果到达可选择的上限则回调提示用户不能再选。

基本处理完以上一种模式,就是以上这个复杂的代码了。

6. 为Adapter设置DataObserver

我们知道使用Adapter的时候需要更新数据时直接调用Adapter#notifyDataSetChange方法即可更新数据。实际上这是个观察者模式,Adapter是被观察对象,我们需要创建观察者监听Adapter的数据更新通知:

private boolean registeredDataObserver = false;private DataSetObserver dataSetObserver = new DataSetObserver() {@Overridepublic void onChanged() {setAdapter(mAdapter);}
};public void setAdapter(BaseAdapter adapter) {this.mAdapter = adapter;removeAllViews();if (mAdapter == null) {return;}if (!registeredDataObserver) {registeredDataObserver = true;mAdapter.registerDataSetObserver(dataSetObserver);}for (int i = 0; i < this.mAdapter.getCount(); i++) {View child = this.mAdapter.getView(i, null, this);addView(child);}setSelectMode(mMaxSelectCount);
}

我们这里简单处理一下,当数据更新的时候重新设置下适配器完成数据更新。

7. 测量控件onMeasure

下面就要说自定义ViewGroup的重要步骤之一了—-测量过程onMeasure。自定义View中onMeasure过程主要是测量自己的大小,而ViewGroup中除了测量自己的大小外,还要测量子控件的大小,实际上这两个过程应该反过来,首先测量子控件测尺寸,然后通过子控件的尺寸计算自身测尺寸,因为ViewGroup的layout_width/layout_height如果设置为wrap_content的话就i需要根据子控件的尺寸计算自身的大小,如果不这样那么ViewGroup默认的会将wrap_content设置为match_parent一样的效果。下面我们先来看一下onMeasure方法中的代码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 解析MeasureSpecint widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);//测量子控件measureChildren(widthMeasureSpec, heightMeasureSpec);int childWidth = 0;int childHeight = 0;int lines = 1;int lineWidth = 0;mMaxWidth = 0;mMaxHeight = 0;int eachLineHeight = 0;/*** 根据我们的布局规则计算子控件需要占父控件的大小,* 这里我们先不管ViewGroup的MeasureSpec模式,我们* 先根据布局规则计算高度,宽度根据ViewGroup的的* widthMeasureSpec 确定宽度,这个就是ViewGroup的最大宽度。*/for (int i = 0; i < getChildCount(); i++) {MarginLayoutParams params = (MarginLayoutParams)getChildAt(i).getLayoutParams();// calculate child total size include margin.childWidth = getChildAt(i).getMeasuredWidth() +params.leftMargin + params.rightMargin;childHeight = getChildAt(i).getMeasuredHeight() +params.topMargin + params.bottomMargin;// 这里我们不对每一行高度单独计算,假定每一行的高度都是一样的。eachLineHeight = childHeight;lineWidth += childWidth;if (lineWidth >= widthSpecSize) {lines++;lineWidth = childWidth;mMaxWidth = widthSpecSize;}}mMaxHeight = eachLineHeight * lines;// 更具子控件的需要的尺寸计算自身的大小setSelfSize(widthSpecMode, widthSpecSize, heightSpecMode, heightSpecSize);
}

这个函数整体来说很简洁,做了以下几件事:

  1. 测量所有子控件的大小 measureChildren

    该方法会遍历所有子控件计算每一个控件的尺寸。

  2. 根据子控件的尺寸计算子控件所需要的最大空间

    根据我们的布局规则,tag标签横向排列占满一行想下换行,统计计算时应考虑子控件的layout_margin参数,就是以上for循环中的计算步骤,就算出一个最大宽度和最大高度。计算最大宽度的原因是tag标签有可能不能占满一行,这时候ViewGroup就要重新设置下width。

  3. 根据子控件所需要的尺寸计算自身的尺寸 setSelfSize

    setSelfSize函数中会根据ViewGroup的测量模式具体设置ViewGroup的尺寸,下面我们来详细看下这个函数。

/*** set self size.** @param widthSpecMode  width measure spec mode* @param widthSpecSize  width measure spec size* @param heightSpecMode height measure spec mode* @param heightSpecSize height measure spec size*/
private void setSelfSize(int widthSpecMode, int widthSpecSize,int heightSpecMode, int heightSpecSize) {int finalWidth = 0;int finalHeight = 0;switch (widthSpecMode) {case MeasureSpec.EXACTLY:case MeasureSpec.UNSPECIFIED:finalWidth = widthSpecSize;break;case MeasureSpec.AT_MOST:finalWidth = mMaxWidth;break;}switch (heightSpecMode) {case MeasureSpec.EXACTLY:case MeasureSpec.UNSPECIFIED:finalHeight = heightSpecSize;break;case MeasureSpec.AT_MOST:finalHeight = mMaxHeight;break;}setMeasuredDimension(finalWidth, finalHeight);
}

MeasureSpec的模式之前我们已经有介绍过了,这里我们针对UNSPECIFIED不限制模式,EXACTLY知道具体宽度模式,设置为VewGroup的父控件传过来的尺寸,这种模式下表明父控件已经知道了具体的尺寸我们直接设置即可。另外一种AT_MOST就要用到我们之前计算的子控件所需的最大尺寸,AT_MOST表示的是子控件需要的最大空间,我们将上一步的计算值直接赋值即可。最后的关键函数是setMeasuredDimension设置ViewGroup自身的测量值,这时候不能使用setWidth/setHeight来设置宽高,我们还在测量过程中这个方法是无效的,需要布局完成后使用该方法才能有效设置尺寸。

8. 布局控件onLayout

上一步中我们测量测所需的尺寸,接下来就是布局了。布局说简单了就是设置ViewGroup的子控件子ViewGroup中的位置,下面先来看下onLayout函数:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {int width = getWidth();int lines = 0;int lineWidth = 0;int lineHeight = 0;// 为每一个子控件设置位置for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);MarginLayoutParams layoutParams =(MarginLayoutParams) child.getLayoutParams();// 计算每一个子控件的位置lineWidth += child.getMeasuredWidth() + layoutParams.leftMargin+ layoutParams.rightMargin;if (lineWidth >= width) {lines++;lineWidth = child.getMeasuredWidth() + layoutParams.leftMargin+ layoutParams.rightMargin;lineHeight = lines * (child.getMeasuredHeight() +layoutParams.topMargin + layoutParams.bottomMargin);}// 布局子控件child.layout(lineWidth - child.getMeasuredWidth() - layoutParams.rightMargin,lineHeight + layoutParams.topMargin,lineWidth - layoutParams.rightMargin,lineHeight + layoutParams.topMargin + child.getMeasuredHeight());}
}

需要注意的是我们设置子控件的位置是子控件相对于ViewGroup的位置,也就是说父控件的左上角的坐标是(0,0),右下角的坐标是(viewGroupWidth,viewGroupHeight)。

以上代码也很简单,View#layout函数的四个参数分别是lefttoprightbottom我们根据子控件自身的尺寸就算right和bottom,通过布局规则计算left和top。需要注意的是layout_margin参数不能包含在其中,否则会将控件放大,我们需要跳过layout_margin响应的空间来计算上下左右的坐标。

9. 完整代码

最后我们来看一张全家福,总览以下TagLayout的代码有个更直观的认识:

import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;import java.util.LinkedList;
import java.util.List;/*** Created by lion on 2016/11/22.* Tag item layout.*/public class TagLayout extends ViewGroup {/*** the select mode all, you can select all tag.*/public static final int SELECT_MODE_ALL = -1;/*** the select mode none, you can't select any item.*/public static final int SELECT_MODE_NONE = 0;/*** the select mode single, you can only select one item.*/public static final int SELECT_MODE_SINGLE = 1;/*** save the {@link TagLayout} max width.*/private int mMaxWidth;/*** save the {@link TagLayout} max width.*/private int mMaxHeight;/*** save the max select count.</p>* it is also select mode.* {@link SELECT_MODE_ALL},{@link SELECT_MODE_NONE},{@link SELECT_MODE_SINGLE}*/private int mMaxSelectCount = 1;private BaseAdapter mAdapter;private OnTagItemSelectedListener onTagItemSelectedListener;private boolean registeredDataObserver = false;private DataSetObserver dataSetObserver = new DataSetObserver() {@Overridepublic void onChanged() {setAdapter(mAdapter);}};public void setOnTagItemSelectedListener(OnTagItemSelectedListener onTagItemSelectedListener) {this.onTagItemSelectedListener = onTagItemSelectedListener;}public TagLayout(Context context) {super(context);init(null);}public TagLayout(Context context, AttributeSet attrs) {super(context, attrs);TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TagLayout);init(array);}public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.TagLayout, defStyleAttr, 0);init(array);}@RequiresApi(21)public TagLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.TagLayout, defStyleAttr, defStyleRes);init(array);}private void init(TypedArray array) {if (array != null) {mMaxSelectCount = array.getInteger(R.styleable.TagLayout_maxSelectCount, 1);}}public int getMaxSelectCount() {return mMaxSelectCount;}public void setAdapter(BaseAdapter adapter) {this.mAdapter = adapter;removeAllViews();if (mAdapter == null) {return;}if (!registeredDataObserver) {registeredDataObserver = true;mAdapter.registerDataSetObserver(dataSetObserver);}for (int i = 0; i < this.mAdapter.getCount(); i++) {View child = this.mAdapter.getView(i, null, this);addView(child);}setSelectMode(mMaxSelectCount);}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int width = getWidth();int lines = 0;int lineWidth = 0;int lineHeight = 0;for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);MarginLayoutParams layoutParams =(MarginLayoutParams) child.getLayoutParams();lineWidth += child.getMeasuredWidth() + layoutParams.leftMargin+ layoutParams.rightMargin;if (lineWidth >= width) {lines++;lineWidth = child.getMeasuredWidth() + layoutParams.leftMargin+ layoutParams.rightMargin;lineHeight = lines * (child.getMeasuredHeight() +layoutParams.topMargin + layoutParams.bottomMargin);}child.layout(lineWidth - child.getMeasuredWidth() - layoutParams.rightMargin,lineHeight + layoutParams.topMargin,lineWidth - layoutParams.rightMargin,lineHeight + layoutParams.topMargin + child.getMeasuredHeight());}}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);measureChildren(widthMeasureSpec, heightMeasureSpec);int childWidth = 0;int childHeight = 0;int lines = 1;int lineWidth = 0;mMaxWidth = 0;mMaxHeight = 0;int eachLineHeight = 0;for (int i = 0; i < getChildCount(); i++) {MarginLayoutParams params = (MarginLayoutParams)getChildAt(i).getLayoutParams();// calculate child total size include margin.childWidth = getChildAt(i).getMeasuredWidth() +params.leftMargin + params.rightMargin;childHeight = getChildAt(i).getMeasuredHeight() +params.topMargin + params.bottomMargin;eachLineHeight = childHeight;lineWidth += childWidth;if (lineWidth >= widthSpecSize) {lines++;lineWidth = childWidth;mMaxWidth = widthSpecSize;}}mMaxHeight = eachLineHeight * lines;setSelfSize(widthSpecMode, widthSpecSize, heightSpecMode, heightSpecSize);}/*** set self size.** @param widthSpecMode  width measure spec mode* @param widthSpecSize  width measure spec size* @param heightSpecMode height measure spec mode* @param heightSpecSize height measure spec size*/private void setSelfSize(int widthSpecMode, int widthSpecSize,int heightSpecMode, int heightSpecSize) {int finalWidth = 0;int finalHeight = 0;switch (widthSpecMode) {case MeasureSpec.EXACTLY:case MeasureSpec.UNSPECIFIED:finalWidth = widthSpecSize;break;case MeasureSpec.AT_MOST:finalWidth = mMaxWidth;break;}switch (heightSpecMode) {case MeasureSpec.EXACTLY:case MeasureSpec.UNSPECIFIED:finalHeight = heightSpecSize;break;case MeasureSpec.AT_MOST:finalHeight = mMaxHeight;break;}setMeasuredDimension(finalWidth, finalHeight);}/*** set the item layout parmas to {@link android.view.ViewGroup.MarginLayoutParams}.** @param attrs* @return*/@Overridepublic LayoutParams generateLayoutParams(AttributeSet attrs) {return new MarginLayoutParams(getContext(), attrs);}/*** clear all the selected item.*/public void clearSelect() {for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);child.setSelected(false);}}/*** get selected item id.** @return a list of selected item's id.*/public List<Integer> getSelected() {List<Integer> selected = new LinkedList<>();for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);if (child.isSelected()) {selected.add(i);}}return selected;}/*** set item state to selected.** @param items the items you want to be set select.*/public void setSelect(List<Integer> items) {if (items == null) {return;}clearSelect();for (int i = 0; i < items.size(); i++) {if (i + 1 <= mMaxSelectCount) {if (items.get(i) + 1 < getChildCount()) {View child = getChildAt(items.get(i));child.setSelected(true);}}}}/*** set the select mode.** @param selectModeOrMaxSelectCount {@link TagLayout.SELECT_MODE_ALL} can select all.</p>*                                   {@link TagLayout.SELECT_MODE_NONE} can select none.</p>*                                   {@link TagLayout.SELECT_MODE_SINGLE} can select one.</p>*                                   or you can set the max select count you want.</p>*/public void setSelectMode(int selectModeOrMaxSelectCount) {this.mMaxSelectCount = selectModeOrMaxSelectCount;if (mMaxSelectCount == 0) {for (int i = 0; i < this.mAdapter.getCount(); i++) {getChildAt(i).setOnClickListener(null);}clearSelect();} else {for (int i = 0; i < this.mAdapter.getCount(); i++) {final int index = i;final View child = getChildAt(i);child.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {if (mMaxSelectCount == 1) {clearSelect();child.setSelected(!child.isSelected());} else if (mMaxSelectCount > 1) {List<Integer> selected = getSelected();if (selected.size() >= mMaxSelectCount) {if (child.isSelected()) {child.setSelected(false);if (onTagItemSelectedListener != null) {onTagItemSelectedListener.onSelected(false, index, selected);}} else {if (onTagItemSelectedListener != null) {onTagItemSelectedListener.onCanNotSelectMore(false, index, selected);}}} else {child.setSelected(!child.isSelected());if (onTagItemSelectedListener != null) {onTagItemSelectedListener.onSelected(child.isSelected(), index, selected);}}} else if (mMaxSelectCount == -1) {child.setSelected(!child.isSelected());if (onTagItemSelectedListener != null) {List<Integer> selected = getSelected();onTagItemSelectedListener.onSelected(child.isSelected(), index, selected);}}}});}}}/*** set the tag layout max select count.</p>* this method is the same as {@link TagLayout#setSelectMode(int)}** @param maxSelectCount it is bigger than 0,</p>*                       the -1 is means you can select all tag item.</p>*                       {@link TagLayout.SELECT_MODE_ALL} can select all.</p>*                       {@link TagLayout.SELECT_MODE_NONE} can select none.</p>*                       {@link TagLayout.SELECT_MODE_SINGLE} can select one.</p>*/public void setMaxSelectCount(int maxSelectCount) {setSelectMode(maxSelectCount);}public interface OnTagItemSelectedListener {void onSelected(boolean selected, int currentSelected, List<Integer> allSelected);void onCanNotSelectMore(boolean selected, int currentSelected, List<Integer> allSelected);}}

以上就是布局的全部过程啦,这里我们自定义TagLayout就全部完成了下面来归纳下自定义ViewGroup的一般步骤。

自定义ViewGroup的一般步骤

1. 首先确定ViewGroup的子控件的来源

ViewGroup的来源有很多种,怎么使用取决于是当时的使用场景,一般包含以下几种:

  1. 通过xml布局文件中添加
  2. 通过Adapter适配器模式添加
  3. 通过代码动态添加

了解来源的原因是我们要知道怎样合理的初始化控件。例如TagLayout需要添加多个标签,而这些标签有可能是网络来源也有可能是本地来源,个数不固定,所以适配器模式添加非常合适,只要是有规律的数据都可以使用Adapter模式。

2. 测量控件尺寸onMeasure

ViewGroup测量自身尺寸之前需要先测量子控件的尺寸这样才能很好的适配wrap_content参数,测量完子控件再对自身进行测量就完成了测量过程。

3. 布局子控件onLayout

测量完成后就需要根据布局规则对所有的子控件进行布局过程,为了使得ViewGroup更加的通用一般的我们会考虑layout_margin参数。

4. 绘制ViewGroup onDraw

一般的ViewGroup中我们强调的是布局过程,一般不会在ViewGroup中绘制一些特殊的图形,重绘制一般会在自定义View中比较常用。

以上就是自己一ViewGroup的一般步骤,希望这篇文章能够帮助到想要获得跟多知识的你,如果你喜欢这篇文章你可以在文章末尾给我评论支持我继续更新文章。

这篇关于从自定义TagLayout看自定义布局的一般步骤[手动加精]的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/850488

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

arduino ide安装详细步骤

​ 大家好,我是程序员小羊! 前言: Arduino IDE 是一个专为编程 Arduino 微控制器设计的集成开发环境,使用起来非常方便。下面将介绍如何在不同平台上安装 Arduino IDE 的详细步骤,包括 Windows、Mac 和 Linux 系统。 一、在 Windows 上安装 Arduino IDE 1. 下载 Arduino IDE 打开 Arduino 官网

Oracle type (自定义类型的使用)

oracle - type   type定义: oracle中自定义数据类型 oracle中有基本的数据类型,如number,varchar2,date,numeric,float....但有时候我们需要特殊的格式, 如将name定义为(firstname,lastname)的形式,我们想把这个作为一个表的一列看待,这时候就要我们自己定义一个数据类型 格式 :create or repla

lvgl8.3.6 控件垂直布局 label控件在image控件的下方显示

在使用 LVGL 8.3.6 创建一个垂直布局,其中 label 控件位于 image 控件下方,你可以使用 lv_obj_set_flex_flow 来设置布局为垂直,并确保 label 控件在 image 控件后添加。这里是如何步骤性地实现它的一个基本示例: 创建父容器:首先创建一个容器对象,该对象将作为布局的基础。设置容器为垂直布局:使用 lv_obj_set_flex_flow 设置容器

HTML5自定义属性对象Dataset

原文转自HTML5自定义属性对象Dataset简介 一、html5 自定义属性介绍 之前翻译的“你必须知道的28个HTML5特征、窍门和技术”一文中对于HTML5中自定义合法属性data-已经做过些介绍,就是在HTML5中我们可以使用data-前缀设置我们需要的自定义属性,来进行一些数据的存放,例如我们要在一个文字按钮上存放相对应的id: <a href="javascript:" d

一步一步将PlantUML类图导出为自定义格式的XMI文件

一步一步将PlantUML类图导出为自定义格式的XMI文件 说明: 首次发表日期:2024-09-08PlantUML官网: https://plantuml.com/zh/PlantUML命令行文档: https://plantuml.com/zh/command-line#6a26f548831e6a8cPlantUML XMI文档: https://plantuml.com/zh/xmi