【Android】自定义View / ViewGroup_android 自定义viewgroup-程序员宅基地

技术标签: android  kotlin  Android  

1. 自定义View

1.1 简介

我们自定义View的目的是为了针对我们的工程需要,完成一些内置View不能实现或者实现起来很麻烦的功能。其中我们需要复写onMeasure(), onLayout()以及onDraw()

接下来我们将通过自定义View实现类似于微信头像的效果。

在这里插入图片描述

首先我们需要继承View或者View的子类并完成构造函数。比如我们在这里自定义一个CustomImageView

// 主构造函数 第三个参数为默认Style
class CustomImageView(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) :
androidx.appcompat.widget.AppCompatImageView(context, attributeSet, defStyleAttr) {
    

  // 次构造函数 第二个参数为自定义属性集合,用与取出XML配置中的自定义属性
  constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

  // 次构造函数 参数为上下文对象
  constructor(context: Context) : this(context, null)
}

1.2 onMeasure

自定义View中有一项很重要的工作就是测量View的大小,这是由于我们在xml配置中不仅仅会使用具体的精确值,还有可能会使用match_parent以及wrap_content。其中,match_parent是设置子视图尺寸为占满父视图;wrap_content是设置子视图尺寸为包裹着自身内容。由于这两种设置没有设置具体的大小,因此我们需要在自定义的View中复写具体的逻辑。此外,若我们对View的形状有特别的要求,比如圆形或者正方形等,即使在配置中指定具体的数值也无法满足我们的要求,这种情况也需要复写逻辑。

我们接下来通过复写onMeasure方法将View变成一个正方形,方便后续在onDraw中画圆。

在复写方法之前我们还需要了解一个类MeasureSpec


MeasureSpec

MeasureSpec类非常简单,加上注释也不过一百多行。他的主要功能就是将View测量的数据通过一个整型保存下来,其中不光有尺寸信息还包括测量模式。主要思想就是一个Int有32个字节,将其前面两个字节用来保存三种测量模式,后面30位来保存尺寸信息。

// 用于将测量模式的左移操作
private static final int MODE_SHIFT = 30;
// 最高两位都是1的整型,方便取前2位或后30位的值
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
/**
 * 测量模式1: 父视图对子视图没有限制,子视图可以是任意大小
 */
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

/**
 * 测量模式2: 父视图对子视图有确定的大小要求
 */
public static final int EXACTLY     = 1 << MODE_SHIFT;

/**
 * 测量模式3: 子视图可以根据需要任意大,直到指定大小。
 */
public static final int AT_MOST     = 2 << MODE_SHIFT;

@MeasureSpecMode
public static int getMode(int measureSpec) {
    
  //noinspection ResourceType
  return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    
  return (measureSpec & ~MODE_MASK);
}

相应的存取方法也已经封装好了:

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1)       int size,@MeasureSpecMode int mode) {
    
  // sUseBrokenMakeMeasureSpec为true代表使用旧方法,与else的计算结果是一致的。
  if (sUseBrokenMakeMeasureSpec) {
    
    return size + mode;
  } else {
    
    return (size & ~MODE_MASK) | (mode & MODE_MASK);
  }
}


@MeasureSpecMode
public static int getMode(int measureSpec) {
    
  //noinspection ResourceType
  return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    
  return (measureSpec & ~MODE_MASK);
}

我们继续回归复写onMeasure方法上,由于我们想要一个正方形即长等于宽,因此我们需要将测量所得的长宽信息取出后设置为相同的值后再保存起来。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
  // 取长宽的最小值作为圆的半径,即此时仍为长方形的长宽
  radius = min(getSize(radius, widthMeasureSpec), getSize(radius, heightMeasureSpec))
  // 此函数只可以在onMeasure中使用,可以为View设置长宽
  setMeasuredDimension(radius, radius)
}

private fun getSize(defaultSize: Int, measureSpec: Int): Int {
    
  val measureMode = MeasureSpec.getMode(measureSpec)
  val measureSize = MeasureSpec.getSize(measureSpec)

  // 当测量模式为精确值或最大值模式时,我们取测量值,否则使用默认值
  return when (measureMode) {
    
    MeasureSpec.EXACTLY, MeasureSpec.AT_MOST -> measureSize
    MeasureSpec.UNSPECIFIED -> defaultSize
    else -> defaultSize
  }
}

1.3 onDraw

由于我们需要View能显示圆形的照片,而普通的ImageView是矩形的,因此我们必然要复写onDraw方法。

// 设置传输模式,传输模式定义了源像素与目标像素的组合规则。
// PorterDuff.Mode.SRC_IN 这种模式,两个绘制的效果叠加后取两者的交集部分并展现后图
// 此处为显示圆形图片的关键地方
private val mXfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)

override fun onDraw(canvas: Canvas?) {
    
  super.onDraw(canvas)
  // 将原背景透明度设为0
  background.alpha = 0
  canvas?.drawBitmap(createCircleBitmap(mProfilePhoto, radius), 0F, 0F, null)
}

/**
 * 显示圆形图片的关键方法
 * src: 需要显示的图片,需转成位图
 * radius: 圆形的半径
 */
private fun createCircleBitmap(src: Bitmap, radius: Int) : Bitmap {
    
  mPaint.reset()
  // createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter)
  // 可以根据原始位图以及给定的长宽相等的位图,若原图满足要求则返回原图
  // src 原图
  // dstWidth 目标宽度
  // dstHeight 目标长度
  // filter 缩放位图时是否使用过滤,过滤会损失部分性能并显著提高图像质量
  val srcScaled = Bitmap.createScaledBitmap(src, radius, radius, true)
  
  // createBitmap(@Nullable DisplayMetrics display, int width,
  //        int height, @NonNull Config config)
  // 生成指定宽度和高度的位图
  // config: 创建位图的设置  Bitmap.Config.ARGB_8888:可以提供最好的位图质量
  val target = Bitmap.createBitmap(radius, radius, Bitmap.Config.ARGB_8888)
  // 指定画布要绘制的位图
  mCanvas.setBitmap(target)
  // 画圆
  mCanvas.drawCircle(radius / 2F, radius / 2F, radius / 2F, mPaint)
  
  mPaint.xfermode = mXfermode
  // 由于我们使用PorterDuff.Mode.SRC_IN 这种模式,并且先绘制了一个圆形
  // 因此我们再绘制位图就会显示成圆形的图片
  mCanvas.drawBitmap(srcScaled, 0F, 0F, mPaint)
  return target
}

1.4 AttributeSet

一个合格的自定义View应该也要允许用户自定义一些属性值,只有当用户不指定时,我们才使用我们的默认值。首先我们需要在res/values/styles.xml文件中(若无文件,则需要新建)声明我们的自定义属性。格式如下:

<resources>
    <!--  声明属性集合的名称,官方建议与类名相同  -->
    <declare-styleable name="CustomImageView">
        <!--  声明半径属性,名称为radius,格式为尺寸类型(dp等)  -->
        <attr name="radius" format="dimension" />
        <!--  声明引用图片,名称为resID,格式为引用类型  -->
        <attr name="resID" format="reference" />
    </declare-styleable>
</resources>

随后即可在布局文件中使用自定义属性:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:custom="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#dd78dd98"
        tools:context=".MainActivity">

    <com.example.customviewdemo.CustomVerticalLinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/black">

        <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/test_button"
                android:textSize="25sp"
                android:textAllCaps="false" />

        <com.example.customviewdemo.CustomImageView
                android:layout_width="200dp"
                android:layout_height="200dp"
                custom:radius="300dp"
                custom:resID="@drawable/profile" />

        <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/test_button"
                android:textSize="40sp"
                android:textAllCaps="false" />

    </com.example.customviewdemo.CustomVerticalLinearLayout>
</LinearLayout>

CustomVerticalLinearLayout是我们后续会用到的自定义ViewGroup,可以先不用管。而自定义属性custom:radius="300dp"中的custom是命名空间,需要在父容器指定,可以为任意值,如这里的xmlns:custom="http://schemas.android.com/apk/res-auto",其中"http://schemas.android.com/apk/res-auto"是固定格式。


最后我们需要在构造函数中取出相应值。

init {
    
  // 取得属性值数组
  val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.CustomImageView)

  // 获取尺寸属性
  radius = typedArray.getDimensionPixelSize(R.styleable.CustomImageView_radius, 200)

  // 获得引用属性
  mProfilePhoto = BitmapFactory.decodeResource(resources                                               , typedArray.getResourceId(R.styleable.CustomImageView_resID, R.drawable.profile))
  
  // 最后需要回收数组
  typedArray.recycle()
}

Demo代码

总体代码如下,布局文件以及style.xml已经在上文中给出。

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
        super.onCreate(savedInstanceState)
        val viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)
    }
}

class CustomImageView(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) :
    androidx.appcompat.widget.AppCompatImageView(context, attributeSet, defStyleAttr) {
    

    constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

    constructor(context: Context) : this(context, null)

    private val mPaint = Paint()
    private var mProfilePhoto = BitmapFactory.decodeResource(resources, R.drawable.profile)
    private val mCanvas = Canvas()
    private var radius = 200

    init {
    
        val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.CustomImageView)

        radius = typedArray.getDimensionPixelSize(R.styleable.CustomImageView_radius, 200)

        mProfilePhoto = BitmapFactory.decodeResource(resources
            , typedArray.getResourceId(R.styleable.CustomImageView_resID, R.drawable.profile))
        typedArray.recycle()
    }

    companion object {
    
        const val TAG = "CustomImageView"
        private val mXfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        radius = min(getSize(radius, widthMeasureSpec), getSize(radius, heightMeasureSpec))
        setMeasuredDimension(radius, radius)
    }

    override fun onDraw(canvas: Canvas?) {
    
        super.onDraw(canvas)
        background.alpha = 0
        canvas?.drawBitmap(createCircleBitmap(mProfilePhoto, radius), 0F, 0F, null)
    }

    private fun createCircleBitmap(src: Bitmap, radius: Int) : Bitmap {
    
        mPaint.reset()
        val srcScaled = Bitmap.createScaledBitmap(src, radius, radius, true)
        val target = Bitmap.createBitmap(radius, radius, Bitmap.Config.ARGB_8888)
        mCanvas.setBitmap(target)
        mCanvas.drawCircle(radius / 2F, radius / 2F, radius / 2F, mPaint)
        mPaint.xfermode = mXfermode
        mCanvas.drawBitmap(srcScaled, 0F, 0F, mPaint)
        return target
    }


    private fun getSize(defaultSize: Int, measureSpec: Int): Int {
    
        val measureMode = MeasureSpec.getMode(measureSpec)
        val measureSize = MeasureSpec.getSize(measureSpec)

        return when (measureMode) {
    
            MeasureSpec.EXACTLY, MeasureSpec.AT_MOST -> measureSize
            MeasureSpec.UNSPECIFIED -> defaultSize
            else -> defaultSize
        }
    }
}

2. 自定义ViewGroup

2.1 简介

自定义ViewGroup相比于自定义View会更加复杂一点,因为它不仅涉及到自身的测量,摆放以及绘制,还需要安排好子元素的测量,摆放以及绘制。但是ViewGroup本质上还是一个View,它继承自View,因此它也只需像自定义View一样重写onMeasure(), onLayout()以及onDraw()即可。

接下来我们通过自定义ViewGroup实现类似LinearLayout的效果

在这里插入图片描述

2.2 onMeasure()

由于我们的ViewGroup是一个容器,因此我们在计算尺寸的时候理所当然需要知道所有子视图的大小。在这里我们的自定义ViewGroup是模仿垂直布局的LinearLayout。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
  // 测量所有子元视图
  measureChildren(widthMeasureSpec, heightMeasureSpec)

  val width = MeasureSpec.getSize(widthMeasureSpec)
  val widthMode = MeasureSpec.getMode(widthMeasureSpec)
  val height = MeasureSpec.getSize(heightMeasureSpec)
  val heightMode = MeasureSpec.getMode(heightMeasureSpec)

  if (childCount == 0) {
    
    // 无子视图,没有必要展示
    setMeasuredDimension(0, 0)
  } else {
    
    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
    
      // 对子视图的宽高都无限制,则高度为所有子视图的高度之和,宽度为子视图中的最大宽度
      setMeasuredDimension(children.maxOf {
     it.measuredWidth }, children.sumOf {
     it.measuredHeight })
    } else if (widthMode == MeasureSpec.AT_MOST) {
    
      // 对子视图宽度无限制,则高度为自身的测量值,宽度为子视图中的最大宽度
      setMeasuredDimension(children.maxOf {
     it.measuredWidth }, height)
    } else if (heightMode == MeasureSpec.AT_MOST) {
    
      // 对子视图高度无限制,则高度为所有子视图的高度之和,宽度为自身的测量值
      setMeasuredDimension(width, children.sumOf {
     it.measuredHeight })
    }
  }
}

2.3 onLayout()

onLayout()只需按照顺序依次摆放每个子视图即可,其中子视图的上下位置位置可以通过上方所有子视图的累加高度以及自身高度计算得到;左右位置可以通过子元素自身宽度计算得到。

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    
  // 子视图的起始高度
  var curHeight = 0
  for (child in children) {
    
    // 摆放当前子视图的位置
    child.layout(l, t+curHeight, l+child.measuredWidth, t+curHeight+child.measuredHeight)
    // 累加子视图高度
    curHeight += child.measuredHeight
  }
}

2.4 Demo代码

总体代码如下,其余代码已经在上文中给出。

class CustomVerticalLinearLayout(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int, defStyleRes: Int)
: ViewGroup(context, attributeSet, defStyleAttr, defStyleRes) {
    

  constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int)
  : this(context, attributeSet, defStyleAttr, 0)

  constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

  constructor(context: Context) : this(context, null)

  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    
    var curHeight = 0
    for (child in children) {
    
      child.layout(l, t+curHeight, l+child.measuredWidth, t+curHeight+child.measuredHeight)
      curHeight += child.measuredHeight
    }
  }

  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    measureChildren(widthMeasureSpec, heightMeasureSpec)

    val width = MeasureSpec.getSize(widthMeasureSpec)
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val height = MeasureSpec.getSize(heightMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)

    if (childCount == 0) {
    
      setMeasuredDimension(0, 0)
    } else {
    
      if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
    
        setMeasuredDimension(children.maxOf {
     it.measuredWidth }, children.sumOf {
     it.measuredHeight })
      } else if (widthMode == MeasureSpec.AT_MOST) {
    
        setMeasuredDimension(children.maxOf {
     it.measuredWidth }, height)
      } else if (heightMode == MeasureSpec.AT_MOST) {
    
        setMeasuredDimension(width, children.sumOf {
     it.measuredHeight })
      }
    }
  }
}

相关文章

  1. 【Android】Handler机制详解
  2. 【Android】动画简介
  3. 【Android】事件分发详解
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/helloworld370629/article/details/128123226

智能推荐

leetcode 172. 阶乘后的零-程序员宅基地

文章浏览阅读63次。题目给定一个整数 n,返回 n! 结果尾数中零的数量。解题思路每个0都是由2 * 5得来的,相当于要求n!分解成质因子后2 * 5的数目,由于n中2的数目肯定是要大于5的数目,所以我们只需要求出n!中5的数目。C++代码class Solution {public: int trailingZeroes(int n) { ...

Day15-【Java SE进阶】IO流(一):File、IO流概述、File文件对象的创建、字节输入输出流FileInputStream FileoutputStream、释放资源。_outputstream释放-程序员宅基地

文章浏览阅读992次,点赞27次,收藏15次。UTF-8是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。文件字节输入流:每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1。注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码。定义一个与文件一样大的字节数组,一次性读取完文件的全部字节。UTF-8字符集:汉字占3个字节,英文、数字占1个字节。GBK字符集:汉字占2个字节,英文、数字占1个字节。GBK规定:汉字的第一个字节的第一位必须是1。_outputstream释放

jeecgboot重新登录_jeecg 登录自动退出-程序员宅基地

文章浏览阅读1.8k次,点赞3次,收藏3次。解决jeecgboot每次登录进去都会弹出请重新登录问题,在utils文件下找到request.js文件注释这段代码即可_jeecg 登录自动退出

数据中心供配电系统负荷计算实例分析-程序员宅基地

文章浏览阅读3.4k次。我国目前普遍采用需要系数法和二项式系数法确定用电设备的负荷,其中需要系数法是国际上普遍采用的确定计算负荷的方法,最为简便;而二项式系数法在确定设备台数较少且各台设备容量差..._数据中心用电负荷统计变压器

HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板_网页设计成品百度网盘-程序员宅基地

文章浏览阅读7k次,点赞4次,收藏46次。HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 明星、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 军事、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他 等网页设计题目, A+水平作业_网页设计成品百度网盘

【Jailhouse 文章】Look Mum, no VM Exits_jailhouse sr-iov-程序员宅基地

文章浏览阅读392次。jailhouse 文章翻译,Look Mum, no VM Exits!_jailhouse sr-iov

随便推点

chatgpt赋能python:Python怎么删除文件中的某一行_python 删除文件特定几行-程序员宅基地

文章浏览阅读751次。本文由chatgpt生成,文章没有在chatgpt生成的基础上进行任何的修改。以上只是chatgpt能力的冰山一角。作为通用的Aigc大模型,只是展现它原本的实力。对于颠覆工作方式的ChatGPT,应该选择拥抱而不是抗拒,未来属于“会用”AI的人。AI职场汇报智能办公文案写作效率提升教程 专注于AI+职场+办公方向。下图是课程的整体大纲下图是AI职场汇报智能办公文案写作效率提升教程中用到的ai工具。_python 删除文件特定几行

Java过滤特殊字符的正则表达式_java正则表达式过滤特殊字符-程序员宅基地

文章浏览阅读2.1k次。【代码】Java过滤特殊字符的正则表达式。_java正则表达式过滤特殊字符

CSS中设置背景的7个属性及简写background注意点_background设置背景图片-程序员宅基地

文章浏览阅读5.7k次,点赞4次,收藏17次。css中背景的设置至关重要,也是一个难点,因为属性众多,对应的属性值也比较多,这里详细的列举了背景相关的7个属性及对应的属性值,并附上演示代码,后期要用的话,可以随时查看,那我们坐稳开车了······1: background-color 设置背景颜色2:background-image来设置背景图片- 语法:background-image:url(相对路径);-可以同时为一个元素指定背景颜色和背景图片,这样背景颜色将会作为背景图片的底色,一般情况下设置背景..._background设置背景图片

Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程-程序员宅基地

文章浏览阅读2.6k次,点赞2次,收藏8次。Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程

PyCharm2021安装教程-程序员宅基地

文章浏览阅读10w+次,点赞653次,收藏3k次。Windows安装pycharm教程新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入下载安装PyCharm1、进入官网PyCharm的下载地址:http://www.jetbrains.com/pycharm/downl_pycharm2021

《跨境电商——速卖通搜索排名规则解析与SEO技术》一一1.1 初识速卖通的搜索引擎...-程序员宅基地

文章浏览阅读835次。本节书摘来自异步社区出版社《跨境电商——速卖通搜索排名规则解析与SEO技术》一书中的第1章,第1.1节,作者: 冯晓宁,更多章节内容可以访问云栖社区“异步社区”公众号查看。1.1 初识速卖通的搜索引擎1.1.1 初识速卖通搜索作为速卖通卖家都应该知道,速卖通经常被视为“国际版的淘宝”。那么请想一下,普通消费者在淘宝网上购买商品的时候,他的行为应该..._跨境电商 速卖通搜索排名规则解析与seo技术 pdf

推荐文章

热门文章

相关标签