前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android自定义View-入门(明白自定义View和自定义ViewGroup)

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

作者头像
Petterp
发布2022-02-09 12:07:56
1.2K0
发布2022-02-09 12:07:56
举报
文章被收录于专栏:JetPack

自定义View

为什么要自定义View? 主要是Andorid系统内置的View 无法实现我们的 需求,我们需要针对我们的业务需求定制我们想要的 View.自定义View 我们大部分时候只需重写两个函数: onMeasure(),onDraw(). onMeasure()负责对当前View 的尺寸进行测量,onDraw负责把当前这个View绘制出来,当然了,还需要写构造函数。

代码语言:javascript
复制
public Views(Context context) {
    super(context);
}

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

onMeasure 概念

代码语言:javascript
复制
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

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

为什么一个 数里面能放两个信息呢?我们知道,我们在设置宽高时有三个选择,wrap_content,match_partent以及 指定固定尺寸,而测量模式也有三种:UNSPECIFIED,EXACTLY,AT_MOST,但它们并不是一一对应的关系。

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

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

代码语言:javascript
复制
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

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

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

重写 onMeasure 函数Demo

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

代码语言:javascript
复制
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

代码语言:javascript
复制
<com.petterp.studybook.View.Views
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:background="#000"/>

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

重写onDraw

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

代码语言:javascript
复制
@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 文件(如果没有就需要新建),里面声明一个我们自定义的属性:

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

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

代码语言:javascript
复制
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();
}

最后附上完整的代码

代码语言:javascript
复制
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按从上到下垂直顺序一个接一个摆放。

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制
<?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>

更多Android开发知识请访问—— Android开发日常笔记,欢迎Star,你的小小点赞,是对我的莫大鼓励。

参照博客:非常感谢https://www.jianshu.com/p/c84693096e41

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 自定义View
    • onMeasure 概念
      • 重写 onMeasure 函数Demo
        • 重写onDraw
          • 自定义布局属性
            • 自定义ViewGroup
              • 实例Demo
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档