那些年我们熬夜打造一可收缩流式标签控件
来源:互联网 发布:java线程教程 编辑:程序博客网 时间:2024/05/21 08:54
一、前言
时间匆匆,一眨眼来厦门已经一个多月了。似乎已经适应了这边的生活,喜欢这边的风,温和而舒适,还有淡淡海的味道 。。。
还在再次跟大家致个歉意,博客的更新又延期了。新的环境,新的工作加上项目大改版,基本每天都有大量的事情,周末也不得空闲。
非常感谢大家的陪伴,一路有你们,生活会充满美好。
标签控件
本文还是继续讲解自定义控件,近期在项目中有这样一个控件。
实现可收缩的流式标签控件,具体效果图如下:
支持多选,单选,反选
子 View 定制化
效果图不是很清晰,文章后面会提供下载地址。
主要实现功能细分如下:
实现流式布局(第一个子 View 始终位于首行的最右边)
布局可定制化(采取适配模式)
实现控件的收缩
主要有这三个小的功能组成。第一个流式布局实现需要注意的是,第一个元素(子 View)需要固定在首行的最右边,采取的解决方案是首先绘制第一个元素且绘制在最右边;第二个布局可定制化,怎么来理解这句话呢?我希望实现的子 View 不单单是圆角控件,而是高度定制的所有控件,由用户来决定,采取的解决方案是采用了适配模式;第三个控件的收缩,这个实现起来就比较简单了,完成了第一步就可以获取到控件的高度,采用属性动画来动态改变控件的高度。具体我们一起来往下面看看。
流式布局
效果图一栏:
实现效果图的流式布局,有两种方案。一、直接使用 recyclerView ;二、自定义继承 ViewGroup。本文采用第二种方案,相信大家一定非常熟悉自定义 View 三部曲 ->onMeasure() ->onLayout() ->onDraw() ,吐血推荐以下文章:
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
onMeasure()测量
要实现标签流式布局,需要涉及到以下几个问题:
(1)、【下拉按钮】 的测量和布局
标签布局当中【下拉按钮】始终固定在首行的最右边,如果依次绘制子 View 可能导致【下拉按钮】处于第二行,或未处于最右边(与最右边还有一定的间距)。为了满足需求,优先测量和布局【下拉按钮】并把第一个 View 作为【下拉按钮】。
(2)、何时换行
如果当前行已经放不下下一个控件,那么就需要把这个控件移到下一行显示。所以我们要有个变量记录当前行已经占据的宽度,以判断剩下的空间是否还能容得下下一个控件。
(3)、如何得到布局的宽度
为了得到布局的宽度,我们记录每行的高度取最大值。
(4)、如何得到布局的高度
记录每行的高度,布局的高度就是所有行高度之和。
声明的变量如下:
int lineWidth = 0; //记录每行的宽度 int lineHeight = 0; //记录每行的高度 int height = 0; //布局高度 int width = 0; //布局宽度 int count = getChildCount(); //所有子控件数量 boolean firstLine = true; //是否处于第一行 firstLineCount = 0; //第一行子 View 个数
然后开始测量(贴出 onMeasure 的全部代码,再细讲):
for (int i = 0; i < count; i++) { View child = getChildAt(i); //测量子View measureChild(child, widthMeasureSpec, heightMeasureSpec); int childWidth = 0; int childHeight = 0; MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (lineWidth + childWidth > measureWidth) { //需要换行 width = Math.max(lineWidth, width); height += lineHeight; //需要换行,而将此控件调到下一行,所以将此控件的高度和宽度初始化给lineHeight、lineWidth lineHeight = childHeight; lineWidth = childWidth; firstLine = false; } else { // 否则累加值lineWidth,lineHeight取最大高度 lineHeight = Math.max(lineHeight, childHeight); lineWidth += childWidth; if (firstLine) { firstLineCount++; firstLineHeight = lineHeight; } } //注意这里是用于添加尾部收起的布局,宽度为父控件宽度。所以要单独处理 if (i == count - 1) { height += lineHeight; width = Math.max(width, lineWidth); if (firstLine) { firstLineCount = 1; } } } //如果未超过一行 if (mFirstHeight) { measureHeight = height; } setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
首先我们循环遍历每个子控件,计算每个子控件的宽度和高度,代码如下:
View child = getChildAt(i); //测量子View measureChild(child, widthMeasureSpec, heightMeasureSpec); int childWidth = 0; int childHeight = 0; MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
需要注意 child.getMeasuredWidth() , child.getMeasuredHeight() 能够获取到值,必须先调用 measureChild() 方法;同理调用 onLayout() 后,getWidth() 才能获取到值。以下以子控件所占宽度来讲解:
childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
子控件所占宽度=子控件宽度+左右的 Margin 值 。还得注意一点为了获取到子控件的左右 Margin 值,需要重写以下方法:
@Override protected LayoutParams generateLayoutParams(LayoutParams lp) { return new MarginLayoutParams(lp); } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }
下面就是计算是否需要换行,以及计算父控件的宽高度:
if (lineWidth + childWidth > measureWidth) { //需要换行 width = Math.max(lineWidth, width); height += lineHeight; //因为由于盛不下当前控件,而将此控件调到下一行,所以将此控件的高度和宽度初始化给lineHeight、lineWidth lineHeight = childHeight; lineWidth = childWidth; firstLine = false; //控件超过了一行 } else { // 否则累加值lineWidth,lineHeight取最大高度 lineHeight = Math.max(lineHeight, childHeight); lineWidth += childWidth; if (firstLine) { //控件未超过一行 firstLineCount++; //记录首行子控件个数 firstLineHeight = lineHeight;//获取第一行控件的高度 } }
由于 lineWidth 表示当前行已经占据的宽度,所以 lineWidth + childWidth > measureWidth,加上下一个子控件的宽度大于了父控件的宽度,则说明当前行已经放不下当前子控件,需要放到下一行;先看 else 部分,在未换行的情况 lineHeight 为当前行子控件的最大值,lineWidth 为当前行所有控件宽度之和。
在需要换行时,首先将当前行宽 lineWidth 与目前的最大行宽 width 比较计算出最新的最大行宽 width,作为当前父控件所占的宽度,还要将行高 lineHeight 累加到height 变量上,以便计算出父控件所占的总高度。
width = Math.max(lineWidth, width); height += lineHeight;
在需要换行时,需要对当前行宽,高进行赋值。
lineHeight = childHeight; lineWidth = childWidth;
我们还需要处理一件事情,记录首行子控件的个数以及首行的高度。
if (firstLine) { //控件未超过一行 firstLineCount++; //记录首行子控件个数 firstLineHeight = lineHeight;//获取第一行控件的高度 }
如果超过了一行 firstLine 赋值为 false 。
最后一个子控件我们需要单独处理,获取最终的父控件的宽高度。
//最后一行是不会超出width范围的,所以要单独处理 if (i == count - 1) { height += lineHeight; width = Math.max(width, lineWidth); if (firstLine) { firstLineCount = 1; } }
最后就是调用 setMeasuredDimension() 方法,设置到系统中。
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
onLayout()布局
布局所有的子控件,由于控件要后移和换行,所以我们要标记当前控件的 left 坐标和 top 坐标,申明的几个变量如下:
int count = getChildCount(); int lineWidth = 0;//累加当前行的行宽 int lineHeight = 0;//当前行的行高 int top = 0, left = 0;//当前坐标的top坐标和left坐标 int parentWidth = getMeasuredWidth(); //父控件的宽度
首先我们需要布局第一个子控件,使它位于首行的最右边。调用 child.layout
进行子控件的布局。layout 的函数如下,分别计算 l , t , r , b
layout(int l, int t, int r, int b)
l = 父控件的宽度 - 子控件的右Margin - 子控件高度
t = 子控件的顶部Margin
r = l + 子控件宽度
b = t + 子控件高度
具体布局代码如下:
if (i == 0) { child.layout(parentWidth - lp.rightMargin - child.getMeasuredWidth(), lp.topMargin, parentWidth - lp .rightMargin, lp.topMargin + child.getMeasuredHeight()); firstViewWidth = childWidth; firstViewHeight = childHeight; continue; }
接着按着顺序对子控件进行布局,先计算出子控件的宽高:
View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //宽度(包含margin值和子控件宽度) int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; //高度同上 int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
然后判断当前布局子控件是否为首行最后布局的控件,并对 lineWidth
, lineHeight
再次计算:
if (firstLineCount == (i + 1)) { lineWidth += firstViewWidth; lineHeight = Math.max(lineHeight, firstViewHeight); }
然后根据是否要换行来计算当行控件的 top 坐标和 left 坐标:
if (childWidth + lineWidth >getMeasuredWidth()){ //如果换行,当前控件将跑到下一行,从最左边开始,所以left就是0,而top则需要加上上一行的行高,才是这个控件的top点; top += lineHeight; left = 0; //同样,重新初始化lineHeight和lineWidth lineHeight = childHeight; lineWidth = childWidth; }else{ // 否则累加值lineWidth,lineHeight取最大高度 lineHeight = Math.max(lineHeight,childHeight); lineWidth += childWidth; }
在计算好 left,top 之后,然后分别计算出控件应该布局的上、下、左、右四个点坐标,需要非常注意的是 margin 不是 padding,margin 的距离是不绘制的控件内部的,而是控件间的间隔。
//计算childView的left,top,right,bottom int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + child.getMeasuredWidth(); int bc = tc + child.getMeasuredHeight(); child.layout(lc, tc, rc, bc); //将left置为下一子控件的起始点 left += childWidth;
最后在 onLayout
方法当中,我们需要保存当前父控件的高度来实现收缩,展开效果。
if (mFirstHeight) { contentHeight = getHeight(); mFirstHeight = false; if (mListener != null) { mListener.onFirstLineHeight(firstLineHeight); } }
onLayout 的完整代码如下:
private void buildLayout() { int count = getChildCount(); int lineWidth = 0;//累加当前行的行宽 int lineHeight = 0;//当前行的行高 int top = 0, left = 0;//当前坐标的top坐标和left坐标 int parentWidth = getMeasuredWidth(); //父控件的宽度 for (int i = 0; i < count; i++) { View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (i == 0) { child.layout(parentWidth - lp.rightMargin - child.getMeasuredWidth(), lp.topMargin, parentWidth - lp .rightMargin, lp.topMargin + child.getMeasuredHeight()); firstViewWidth = childWidth; firstViewHeight = childHeight; continue; } if (firstLineCount == (i + 1)) { lineWidth += firstViewWidth; lineHeight = Math.max(lineHeight, firstViewHeight); } if (childWidth + lineWidth > getMeasuredWidth()) { //如果换行 top += lineHeight; left = 0; lineHeight = childHeight; lineWidth = childWidth; } else { lineHeight = Math.max(lineHeight, childHeight); lineWidth += childWidth; } //计算childView的left,top,right,bottom int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + child.getMeasuredWidth(); int bc = tc + child.getMeasuredHeight(); child.layout(lc, tc, rc, bc); //将left置为下一子控件的起始点 left += childWidth; } if (mFirstHeight) { contentHeight = getHeight(); mFirstHeight = false; if (mListener != null) { mListener.onFirstLineHeight(firstLineHeight); } } }
布局可定制化
为了实现布局的可定制化,采用了适配模式,
public void setAdapter(ListAdapter adapter) { if (adapter != null && !adapter.isEmpty()) { buildTagItems(adapter);//构建标签列表项 } }
先贴出构建标签列表项的代码:
private void buildTagItems(ListAdapter adapter) { //移除所有控件 removeAllViews(); //添加首view // addFirstView(); for (int i = 0; i < adapter.getCount(); i++) { final View itemView = adapter.getView(i, null, this); final int position = i; if (itemView != null) { if (i == 0) { firstView = itemView; itemView.setVisibility(View.INVISIBLE); itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //展开动画 expand(); } }); } else { itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { //item 点击回调 mListener.onClick(v, position); } } }); } itemView.setTag(TAG + i); mChildViews.put(i, itemView); //添加子控件 addView(itemView); } } //添加底部收起试图 addBottomView(); }
获取子控件:
final View itemView = adapter.getView(i, null, this);
针对第一个子控件,点击展开试图:
if (i == 0) { firstView = itemView; itemView.setVisibility(View.INVISIBLE); itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //展开 expand(); } });
然后添加子控件:
addView(itemView);
最后添加底部:
addBottomView();
源码在文章的末尾,文章有点长,希望各位继续往后面看。
控件的展开和收缩
控件展开为例:
private void expand() { //属性动画 ValueAnimator animator = ValueAnimator.ofInt(firstLineHeight, contentHeight); animator.setDuration(mDuration); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { //获取到属性动画值,并刷新控件 int value = (int) animation.getAnimatedValue(); getLayoutParams().height = value; requestLayout();//重新布局 } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { if (mListener != null) { //主要对蒙层的处理 mListener.showMask(); } firstView.setVisibility(View.INVISIBLE);//第一个View不可见 bottomCollapseLayout.setVisibility(View.VISIBLE);//底部控件可见 } @Override public void onAnimationEnd(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start();}
如果你对属性动画还有疑问的话,请参考如下文章:
自定义控件三部曲之动画篇(四)——ValueAnimator基本使用
自定义控件三部曲之动画篇(七)——ObjectAnimator基本使用
文章讲到这里差不多就要结束了,提前预祝大家【五一快乐】
第二种简单实现方式,效果图如下:
如有什么疑问,欢迎讨论,以下是联系方式:
源码地址
- 那些年我们熬夜打造一可收缩流式标签控件
- 那些年,我们研究过得控件
- 线程<一>---那些年我们一起学习linux程序设计 .
- 网络编程<一>---那些年我们一起学习linux程序设计 .
- 那些年,我们一起用的移动APP开发工具
- 那些年我们一起用过的QQ
- 那些年我们一起追过的缓存写法(一)
- 【航海日志】那些年,我们在做物联网(一)
- 回忆那些年我们一起爆掉的PG
- 那些年我们一起用过的软件
- 那些年我们一起追过的缓存写法(一)
- 那些年我们一起追过的缓存写法(一)
- 打造QQ个性化可拉伸头部控件
- 那些年的我们
- 那些年,我们在一起
- Android Design新控件之TabLaout(一),快速打造一个滑动标签页
- Android Design新控件之TabLaout(一),快速打造一个滑动标签页
- 程序员:我们为什么要熬夜
- libjpeg哈夫曼算法压缩图片
- 四 libnl库详解
- 算法学习---求最大公约数(Gcd算法)
- js中实现继承6种方法总结
- Android性能优化总结
- 那些年我们熬夜打造一可收缩流式标签控件
- 判断点是否在多边形内
- MySQL高效载入数据到MyISAM表的方法
- 利用opencv的FileStorage类实现XML/YAML文件的读写
- 【聚焦Oracle】单行函数
- 一个彻彻底底的水军的ACM校赛感悟~~~
- NGUI之Slider调节音量大小
- 简单新闻管理系统(jsp)
- Android实现Intent传递对象