概述
最近几个月终于有大把时间总结这两年来所学 2019.6.1
前言
上一篇自己写了一个模仿微信的SlideView,主要复习了自定义View方面的一些知识点。这几天有看到自定义ViewGroup相关的知识点,于是也打算拿一个简单的自定义ViewGroup来学习一下。
Github 地址
TagLayout
效果展示
下面这个是一个个人兴趣选择界面,里面主要是各种各样的Tag。下面我选取其中一部分,来实现。
我的实现
模仿的对象
使用方法
先看看怎么用
- 在xml中
...
<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中
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中,做初始化。
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中做了下面的处理:
@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的功能比较单一了,绘制一个文本框,以及对文字做一个偏移,保证文字居中对齐于文本框。实现的效果像下面这样子。
代码如下:
@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即可。 消费事件就是在其他动作里面做一些操作。 代码如下:
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中存储的数据读取出来就可以了,代码如下:
@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的尺寸,以及计算自己的尺寸,还有各种换行的情况需要考虑,但是整体的逻辑是很简单的,先贴一下代码:
@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的尺寸,然后记录下来,最后把自己的总尺寸也记录下来。 这里有三个很关键的方法:measureChildWithMargins
和child.getMeasuredWidth() child.getMeasuredHeight()
和setMeasuredDimension(...);
。
方法1. measureChildWithMargins
:这个方法是获取LayoutParams
,然后调用child.measure(...)
,我可以贴一下他的源码,
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如何实现所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复