我是靠谱客的博主 唠叨犀牛,这篇文章主要介绍自定义View和ViewGroup之TagLayout前言TagLayout如何实现,现在分享给大家,希望可以做个参考。

最近几个月终于有大把时间总结这两年来所学 2019.6.1

前言

上一篇自己写了一个模仿微信的SlideView,主要复习了自定义View方面的一些知识点。这几天有看到自定义ViewGroup相关的知识点,于是也打算拿一个简单的自定义ViewGroup来学习一下。

Github 地址

TagLayout

效果展示

下面这个是一个个人兴趣选择界面,里面主要是各种各样的Tag。下面我选取其中一部分,来实现。

我的实现

模仿的对象

使用方法

先看看怎么用

  • 在xml中
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
... <android.support.constraint.ConstraintLayout ...> <com.cerkerli.library.view.TagLayout android:id="@+id/tag_layout" ...> <com.cerkerli.library.view.TagView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/material_design" android:text="@string/material_design" android:textSize="16sp"/> <com.cerkerli.library.view.TagView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/rx_java" android:text="@string/rxjava" android:textSize="16sp"/> <com.cerkerli.library.view.TagView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/gradle" android:text="@string/gradle" android:textSize="16sp"/> <com.cerkerli.library.view.TagView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/android_studio" android:text="@string/android_studio" android:textSize="16sp"/> <com.cerkerli.library.view.TagView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/kotlin" android:text="@string/kotlin" android:textSize="16sp"/> <com.cerkerli.library.view.TagView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/flutter" android:text="@string/flutter" android:textSize="16sp"/> <com.cerkerli.library.view.TagView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/more..." android:text="@string/more" android:textSize="16sp"/> </com.cerkerli.library.view.TagLayout> </android.support.constraint.ConstraintLayout> 复制代码

在activity中

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TagLayout tagLayout = findViewById(R.id.tag_layout); tagLayout.setOnClickListener(new Listener.TagLayoutClickListener() { @Override public void onClick(int id, String text, boolean isSelected) { switch (id){ case R.id.material_design: case R.id.rx_java: case R.id.gradle: case R.id.android_studio: case R.id.kotlin: case R.id.flutter: Util.toast(text + " " + isSelected); break; default:break; } } }); 复制代码

我们可以在activity中做响应的操作。 在Application中,做初始化。

复制代码
1
2
3
4
5
public void onCreate() { super.onCreate(); Library.init(getApplicationContext()); } 复制代码

--------------------------------我是分割线---------------------------------


如何实现

  • 简单介绍一下实现

TagView

TagView继承自TextView,在TextView的基础上添加了一个椭圆形的文本框。 重点方法有三个:onMeasure,onDraw,onTouchEvent,下面分别介绍一个每个方法。

onMeasure

onMeasure的主要功能是重新测量View的尺寸。我们知道在xml的配置中,layout_width,layout_height有三种值,分别是wrap_content,match_parent,exactly「10dp 30dp」,我在onMeasure中做了下面的处理:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //测量文字长度 getPaint().getTextBounds(getText().toString(), 0, getText().toString().length(), textRect); widthSize = textRect.right - textRect.left; //测量文字的推荐高度 //这个问题被卡的比较久,之前一个用的是 textRect的top和bottom Paint.FontMetrics fontMetrics = getPaint().getFontMetrics(); heightSize = (int)(fontMetrics.bottom - fontMetrics.top); //重新测量长和宽 setMeasuredDimension(widthSize + heightSize * 2, heightSize + heightSize); } 复制代码

这里可以看到,我强制重新测量了文字的width和height,改成了wrap_content。这意味着,整个文本框的大小都由文字的大小决定,而不是 layout_width,layout_height 这两个值,就是说,不管你把layout_width,layout_height写成多少dp,最后整个文本框的大小都由 textSize参数决定。

这里的重要知识点是文字的测量,想要具体学习的话,推荐看看Hencoder课程

onDraw

onDraw的功能比较单一了,绘制一个文本框,以及对文字做一个偏移,保证文字居中对齐于文本框。实现的效果像下面这样子。

代码如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
@Override protected void onDraw(Canvas canvas) { canvas.drawRoundRect(CIRCLE_WIDTH, CIRCLE_WIDTH, getWidth() - (CIRCLE_WIDTH << 1), getHeight() - (CIRCLE_WIDTH << 1), circleRadius, circleRadius, mPaint); canvas.save(); canvas.translate(heightSize, heightSize >> 1); super.onDraw(canvas); canvas.restore(); } 复制代码

onTouchEvent

onTouchEvent 实现点击事件,由于这个点击事件比较简单,所以就没有用框架,自己简单实现了一下。 点击事件重点是两步:捕获事件,消费事件 捕获事件在MotionEvent.ACTION_DOWN中返回true即可。 消费事件就是在其他动作里面做一些操作。 代码如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public boolean onTouchEvent(MotionEvent event) { //捕获事件 if(event.getActionMasked() == MotionEvent.ACTION_DOWN){ return true; } //消费事件 if(event.getActionMasked() == MotionEvent.ACTION_UP){ //未点击-->点击 if(!isSelected){ isSelected = true; setPaintSelected(true); }else { //点击-->未点击 isSelected = false; setPaintSelected(false); } //监听回调 if(clickListener != null){ clickListener.onClick(getText().toString(),isSelected); } invalidate(); } return false; } 复制代码
  • TagView的部分是比较简单的,我在实现的时候,主要是遇到了文字测量的问题,花了不少时间,其他的都很基础。

TagLayout

TagLayout分为两部分,onMeasure和onLayout

onLayout

onLayout 比较容易,分别绘制每一个子View,然后将onMeasure中存储的数据读取出来就可以了,代码如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for(int i = 0;i < getChildCount();i++){ final TagView child = (TagView)getChildAt(i); Rect childBond = mChildBounds.get(i); child.layout(childBond.left,childBond.top,childBond.right,childBond.bottom); //设置点击监听 child.setOnClickListener(new Listener.TagViewClickListener() { @Override public void onClick(String text,boolean isSelected) { if(clickListener != null){ clickListener.onClick(child.getId(),text,isSelected); } } }); } } 复制代码

监听的部分是监听子View中的点击事件。

onMeasure

  • onMeasure的实现非常非常麻烦,麻烦在于需要挨个测量子View的尺寸,以及计算自己的尺寸,还有各种换行的情况需要考虑,但是整体的逻辑是很简单的,先贴一下代码:
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //当前行高 int LineWidthUsed = 0; int LineHeightUsed = 0; //总行高 int HeightUsed = 0; int WidthUsed = 0; final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); for(int i = 0;i < getChildCount();i++){ //获取子 View 以及布局子 View View child = getChildAt(i); measureChildWithMargins(child,widthMeasureSpec,LineWidthUsed,heightMeasureSpec,HeightUsed); if(LineWidthUsed + child.getMeasuredWidth() + Constants.WIDTH_NAP > widthSize && widthMode != MeasureSpec.UNSPECIFIED){ //是否需要换行 WidthUsed = LineWidthUsed; LineWidthUsed = 0; HeightUsed += LineHeightUsed; HeightUsed += Constants.HEIGHT_NAP; measureChildWithMargins(child,widthMeasureSpec,LineWidthUsed,heightMeasureSpec,HeightUsed); LLog.d(TAG,"换行 at " + i + " WidthUsed " + WidthUsed + " HeightUsed " + HeightUsed); } //数据写入List中 供Layout使用 mRect = new Rect(); mRect.set(LineWidthUsed,HeightUsed,LineWidthUsed + child.getMeasuredWidth(),HeightUsed + child.getMeasuredHeight()); mChildBounds.add(mRect); LLog.d(TAG,"i == "+i); LLog.d(TAG,mRect); //数据增加 LineWidthUsed += child.getMeasuredWidth(); LineWidthUsed += Constants.WIDTH_NAP; LineHeightUsed = Math.max(LineHeightUsed,child.getMeasuredHeight()); } HeightUsed+=LineHeightUsed; setMeasuredDimension(widthSize, HeightUsed); LLog.d(TAG,"setMeasuredDimension "+ widthSize + ":" + HeightUsed); } 复制代码

这个整体逻辑其实很简单,就是挨个去测量每个子View的尺寸,然后记录下来,最后把自己的总尺寸也记录下来。 这里有三个很关键的方法:measureChildWithMarginschild.getMeasuredWidth() child.getMeasuredHeight()setMeasuredDimension(...);

方法1. measureChildWithMargins:这个方法是获取LayoutParams,然后调用child.measure(...),我可以贴一下他的源码,

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } 复制代码

从源码可以看出,这个其实是一个集成好的方法,比较方便我们写的时候,就不用在一一判断三种值了(上面说到了wrap_content,match_parent,exactly「10dp 30dp」

方法2. child.getMeasuredWidth() child.getMeasuredHeight():这两个是获取子View的宽高,方便我们计算子View,然后通过子View计算自己的宽高。 方法3. setMeasuredDimension(...);:这个方法就是确定自己的宽高,和View的中用法一模一样。

— 这里就简单介绍一下TagLayout的实现,需要有一些基础才能看懂,所以没有基础的,建议看看Hencoder课程,这个我目前见过讲自定义View最完善的课程,没有之一。

TagLayout GitHub详细地址

再另外,以上都是自己平时所学整理,如果有错误,欢迎留言或者添加微信批评指出,一起学习,共同进步,爱生活,爱技术

最后

以上就是唠叨犀牛最近收集整理的关于自定义View和ViewGroup之TagLayout前言TagLayout如何实现的全部内容,更多相关自定义View和ViewGroup之TagLayout前言TagLayout如何实现内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(60)

评论列表共有 0 条评论

立即
投稿
返回
顶部