Android自定义View-入门(明白自定义View和自定义ViewGroup)

简介: 为什么要自定义View? 主要是Andorid系统内置的View 无法实现我们的 需求,我们需要针对我们的业务需求定制我们想要的 View.

自定义View

为什么要自定义View? 主要是Andorid系统内置的View 无法实现我们的 需求,我们需要针对我们的业务需求定制我们想要的 View.自定义View 我们大部分时候只需重写两个函数:

 onMeasure(),onDraw(). onMeasure()负责对当前View 的尺寸进行测量,onDraw负责把当前这个View绘制出来,当然了,还需要写构造函数。

public Views(Context context) {
    super(context);
}
public Views(Context context,  AttributeSet attrs) {
    super(context, attrs);
}

onMeasure 概念

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

主要用来测量布局,其参数widthMeasureSpecheightMeasureSpec包含宽和高的信息和测量模式。

为什么一个 数里面能放两个信息呢?我们知道,我们在设置宽高时有三个选择,

wrap_content,match_partent以及 指定固定尺寸,而测量模式也有三种:UNSPECIFIED,EXACTLY,AT_MOST,但它们并不是一一对应的关系。

那么google 是如何做到把一个 int同时放测量模式 和尺寸信息呢?我们知道 int型数据占用 32个bit,而google实现的是,将 int数据的前面2个 bit用于区分不同的布局模式,后面 30个bit 存放的是尺寸的数据。

如何提取测量模式与尺寸呢?

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

image.png

上面的测量模式和我们布局时的 warp_content,match_parent以及写成固定的尺寸有什么对应关系呢?

match_parent--->EXACTLY。怎么理解呢?match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。
warp_parent---> AT_MOST
我们想要将大小设置为包裹我们的View内容,那么尺寸大小就是父View给我作为参考的尺寸,至于不超过这个尺寸就可以啦。具体尺寸就根据我们的需求去设定。
固定尺寸(100dp)--->EXACTLY.用户自己制定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主。

重写 onMeasure 函数Demo

我们要实现的效果是一个小正方形。

private int getMySize(int defaultSize,int measureSpec){
    int mySize=defaultSize;
    //取测量模式
    int mode=MeasureSpec.getMode(measureSpec);
    //取测量长度
    int size=MeasureSpec.getSize(measureSpec);
    switch (mode){
        //如果没有指定大小,就设置为默认大小
        case MeasureSpec.UNSPECIFIED:
            mySize=defaultSize;
            break;
            //如果测量模式是最大取值size
            //我们将大小取最大值,你也可以取其他值
        case MeasureSpec.AT_MOST:
            mySize=size;
            break;
            //如果是固定的大小,那就不要去改变它
        case MeasureSpec.EXACTLY:
            mySize=size;
            break;
    }
    return mySize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width=getMySize(100,widthMeasureSpec);
    int heigth=getMySize(100,heightMeasureSpec);
    if (width<heigth){
        heigth=width;
    }else{
        width=heigth;
    }
    setMeasuredDimension(width,heigth);
}

xml

<com.petterp.studybook.View.Views
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:background="#000"/>

这样的效果就是一个正方形了。

重写onDraw

上面我们通过重写 onMeasure 实现了布局的测量与设定,接下来就是绘制了。绘制的话 我们直接在画板 Canvas 对象上绘制就好。

我们以一个简单Demo来实现效果。

@Override
protected void onDraw(Canvas canvas) {
    //调用父View的onDraw函数,因为View这个类帮我们实现了一些
    //基本的绘制功能,拨入绘制背景颜色,背景图片等。
    super.onDraw(canvas);
    //也可以是 getMeasuredHieght()/2,因为这个例子中我们已将宽高设置相等了
    int r=getMeasuredWidth()/2;
    //圆心的横坐标为当前View的View左边起始位置+半径
    int centerx=getLeft()+r;
    //圆心的纵坐标表为当前的View的顶部起始位置+半径
    int centery =getTop()+r;
    //设置画笔
    @SuppressLint("DrawAllocation") Paint paint=new Paint();
    //设置画笔颜色
    paint.setColor(Color.GREEN);
    //开始绘制
    canvas.drawCircle(centerx,centery,r,paint);
}

效果出来就是一个小圆


自定义布局属性

如果有些属性我们希望由用户指定,只有当用户不指定的时候采用我们硬编码的值,比如上面的默认尺寸,我们想要由用户自己在布局文件里面指定该怎么做呢?所以这个时候就需要我们自定属性,让用户用我们定义的属性。


过程

首先我们需要在 res/values/styles.xml 文件(如果没有就需要新建),里面声明一个我们自定义的属性:

<!--name为声明的“属性集合”名,可以随便取,但是最好是设置为根我们的view一样的名称-->
<declare-styleable name="Views">
    <!--声明我们的属性,名称为 default_size,取值类型为尺寸(dp,dx等)-->
    <attr name="default_size" format="dimension"/>
</declare-styleable>

然后在我们自定义View里面吧我们自定义的属性值取出来,在构造函数中,有个AttributeSet的属性,我们需要用它来帮我们把布局里面的属性取出来。

private int defaultSize;
public Views(Context context,  AttributeSet attrs) {
    super(context, attrs);
    //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
    //即属性集合的标签,在R 文件中名称为 R,styleable+name
    TypedArray a=context.obtainStyledAttributes(attrs, R.styleable.Views);
    //第一个参数为属性几个里面的属性,R文件名称:R。styleable+属性集合名称+下划线+属性名称
    //第二个参数为,如果没有设置这个属性,则设置的默认的值
    defaultSize=a.getDimensionPixelSize(R.styleable.Views_default_size,100);
    //最后记得将TypedArray对象回收
    a.recycle();
}

最后附上完整的代码

package com.petterp.studybook.View;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import com.petterp.studybook.R;
/**
 * @author Petterp on 2019/6/27
 * Summary:
 * 邮箱:1509492795@qq.com
 */
public class Views extends View {
    public Views(Context context) {
        super(context);
    }
    private int defaultSize;
    public Views(Context context,  AttributeSet attrs) {
        super(context, attrs);
        //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
        //即属性集合的标签,在R 文件中名称为 R,styleable+name
        TypedArray a=context.obtainStyledAttributes(attrs, R.styleable.Views);
        //第一个参数为属性几个里面的属性,R文件名称:R。styleable+属性集合名称+下划线+属性名称
        //第二个参数为,如果没有设置这个属性,则设置的默认的值
        defaultSize=a.getDimensionPixelSize(R.styleable.Views_default_size,100);
        //最后记得将TypedArray对象回收
        a.recycle();
    }
    private int getMySize(int defaultSize,int measureSpec){
        int mySize=defaultSize;
        //取测量模式
        int mode=MeasureSpec.getMode(measureSpec);
        //取测量长度
        int size=MeasureSpec.getSize(measureSpec);
        switch (mode){
            //如果没有指定大小,就设置为默认大小
            case MeasureSpec.UNSPECIFIED:
                mySize=defaultSize;
                break;
                //如果测量模式是最大取值size
                //我们将大小取最大值,你也可以取其他值
            case MeasureSpec.AT_MOST:
                mySize=size;
                break;
                //如果是固定的大小,那就不要去改变它
            case MeasureSpec.EXACTLY:
                mySize=size;
                break;
        }
        return mySize;
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width=getMySize(defaultSize,widthMeasureSpec);
        int heigth=getMySize(defaultSize,heightMeasureSpec);
        if (width<heigth){
            heigth=width;
        }else{
            width=heigth;
        }
        setMeasuredDimension(width,heigth);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        //调用父View的onDraw函数,因为View这个类帮我们实现了一些
        //基本的绘制功能,拨入绘制背景颜色,背景图片等。
        super.onDraw(canvas);
        //也可以是 getMeasuredHieght()/2,因为这个例子中我们已将宽高设置相等了
        int r=getMeasuredWidth()/2;
        //圆心的横坐标为当前View的View左边起始位置+半径
        int centerx=getLeft()+r;
        //圆心的纵坐标表为当前的View的顶部起始位置+半径
        int centery =getTop()+r;
        //设置画笔
        @SuppressLint("DrawAllocation") Paint paint=new Paint();
        //设置画笔颜色
        paint.setColor(Color.GREEN);
        //开始绘制
        canvas.drawCircle(centerx,centery,r,paint);
    }
}

自定义ViewGroup

自定义View的过程简单,其实也就那几步,可自定义ViewGroup 可就比较麻烦了,因为不仅要管好自己,还要兼顾子View。因为 ViewGroup是一个容器,他装纳 子视图 并且负责把 子视图 放入指定的位置。

一般自定义viewgroup我们从这几方面去思考:

  1. 首先,我们需要知道各个子 view 的大小,只有知道子 view 的大小,我们才知道当前的View Group该设置为多大去容纳它们。
  2. 根据子 View 的大小,以及我们的 ViewGroup 要实现的功能,决定出ViewGroup的大小。
  3. ViewGroup 和 子View 的大小算出来之后,接下来就需要去摆放,具体的排放规则,一般按照我们的需求去定制。
  4. 知道了怎么摆放之后,还需要决定每个子view对号入座,把他们放进它们该放的地方。

实例Demo

我们仿照LinearLayout的垂直布局,将子view按从上到下垂直顺序一个接一个摆放。

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }
    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    /**
     * 切记,这里都是相对于父view
     * @param changed
     * @param l //相对于父view left距离
     * @param t //相对于父view top距离
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //top重置为0
        int curHeight = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            //真正绘制的方法,其内部又有onlayout的调用,因为子view也是一个viewgroup
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //将所有的子view进行测量,这会触发每个子view的 onMeasure函数
        //注意要与 measureChild 区分,measureChild 是对单个view进行测量
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        //测量模式
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        //测量大小
        int widthSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);
        //获取子元素数量
        int childCount=getChildCount();
        //如果没有子view,当前viewGroup没有存在的意义,不用占用空间
        if (childCount==0){
            setMeasuredDimension(0,0);
        }else{
            //如果宽高都是包裹内容
            if (widthMode==MeasureSpec.AT_MOST&&heightMode==MeasureSpec.AT_MOST){
                //我们将高度设置为所有子view的高度相加,宽度设置为子view中最大的宽度
                int height=getTotleHeight();
                int width=getMaxChildWidth();
                setMeasuredDimension(width,height);
            }else if (heightMode==MeasureSpec.AT_MOST){
                //如果只有高度是warp
                //宽度设置为ViewGroup自己的测量宽度,高度设置为所有view的高度总和
                setMeasuredDimension(widthSize,getTotleHeight());
            }else if (widthMode==MeasureSpec.AT_MOST){
                //宽度设置为子View中宽度最大的值,高度设置为 ViewGroup自己测量的值
                setMeasuredDimension(getMaxChildWidth(),heightSize);
            }
        }
    }
    /**
     * 获取子view中宽度最大的值
     * @return
     */
    private int getMaxChildWidth(){
        int childCount=getChildCount();
        int maxWidth=0;
        for (int i=0;i<childCount;i++){
            //获取相应的子view
            View  childView =getChildAt(i);
            //对比出最宽的子view -> 因为是垂直布局
            if (childView.getMeasuredWidth()>maxWidth){
                maxWidth=childView.getMeasuredWidth();
            }
        }
        return maxWidth;
    }
    /**
     * 将子view的高度相加
     * @return
     */
    private int getTotleHeight(){
        int childCount=getChildCount();
        int height=0;
        for (int i=0;i<childCount;i++){
            View childView=getChildAt(i);
            height+=childView.getMeasuredHeight();
        }
        return height;
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_height="match_parent"
    tools:context=".View.ViewActivity">
    <com.petterp.studybook.View.MyViewGroup
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:text="1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <Button
            android:text="2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <Button
            android:text="3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </com.petterp.studybook.View.MyViewGroup>
</LinearLayout>

目录
相关文章
|
1月前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
1月前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
32 5
|
2月前
|
缓存 数据处理 Android开发
在 Android 中使用 RxJava 更新 View
【10月更文挑战第20天】使用 RxJava 来更新 View 可以提供更优雅、更高效的解决方案。通过合理地运用操作符和订阅机制,我们能够轻松地处理异步数据并在主线程中进行 View 的更新。在实际应用中,需要根据具体情况进行灵活运用,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在 Android 中使用 RxJava 更新 View 的技巧和方法,为开发高质量的 Android 应用提供有力支持。
|
2月前
|
缓存 调度 Android开发
Android 在子线程更新 View
【10月更文挑战第21天】在 Android 开发中,虽然不能直接在子线程更新 View,但通过使用 Handler、AsyncTask 或 RxJava 等方法,可以实现子线程操作并在主线程更新 View 的目的。在实际应用中,需要根据具体情况选择合适的方法,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在子线程更新 View 的技巧和方法,为开发高质量的 Android 应用提供支持。
35 2
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
2月前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
31 1
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
26 2
|
1月前
|
XML IDE Java
安卓应用开发入门:从零开始的旅程
【10月更文挑战第23天】本文将带领读者开启一段安卓应用开发的奇妙之旅。我们将从最基础的概念讲起,逐步深入到开发实践,最后通过一个简易的代码示例,展示如何将理论知识转化为实际的应用。无论你是编程新手,还是希望扩展技能的软件工程师,这篇文章都将为你提供有价值的指导和启发。
30 0
|
2月前
|
Web App开发 编解码 视频直播
视频直播技术干货(十二):从入门到放弃,快速学习Android端直播技术
本文详细介绍了Android端直播技术的全貌,涵盖了从实时音视频采集、编码、传输到解码与播放的各个环节。文章还探讨了直播中音视频同步、编解码器选择、传输协议以及直播延迟优化等关键问题。希望本文能为你提供有关Andriod端直播技术的深入理解和实践指导。
53 0
|
2月前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
43 3