【Android - 自定义View】之自定义可滚动的流式布局
首先来介绍一下这个自定义View:
- (1)这个自定义View的名称叫做 FlowLayout ,继承自ViewGroup类;
- (2)在这个自定义View中,用户可以放入所有继承自View类的视图,这个布局会自动获取其宽高并排列在布局中,保证每一个视图都完整的显示在界面上;
- (3)如果用户放入布局的视图的总高度大于设置给这个视图的高度,那么视图就可以支持上下滚动;
- (4)可以在XML布局文件或JAVA文件中设置布局的padding属性和子元素的margin属性。
接下来简单介绍一下在这个自定义View中用到的技术点:
- (1)用到了自定义View三大流程中的测量和布局流程,分别体现在 onMeasure() 和 onLayout() 两个方法中;
- (2)在onMeasure()方法中,测量所有子元素的宽高,最后通过累加判断得到自身要显示的宽高;
- (3)在onMeasure()方法中还为所有子元素进行了分行,保证每个子元素都能完整的显示在布局中,达到“流式布局”的功能需求;
- (4)在onLayout()方法中,一次取出所有子元素,获取onMeasure()方法中测量的宽高,开始布局;
- (5)在上面的测量和布局过程中,都有将布局的 padding 属性和元素的 margin 属性考虑在内;
- (6)设置了这个布局中的子元素具有的LayoutParams:通过 generateLayoutParams() 方法设置;
- (7)在 onInterceptTouchEvent() 方法中对事件进行拦截,保证布局滚动和元素点击不会产生冲突;
- (8)在 onTouchEvent() 方法中处理了触摸事件,实现布局的滚动功能。
下面是这个自定义View—— FlowLayout 的实现代码:
自定义View类 FlowLayout.java 中的代码:
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup; import java.util.ArrayList;
import java.util.List; /**
* 自定义流式布局
*/
public class FlowLayout extends ViewGroup {
private List<List<View>> views; // 存放所有子元素(一行一行存储)
private List<View> lineViews; // 存储每一行中的子元素
private List<Integer> heights; // 存储每一行的高度 private boolean scrollable; // 是否可以滚动
private int measuredHeight; // 测量得到的高度
private int realHeight; // 整个流式布局控件的实际高度
private int scrolledHeight = 0; // 已经滚动过的高度
private int startY; // 本次滑动开始的Y坐标位置
private int offsetY; // 本次滑动的偏移量
private boolean pointerDown; // 在ACTION_MOVE中,视第一次触发为手指按下,从第二次触发开始计入正式滑动 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);
} /**
* 初始化
*/
private void init() {
views = new ArrayList<>();
lineViews = new ArrayList<>();
heights = new ArrayList<>();
} /**
* 计算布局中所有子元素的宽度和高度,累加得到整个布局最终显示的宽度和高度
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 当前行的宽度和高度(宽度是子元素宽度的和,高度是子元素高度的最大值)
int lineWidth = 0;
int lineHeight = 0;
// 整个流式布局最终显示的宽度和高度
int flowLayoutWidth = 0;
int flowLayoutHeight = 0;
// 初始化各种参数(列表)
init();
// 遍历所有子元素,对子元素进行排列
int childCount = this.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
// 获取到子元素的宽度和高度
measureChild(child, widthMeasureSpec, heightMeasureSpec);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 如果当前行中剩余的空间不足以容纳下一个子元素,则换行
// 换行的同时,保存当前行中的所有元素,叠加行高,然后将行宽和行高重置为0
if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin > widthSize - getPaddingLeft() - getPaddingRight()) {
views.add(lineViews);
lineViews = new ArrayList<>();
flowLayoutWidth = Math.max(flowLayoutWidth, lineWidth); // 以最宽的行的宽度作为最终布局的宽度
flowLayoutHeight += lineHeight;
heights.add(lineHeight);
lineWidth = 0;
lineHeight = 0;
}
// 无论换不换行,都需要将元素添加到列表中、处理宽度和高度的值
lineViews.add(child);
lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin);
// 处理最后一行,否则最后一行不能显示
if (i == childCount - 1) {
flowLayoutHeight += lineHeight;
flowLayoutWidth = Math.max(flowLayoutWidth, lineWidth);
heights.add(lineHeight);
views.add(lineViews);
}
}
// 得到最终的宽高
// 宽度:如果是EXACTLY模式,则遵循测量值,否则使用我们计算得到的宽度值
// 高度:只要布局中内容的高度大于测量高度,就使用内容高度(无视测量模式);否则才使用测量高度
int width = widthMode == MeasureSpec.EXACTLY ? widthSize : flowLayoutWidth + getPaddingLeft() + getPaddingRight();
realHeight = flowLayoutHeight + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.EXACTLY) {
realHeight = Math.max(measuredHeight, realHeight);
}
scrollable = realHeight > measuredHeight;
// 设置最终的宽高
setMeasuredDimension(width, realHeight);
} /**
* 对所有子元素进行布局
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 当前子元素应该布局到的X、Y坐标
int currentX = getPaddingLeft();
int currentY = getPaddingTop();
// 遍历所有子元素,对每个子元素进行布局
// 遍历每一行
for (int i = 0; i < views.size(); i++) {
int lineHeight = heights.get(i);
List<View> lineViews = views.get(i);
// 遍历当前行中的每一个子元素
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
// 获取到当前子元素的上、下、左、右的margin值
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childL = currentX + lp.leftMargin;
int childT = currentY + lp.topMargin;
int childR = childL + child.getMeasuredWidth();
int childB = childT + child.getMeasuredHeight();
// 对当前子元素进行布局
child.layout(childL, childT, childR, childB);
// 更新下一个元素要布局的X、Y坐标
currentX += lp.leftMargin + child.getMeasuredWidth() + lp.rightMargin;
}
currentY += lineHeight;
currentX = getPaddingLeft();
}
} /**
* 滚动事件的处理,当布局可以滚动(内容高度大于测量高度)时,对手势操作进行处理
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// 只有当布局可以滚动的时候(内容高度大于测量高度的时候),才会对手势操作进行处理
if (scrollable) {
int currY = (int) event.getY();
switch (event.getAction()) {
// 因为ACTION_DOWN手势可能是为了点击布局中的某个子元素,因此在onInterceptTouchEvent()方法中没有拦截这个手势
// 因此,在这个事件中不能获取到startY,也因此才将startY的获取移动到第一次滚动的时候进行
case MotionEvent.ACTION_DOWN:
break;
// 当第一次触发ACTION_MOVE事件时,视为手指按下;以后的ACTION_MOVE事件才视为滚动事件
case MotionEvent.ACTION_MOVE:
// 用pointerDown标志位只是手指是否已经按下
if (!pointerDown) {
startY = currY;
pointerDown = true;
} else {
offsetY = startY - currY; // 下滑大于0
// 布局中的内容跟随手指的滚动而滚动
// 用scrolledHeight记录以前的滚动事件中滚动过的高度(因为不一定每一次滚动都是从布局的最顶端开始的)
this.scrollTo(0, scrolledHeight + offsetY);
}
break;
// 手指抬起时,更新scrolledHeight的值;
// 如果滚动过界(滚动到高于布局最顶端或低于布局最低端的时候),设置滚动回到布局的边界处
case MotionEvent.ACTION_UP:
scrolledHeight += offsetY;
if (scrolledHeight + offsetY < 0) {
this.scrollTo(0, 0);
scrolledHeight = 0;
} else if (scrolledHeight + offsetY + measuredHeight > realHeight) {
this.scrollTo(0, realHeight - measuredHeight);
scrolledHeight = realHeight - measuredHeight;
}
// 手指抬起后别忘了重置这个标志位
pointerDown = false;
break;
}
}
return super.onTouchEvent(event);
} /**
* 调用在这个布局中的子元素对象的getLayoutParams()方法,会得到一个MarginLayoutParams对象
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
} /**
* 事件拦截,当手指按下或抬起的时候不进行拦截(因为可能这个操作只是点击了布局中的某个子元素);
* 当手指移动的时候,才将事件拦截;
* 因此,我们在onTouchEvent()方法中,只能将ACTION_MOVE的第一次触发作为手指按下
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
intercepted = true;
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
}
布局文件 activity_main.xml 中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <my.itgungnir.flowlayout.FlowLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20.0dip"> <Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="My name is ITGungnir" /> ......(省略N个Button) <Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20.0dip"
android:text="Hello" /> ......(省略N个Button) <Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="搜嘎" />
</my.itgungnir.flowlayout.FlowLayout> </LinearLayout>
这里注意:我给FlowLayout布局设置了padding为20.0dip;Hello那个按钮的margin属性也设置成了20.0dip,具体的效果见最后的运行效果图。
主界面JAVA代码中不需要书写任何代码。当然,我们也可以在Activity中通过JAVA代码,动态生成View后添加到这个布局中。这里就不演示了。
项目的运行效果图如下图所示:

【Android - 自定义View】之自定义可滚动的流式布局的更多相关文章
- Android 自定义View修炼-Android中常见的热门标签的流式布局的实现
一.概述:在日常的app使用中,我们会在android 的app中看见 热门标签等自动换行的流式布局,今天,我们就来看看如何 自定义一个类似热门标签那样的流式布局吧(源码下载在下面最后给出哈) 类似的 ...
- 自定义View(三)--实现一个简单地流式布局
Android中的流式布局也就是常说的瀑布流很是常见,不仅在很多项目中都能见到,而且面试中也有很多面试官问道,那么什么是流式布局呢?简单来说就是如果当前行的剩余宽度不足以摆放下一个控件的时候,则自动将 ...
- Android控件进阶-自定义流式布局和热门标签控件
技术:Android+java 概述 在日常的app使用中,我们会在android 的app中看见 热门标签等自动换行的流式布局,今天,我们就来看看如何 自定义一个类似热门标签那样的流式布局吧,类 ...
- Android自定义之流式布局
流式布局,好处就是父类布局可以自动的判断子孩子是不是需要换行,什么时候需要换行,可以做到网页版的标签的效果.今天就是简单的做了自定义的流式布局. 具体效果: 原理: 其实很简单,Measure La ...
- 28 自定义View流式布局
流式布局每行的行高以本行中最高的元素作为高,如果一个元素放不下到一行时直接到第二行 FlowLayoutView package com.qf.sxy.customview05.widget; imp ...
- 自定义ViewGroup 流式布局
使用 public class MainActivity extends Activity { @Override protected void onCreate(Bundle sav ...
- 自定义流式布局:ViewGroup的测量与布局
目录 1.View生命周期以及View层级 1.1.View生命周期 1.2.View层级 2.View测量与MeasureSpec类 2.1.MeasureSpec类 2.2.父View的限制 :测 ...
- Dialog详解(包括进度条、PopupWindow、自定义view、自定义样式的对话框)
Dialog详解(包括进度条.PopupWindow.自定义view.自定义样式的对话框) Android中提供了多种对话框,在实际应用中我们可能会需要修改这些已有的对话框.本实例就是从实际出发, ...
- Android 自动换行流式布局的RadioGroup
效果图 用法 使用FlowRadioGroup代替RadioGroup 代码 import android.content.Context; import android.util.Attribute ...
随机推荐
- zabbix导入数据库报错1046 (3D000) : No database selected
Zabbix导入数据库时报错 使用如下命令导入Zabbix数据库时报错 zcat /usr/share/doc/zabbix-server-mysql/create.sql.gz | mysql -u ...
- Access教程 Access学习 Access培训 Access QQ交流集中地
Access教程 Access学习 Access培训 Access QQ交流集中地 http://www.office-cn.net/plugin.php?id=zstm_qqgroup:index ...
- 跳跳棋——二分+建模LCA
题目描述 跳跳棋是在一条数轴上进行的.棋子只能摆在整点上.每个点不能摆超过一个棋子. 我们用跳跳棋来做一个简单的游戏:棋盘上有3颗棋子,分别在a,b,c这三个位置.我们要通过最少的跳动把他们的位置移动 ...
- Project Euler 54: Poker hands
在纸牌游戏中,一手包含五张牌并且每一手都有自己的排序,从低到高的顺序如下: 大牌:牌面数字最大 一对:两张牌有同样的数字 两对:两个不同的一对 三条:三张牌有同样的数字 顺子:所有五张牌的数字是连续的 ...
- jdbc 加载数据库驱动如何破坏双亲委托模式
导读 通过jdbc链接数据库,是每个学习Java web 方向的人必然一开始会写的代码,虽然现在各路框架都帮大家封装好了jdbc,但是研究一下jdbc链接的套路还是很意义 术语以及相 ...
- python——inspect模块
inspect模块常用功能 import inspect # 导入inspect模块 inspect.isfunction(fn) # 检测fn是不是函数 inspect.isgenerator((x ...
- jquery.eraser制作擦涂效果
jquery.eraser制作擦涂效果 <pre><!DOCTYPE html><html> <head> <meta http-equiv=&q ...
- nyoj 208 + poj 1456 Supermarket (贪心)
Supermarket 时间限制:1000 ms | 内存限制:65535 KB 难度:4 描述 A supermarket has a set Prod of products on sal ...
- Hadoop2.8.2 运行wordcount
1 例子jar位置 [hadoop@hadoop02 mapreduce]$ pwd /hadoop/hadoop-2.8.2/share/hadoop/mapreduce [hadoop@hadoo ...
- SQLite性能 - 它不是内存数据库,不要对IN-MEMORY望文生意。
SQLite创建的数据库有一种模式IN-MEMORY,但是它并不表示SQLite就成了一个内存数据库.IN-MEMORY模式可以简单地理解为,本来创建的数据库文件是基于磁盘的,现在整个文件使用内存空间 ...