文章收藏的好句子:成功从来不是一蹴而就,而是日积月累的努力叠加的结果。
ps:本文是基于 Android API 31 来分析的,文章写的 demo 是用 AndroidStudio 工具来开发的。
目录
1、自定义 View 的分类
2、自定义 View 的注意事项
3、自定义 View 的实例
1、自定义 View 的分类
自定义 View 的分类到目前还没有统一的标准,以我对自定义 View 的学习积累,我把自定义 View 规划为以下这4类。
(1)直接继承于 View
直接继承于 View 来绘制一些不规则的视图,需要通过绘制的方式来实现,也就是要重写 View 的 onDraw方法;直接继承于 View 的子类都是需要自己去处理 wrap_content (看Android中View的工作流程之measure过程这篇文章,如果自定义的 View 直接继承 View 不处理 wrap_content,那么就和 match_parent 的效果是一样的)和 padding ,否则做出来的效果和我们实现的不一样。
(2)直接继承于 ViewGroup
这种方法用于实现自定义的布局,像 LinearLayout、RelativeLayout 等是属于系统的布局,如果我们需要重新定义一种新布局,可以采用这种方法来实现,比如说布局可以左右(把ViewPager看作是一种自定义布局嘛)且还产生水波纹效果的时候,用这种方式有点复杂,需要适当地处理 ViewGroup 的测量、布局这两个过程,与此同时处理子元素的测量和布局过程。
(3)直接继承于具体的 View(例如 TextView)
用于拓展某种已具有的 View 功能,比如说 TextView,它被 Button 继承着,Button 主要用来设置点击事情的;直接继承于具体的 View,它不需要自己去处理 wrap_content 和 padding。
(4)直接继承于具体的 ViewGroup(例如 FrameLayout)
如果某种效果看起来和具体的 ViewGroup 很像的时候,也就是和具体的 ViewGroup 布局子元素特性很像的时候,可以采用这种方法来实现,采用这种方法不需要自己处理 ViewGroup的 measure 和 layout 这两个过程。
一种 View 的显示效果有很多种自定义 View 的方式去实现,喜欢用哪种最终还是看开发者自己的喜好,好,我们现在说一下自定义 View 应该注意的一些事项。
2、自定义 View 的注意事项
(1)直接继承于 View 或者 ViewGroup,当用到 wrap_content 值时,必须支持 wrap_content;如果不在 onMeasure 中对 wrap_content 做特殊处理,那么当自定义 View 或者自定义 ViewGroup 在布局中使用 wrap_content 时就没法达到预期的效果,那么我们看到的就是和 match_parent 一样的效果,可以看Android中View的工作流程之measure过程这篇文章,你们就可以看到和 match_parent 一样的效果了。
(2)如果直接继承 View 且使用到 padding 时,如果不在 draw 方法中处理 padding,那么 padding 属性就不会起作用;如果直接继承自 ViewGroup 并且使用到 wrap_content 时,需要在 onMeasure 和 onLayout 方法中处理 padding 和子 View 的 margin 对它造成的影响。
(3)不要在自定义 View 中创建一个 Handler ,因为 View 提供了 post 一系列的方法,View 的一系列 post 方法其实还是调用了 Handler 的 post 一系列的方法。
(4)当 View 变得不可见时我们需要停止线程和动画,如果不处理这种情况,很容易造成内存泄漏;如果有线程或者动画需要停止时,最好是在 View 的 onDetachedFromWindow 方法中进行调用,因为 Activity 退出或者 View 被删除的时候,View 的 onDetachedFromWindow 方法会被执行。
(5)如果 View 存在滑动冲突,应当处理好滑动冲突,否则会影响用户体验效果;有关滑动冲突的内容可以看Android中View的滑动冲突这篇文章。
3、自定义 View 的实例
我们这里就写一个直接继续于 View 的案例,我们就画一个椭圆,步骤如下所示;
(1)写一个 EllipseView并继承于 View;
package com.xe.myapplication4;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowManager;
/**
* Created by 86188 on 2022/7/7.
*/
public class EllipseView extends View {
Paint p;
private int color;
public EllipseView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context,attrs);
}
public EllipseView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
p = new Paint();
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.EllipseView);
//1、
color = ta.getColor(R.styleable.EllipseView_ellipse_view_color,Color.GREEN);
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//2、
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200,150);
//3、
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200,heightSpecSize);
//4、
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize,150);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//5、
int leftPadding = getPaddingLeft();
int rightPadding = getPaddingRight();
int topPadding = getPaddingTop();
int buttomPadding = getPaddingBottom();
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int width2 = width - rightPadding;
int height2 = height - buttomPadding;
p.setColor(color);
p.setAntiAlias(true);
RectF rectF = new RectF();
rectF.set(leftPadding,topPadding,width2,height2);
canvas.drawOval(rectF, p);
}
}
(2)在 values 目录下创建一个 attrs.xml 文件,并自定义一个 ellipse_view_color属性,格式为 color 类型的,其值就是颜色值;
(3)自定义 View 在 xml 中的布局;
说明:
1)看注释1,它是解析自定义属性的 ellipse_view_color 值,如果在 xml 布局文件中没有使用该属性,那么就默认为绿色。
2)看注释2、3、4,处理 wrap_content 模式,当宽在 wrap_content 模式时,就将宽设置为200;当高在 wrap_content模式时,就将高设置为150。
3)看注释5,获取上下左右的 padding,绘制自定义 View 时处理 padding。
4)看注释6的 ellipse_view_color 属性,是不是有 app 的前缀?这是自定义属性的标志,使用自定义属性时必须在 xml 布局文件中添加 schemas 声明:xmlns:app="http://schemas.android.com/apk/res-auto"。
程序运行结果如下所示;