开篇
前面已经介绍了一系列的 View 的自定义,后面的几篇会找几个实际的例子来动手练一下,今天就先瞅瞅 标签流容器
先给出效果图:
image.png这个自定义 View 是非常简单的,只要你把前面的 view 的工作原理一、二、三 大致看一遍就可以很轻松的撸出来
自定义 View 的种类
自定义 View 的分类标准不唯一,大致可以分为 4 类
- 1、继承 View 重写 onDraw 方法
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写 onDraw 方法。采用这种方式需要自己支持 wrap_content,并且 padding 也需要自己处理。
- 2、继承 ViewGroup 派生特殊的 Layout
这种方法主要用于实现自定义的布局,即除了 LinearLayout、RelativeLayout、FrameLayout 这几种系统的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
- 3、继承特定的 View (比如 TextView)
这种方法比较常见,一般是用于扩展某种已有的 View 的功能,比如 TextView,这种方法比较容易实现。这种方法不需要自己支持 wrap_content 和 padding 等。
- 4、继承特定的 ViewGroup (比如 LinearLayout)
这种方法也比较常见,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理 ViewGroup 的测量和布局这两个过程。需要注意这种方法和方法 2 的区别,一般来说 方法 2 能实现的效果方法 4 也都能实现,两者的主要差别在于 方法 2 更接近 View 的底层。
自定义 View 常见注意事项
这里我们会列举一些自定义 View 过程中的一些注意事项,这些问题如果处理不好,有些会影响 View 的正常使用,而有些会导致内存泄漏等。
- 1、让 View 支持 wrap_content
这是因为直接继承 View 或者 ViewGroup 的控件,如果不在 onMeasure 中对 wrap_content 做特殊处理,那么外界在布局中使用 wrap_content 时就无法达到预期的效果,这个就不在这里细说了,有兴趣的可以去看一下我 CSDN 上的简单介绍 Android——View的工作原理(一)
- 2、如果有必要,让你的 View 支持 padding
这是因为直接继承 View 的控件,如果不在 draw 方法中处理 padding,那么 padding 属性是无法起作用的。另外,直接继承自 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考虑 padding 和 子元素的 margin 对其造成的影响,不然将导致 padding 和 子元素的 margin 失效。
- 3、尽量不要在 View 中使用 Handler,没必要
这是因为 View 内部本身就提供了 post 系列方法,完全可以替代 Handler 的作用,当然除非你很明确地要使用 Handler 来发送消息。
- 4、View 中如果有线程或动画,需要及时停止,参考 View#onDetachedFromWindow
这一条也很好理解,如果有线程或者动画需要停止时,那么 onDetachedFromWindow 方法是一个很好的时机。当包含此 View 的 Activity 退出或者当前 View 被 remove 时,View 的 onDetachedFromWindow 方法会被调用,和此方法对应的是 onAttachedToWindow 方法,当包含此 View 的 Activity 启动时,View 的 onAttachedToWindow 方法会被调用。同时,当 View 变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
- 5、View 带有滑动嵌套情形时,需要处理号滑动冲突
如果有滑动冲突的话,那么要合适地处理滑动冲突,否则将会严重影响 View 的效果
自定义 标签流容器
onMeasure 方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int usedWidth = 0; //已使用的宽度
int remaining = 0; //剩余可用宽度
int totalHeight = 0; //总高度
int lineHeight = 0; //当前行高
int maxLineHeight = 0; //最大行高
//for 循环遍历 子 view
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
//获取 layoutParams
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
if (widthMode == MeasureSpec.AT_MOST) {
throw new RuntimeException("FlowLayout 的 \"layout_width\" 必须为 \"match_parent\" 或者 精确数值");
} else {
//测量 子 view
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
// 剩余可用 width
remaining = widthSize - usedWidth - getPaddingLeft() - getPaddingRight();
//当剩余空间不足以放下一个新 view 时,换行
if (childView.getMeasuredWidth() > remaining) {
//累加高度,用于作为当前 FlowLayout 的最终高度
totalHeight += maxLineHeight;
//重置
maxLineHeight = 0;
usedWidth = 0;
}
//已使用 width 进行 累加
usedWidth += lp.leftMargin + lp.rightMargin + childView.getMeasuredWidth();
//当前 view 的高度
lineHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//取出每行 view 的最大高度
maxLineHeight = Math.max(lineHeight, maxLineHeight);
}
}
//最终高度,记得加上最后一行的view 的高度
totalHeight += maxLineHeight + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = totalHeight;
}
//去较大的一个作为 FlowLayout 的最终高度
heightSize = Math.max(totalHeight, heightSize);
setMeasuredDimension(widthSize, heightSize);
}
其实就是一个遍历的过程,通过遍历获取子 view 的 layoutParams,然后进行一个模拟排版过程,最终拿到 FlowLayout 的最终高度,并设置
需要注意的地方:
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); 这是一个强制转换过程,因为我们在 addView 时,addView 内部会去获取并创建一个 LayoutParams,而这个 LayoutParams 是需要我们自己自定的
addView 内部实现代码:
...
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams(); //返回默认地LayoutParams类,作为该View的属性值
if (params == null) {//如果不能获取到LayoutParams对象,则抛出异常。
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
public void addView(View child, int width, int height) {
//返回默认地LayoutParams类,作为该View的属性值
final LayoutParams params = generateDefaultLayoutParams();
params.width = width; //重新设置width值
params.height = height; //重新设置height值
addView(child, -1, params); //这儿,我们有指定width、height的大小了。
}
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
public void addView(View child, int index, LayoutParams params) {
...
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate();
addViewInner(child, index, params, false);
}
protected LayoutParams generateDefaultLayoutParams() {
//width 为 WRAP_CONTENT大小 , height 为WRAP_CONTENT
//ViewGroup的子类可以重写该方法,达到其特定要求。
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
if (!checkLayoutParams(params)) { //params对象是否为null
params = generateLayoutParams(params); //如果params对象是为null,重新构造个LayoutParams对象
}
//preventRequestLayout值为false
if (preventRequestLayout) {
child.mLayoutParams = params; //为View的mLayoutParams属性赋值
} else {
child.setLayoutParams(params);//为View的mLayoutParams属性赋值,但会调用requestLayout()请求重新布局
}
//if else 语句会设置View为mLayoutParams属性赋值
...
}
...
}
onLayout 方法
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
int childTop = paddingTop;
int childLeft = paddingLeft;
int childRight = 0;
int childBottom = 0;
// FlowLayout 的 width
final int width = right - left;
//当前 FlowLayout 中 子 View 可使用的最大宽度
int childWidthSpace = width - paddingLeft - paddingRight;
//行高
int lineHeight = 0;
//最大行高
int maxLineHeight = 0;
//已使用 width
int usedWidth = 0;
//总高度
int totalHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
//已使用的 width 计算
usedWidth += lp.leftMargin + lp.rightMargin + childWidth;
//当前 view 的高度
lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
//当剩余空间不足时,换行
if (usedWidth > childWidthSpace) {
totalHeight += maxLineHeight;
//重置 left
childLeft = paddingLeft;
//增加 top 值
childTop = paddingTop + totalHeight;
maxLineHeight = 0;
usedWidth = lp.leftMargin + lp.rightMargin + childWidth;
}
maxLineHeight = Math.max(lineHeight, maxLineHeight);
childLeft += lp.leftMargin;
childTop += lp.topMargin;
childRight = childLeft + childWidth;
childBottom = childTop + childHeight;
childView.layout(childLeft, childTop, childRight, childBottom);
childLeft = childRight + lp.rightMargin;
}
}
代码都非常简单,而且注释也挺全,就不在一步一步详细说了
添加子 View
//添加 子 view
public void setAdapter(BaseAdapter mAdapter) {
this.mAdapter = mAdapter;
if (mAdapter == null || mAdapter.getCount() == 0) {
return;
}
removeAllViews();
for (int i = 0; i < mAdapter.getCount(); i++) {
View view = mAdapter.getView(i, null, null);
;
; Log.e("=========",view.getLayoutParams()+"");
final int position = i;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mListener != null) {
mListener.itemClick(position);
}
}
});
addView(view);
}
//这个 requestLayout 其实没必要现式调用,addView 方法内部其实已经调用了 requestLayout 方法
//requestLayout();
}
其他重写方法 LayoutParams 的创建
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p){
return new MarginLayoutParams(p);
}
具体使用
final List<String> list = new ArrayList<>();
list.add("美妆");
list.add("画板");
list.add("漫画");
list.add("高科技");
list.add("韩国电影");
list.add("高富帅");
list.add("鸿泰安");
list.add("外语");
list.add("财经");
list.add("大叔");
list.add("非主流");
list.add("暴走漫画");
list.add("心理学");
list.add("汉语");
list.add("白富美");
list.add("自定义");
flowLayout.setAdapter(new BaseAdapter() {
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = LayoutInflater.from(FlowLayoutActivity.this).inflate(R.layout.item_tag, parent, false);
TextView textView = view.findViewById(R.id.tv_text);
textView.setText(list.get(position));
return view;
}
});
flowLayout.setItemClickListener(new FlowLayout.TagItemClickListener() {
@Override
public void itemClick(int position) {
Toast.makeText(FlowLayoutActivity.this, list.get(position), Toast.LENGTH_LONG).show();
}
});
给出完整代码:
package com.summary.hecom.custom.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
/**
* Created by hecom on 2018/4/28.
*/
public class FlowLayout extends ViewGroup {
private Context mContext;
private BaseAdapter mAdapter;
private TagItemClickListener mListener;
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int usedWidth = 0; //已使用的宽度
int remaining = 0; //剩余可用宽度
int totalHeight = 0; //总高度
int lineHeight = 0; //当前行高
int maxLineHeight = 0; //最大行高
//for 循环遍历 子 view
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
//获取 layoutParams
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
if (widthMode == MeasureSpec.AT_MOST) {
throw new RuntimeException("FlowLayout 的 \"layout_width\" 必须为 \"match_parent\" 或者 精确数值");
} else {
//测量 子 view
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
// 剩余可用 width
remaining = widthSize - usedWidth - getPaddingLeft() - getPaddingRight();
//当剩余空间不足以放下一个新 view 时,换行
if (childView.getMeasuredWidth() > remaining) {
//累加高度,用于作为当前 FlowLayout 的最终高度
totalHeight += maxLineHeight;
//重置
maxLineHeight = 0;
usedWidth = 0;
}
//已使用 width 进行 累加
usedWidth += lp.leftMargin + lp.rightMargin + childView.getMeasuredWidth();
//当前 view 的高度
lineHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//取出每行 view 的最大高度
maxLineHeight = Math.max(lineHeight, maxLineHeight);
}
}
//最终高度,记得加上最后一行的view 的高度
totalHeight += maxLineHeight + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = totalHeight;
}
//去较大的一个作为 FlowLayout 的最终高度
heightSize = Math.max(totalHeight, heightSize);
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
int childTop = paddingTop;
int childLeft = paddingLeft;
int childRight = 0;
int childBottom = 0;
// FlowLayout 的 width
final int width = right - left;
//当前 FlowLayout 中 子 View 可使用的最大宽度
int childWidthSpace = width - paddingLeft - paddingRight;
//行高
int lineHeight = 0;
//最大行高
int maxLineHeight = 0;
//已使用 width
int usedWidth = 0;
//总高度
int totalHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
//已使用的 width 计算
usedWidth += lp.leftMargin + lp.rightMargin + childWidth;
//当前 view 的高度
lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
//当剩余空间不足时,换行
if (usedWidth > childWidthSpace) {
totalHeight += maxLineHeight;
//重置 left
childLeft = paddingLeft;
//增加 top 值
childTop = paddingTop + totalHeight;
maxLineHeight = 0;
usedWidth = lp.leftMargin + lp.rightMargin + childWidth;
}
maxLineHeight = Math.max(lineHeight, maxLineHeight);
childLeft += lp.leftMargin;
childTop += lp.topMargin;
childRight = childLeft + childWidth;
childBottom = childTop + childHeight;
childView.layout(childLeft, childTop, childRight, childBottom);
childLeft = childRight + lp.rightMargin;
}
}
//添加 子 view
public void setAdapter(BaseAdapter mAdapter) {
this.mAdapter = mAdapter;
if (mAdapter == null || mAdapter.getCount() == 0) {
return;
}
removeAllViews();
for (int i = 0; i < mAdapter.getCount(); i++) {
View view = mAdapter.getView(i, null, null);
;
; Log.e("=========",view.getLayoutParams()+"");
final int position = i;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mListener != null) {
mListener.itemClick(position);
}
}
});
addView(view);
}
requestLayout();
}
public void setItemClickListener(TagItemClickListener mListener) {
this.mListener = mListener;
}
public interface TagItemClickListener {
void itemClick(int position);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
}
现在的 FlowLayout 就已经支持了 padding 以及 margin ,代码非常简单,因为只是一个练手项目,也没对外提供其他功能,下篇可能会实现 点击拖拽排序 功能。
网友评论