自定义视图组件流程

一、前言

Android提供了一个复杂且强大的组件化模型,帮我我们根据布局类View和ViewGroup来构建界面。Button、TestView、EdiText,LinearLayout、FrameLayout、RelativeLayout 等,然而在开发过程,一些系统通过的控件不能满足我们的要求,因此需要我自定义视图组件。

二、自定义视图组件的方式

  1. 完全自定义视图组件,继承View或者ViewGoup来完成
  2. 复合控件,结合现有的视图组件组合为满足我们需求的控件
  3. 修改现有的View类型,比如继承自ImageView,TextView,EditText,根据要求重写相应的方法来完成

自定义视图组件用上面的三种方式基本满足我们要求,其中实现过程的复杂度由我们具体的功能来定,但一些基本的流程不变,接下来通过一个绘制水杯的实例来了解自定义View的流程。

三、自定义View的流程

3.1 创建自定义视图类

public class WaterView extends View {
    public WaterView(Context context) {
        super(context);
    }
    public WaterView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public WaterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

}

3.2 定义自定义属性

如果需要自定义属性,需要向项目中添加 资源。这些资源通常放在res/values/attrs.xml文件中。如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="WaterView">
        <attr name="color" format="color"></attr>
    </declare-styleable>

</resources>

定义好属性后,接下来我们可以像内置属性一样在布局XML文件中使用他们。唯一的区别是自定义属性属于另一个命名空间。他们不属于 http://schemas.android.com/apk/res/android 命名空间,而是属于http://schemas.android.com/apk/res/[your package name]。如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.water.view.demo.WaterView
        android:id="@+id/waterView"
        android:layout_width="100dp"
        android:layout_height="200dp"
        app:color="@color/colorPrimaryDark" />
</LinearLayout>

这里为了避免反复使用冗长的命名空间URI,我们使用xmlns指令。此指令将别名app分配给命名空间http://schemas.android.com/apk/res-auto,改别名可以为任何别名,只要同一个文件不相同即可。

3.3 获取自定义属性

通过XML创建布局视图时,XML标记的所有属性都会从资源包读取,并作为AttributeSet传递到视图的构造函数。

Android资源编译器做了大量的工作,context.getTheme().obtainStyledAttributes()获得个TypeArray数组,其中包含已解除引用并设置了样式的值。对应res目录中的各个资源,生成的R.java定义一个由属性ID组成的数组,同时定义一组常量,用于定义改数组中各属性的索引。我们可以按照下面的方法从TypeArray中读取属性值。

public WaterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WaterView, 0, 0);
    try {
        color = typedArray.getColor(R.styleable.WaterView_color, DEFAULT_COLOR);
    } finally {
        typedArray.recycle();
    }
}

NoteTypedArray 对象是共享资源,必须在使用后回收。

3.4 处理布局大小

为了正确绘制自定义视图,我们需要知道他有多大。View提供多种测量方法,如果想要更精细地控制视图布局参数,需要重写onMeasure()方法,如果不需要对视图大小进行控制,只需要重写onSizeChanged()方法。

系统会在首次为您的视图分配大小时调用 onSizeChanged(),如果视图大小由于任何原因而改变,系统会再次调用该方法。可在该方法中计算位置、尺寸以及其他与视图大小相关的任何值,而不是在每次绘制时都重新计算。如下所示,我们可以在下面的方法中获取视图的高宽以及计算水位的结束Y坐标

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    viewWidth = w;
    viewHeight = h;
    waterEndYpx = viewHeight - waterPx;
}

onMeasure()可以更精细的控制布局参数。该方法参数是View.MeasureSpec值,用于告诉父视图希望子视图有多大,以及该大小是硬性最大值还是建议值。在实际开发中,我们可以根据以下代码得到specMode和specSize

    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

这里的specMode有三个值:

  1. MeasureSpec.UNSPECIFIED:The parent has not imposed any constraint
    on the child. It can be whatever size it wants;父视图没有对子视图添加任何约束,子视图可以任意大小。
  2. MeasureSpec.EXACTLY:The parent has determined an exact size
    for the child. The child is going to be given those bounds regardless
    of how big it wants to be;父视图决定子视图的确切大小,子视图被限定在给定的边界里,忽略本身想要的大小。
  3. MeasureSpec.AT_MOST:The child can be as large as it wants up
    to the specified size.;子最大视图可以达到指定的大小。

我们按照下面的代码模式,分别设置WaterView以及父视图layout_width来探讨onMeasue中获取到的specMode和specSize。这里的specMode和specSize按照下面的方式回去

    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <com.water.view.demo.WaterView
            android:id="@+id/waterView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:color="@color/colorPrimaryDark" />
    </LinearLayout>
</LinearLayout>

下图只展示更改layout_width的变化,layout_height变化和layout_width类似:

父视图-layout_widthWaterView-layout_widthWaterView-specModeWaterView-specSize
wrap_contentwrap_contentMeasureSpec.AT_MOST1080px
match_parentMeasureSpec.AT_MOST1080px
100dpMeasureSpec.EXACTLY300px
match_contentwrap_contentMeasureSpec.AT_MOST1080px
match_parentMeasureSpec.EXACTLY1080px
100dpMeasureSpec.EXACTLY300px
50dpwrap_contentMeasureSpec.AT_MOST150px
match_parentMeasureSpec.EXACTLY150px
100dpMeasureSpec.EXACTLY300px

那么在实际的开发中我们拿到specMode和specSize做什么用呢?更多是为setMeasuredDimension()使用,如果我们view的大小超过specSize。我们就可以根据specMode和specSize来进行裁剪、滚动、换行等操作,从而计算View的精确大小。

3.5 控制子View位置

对子View的位置进行控制主要是针对ViewGrop视图,如果添加了多个子View,那么我就需要功能以及测量的大小对子View的位置进行确定。这里需要重写onLayout方法,计算好子view的left、top、right、bottom后,调用view.layout()确定子view的位置。

3.6 自定义绘制

自定义视图最重要的部分是外观。绘制自定义视图可能很简单,也可能横复杂,具体取决于具体的需求。

要实现自定义视图的外观效果,我们需要重写onDraw()方法,利用Canvas进行绘制。

android.graphics 框架将绘制分为两个方面:

  • 需要绘制什么,有Canvas处理。
  • 如何绘制,由Paint处理。

例如,Canvas提供了drawLine()绘制线的方法,那么就需要Paint对象设置颜色,最后绘制出一条指定颜色的线条。

如果视图非常频繁地重新绘制,那么onDraw()会不停的执行,因此我们就不能再onDraw()里面做一些对象的创建,需要对象的创建提前到类的构造函数中,这样可以避免内存抖动带来的界面卡顿问题。我们可以按照以下方式进行对象的构建。

public class WaterView extends View {
    private Paint paintWater;
    private RectF rectF;
    
     public WaterView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init()
    }
    ......
    
    private void init() {
        paintWater = new Paint();
        paintWater.setColor(color);
        paintWater.setAntiAlias(true);
        rectF = new RectF();
    }
}    

最后在onDraw()中完成绘制

    @Override
    protected void onDraw(Canvas canvas) {
        rectF.set(0,viewHeight/2,viewWidth,viewHeight);
        canvas.drawRect(rectF,paintWater);
    }

参考:

官网参考文档

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值