`

Android自定义View

 
阅读更多

为什么我们觉得自定义View是学习Android的一道坎?
为什么那么多Android大神却认为自定义View又是如此的简单?
为什么google随便定义一个View都是上千行的代码?
以上这些问题,相信学Android的同学或多或少都有过这样的疑问。
那么,看完此文,希望对你们的疑惑有所帮助。

回到主题,自定义View ,需要掌握的几个点是什么呢?
我们先把自定义View细分一下,分为两种
1) 自定义ViewGroup
2) 自定义View

其实ViewGroup最终还是继承之View,当然它内部做了许多操作;继承之ViewGroup的View我们一般称之为容器,而今天我们不讲这方面,后续有机会再讲。
来看看自定义View 需要掌握的几点,主要就是两点

一、重写 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法。
二、重写 protected void onDraw(Canvas canvas) {}方法

空讲理论很难理解,我们还得用例子来说明

我们可以把View理解为一张白纸,而自定义View就是在这张白纸上画上我们自己绘制的图案,可以在绘制任何图案,也可以在白纸的任何位置绘制,那么问题来了,白纸哪里来?图案哪里来?位置如何计算?

a)白纸好说,只要我们继承之View,在onDraw(Canvas canvas)中的canvas就是我们所说的白纸

public class CustomView extends View {
    public CustomView(Context context) {
        super(context);
    }

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

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // canvas 即为白纸
        super.onDraw(canvas);
    }
}

 

b)图案呢?这里的图案就是有图片和文字组成,这个也好说,定义一个Bitmap 成员变量,和一个String的成员变量

private Bitmap mBitmap ;
private String mName ;
mName = "这里直接赋值";
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher) ;

 

图片可以通过资源文件可以拿到。

c)计算位置
所以最核心的也是我们认为最麻烦的地方就是计算绘制的位置,计算位置就得先测量自身的大小,也就是我们必须掌握的两点中的第一点:需要重写 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法
先来看一下google写的TextView的onMeasure()方法是如何实现的

 

哇!好长!而且方法中还嵌套方法,如果真要算下来,代码量不会低于500行,看到这么多代码,头都大了,我想这也是我们为什么在学习Android自定义View的时候觉得如此困难的原因。大多数情况下,因为我们是自定义的View,可以说是根据我们的需求定制的View,所以很多里面的功能我们完全没必要,只需要几十行代码就能搞定。看到几十行代码就能搞定,感觉顿时信心倍增(^.^)
在重写这个方法之前,得先了解一个类 MeasureSpec ,如果不了解,没关系,下面就一起来了解一下这个类。先把代码贴出来,膜拜一下

  

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

 

这里我把里面一些我认为没必要的代码都去掉了,只留了以上几行代码,这样看起来很清晰,也非常容易 理解。 我们先做个转化,把上面几个成员变量转化成二进制 这个就不需要转化了,这里代表的只是一个移动的位置,也就是一个单纯的数字
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 1100 0000 0000 0000 0000 0000 0000 0000;
// 0x3 就是 0b11 左移30位 ,就是补30个0;
public static final int UNSPECIFIED = 0000 0000 0000 0000 0000 0000 0000 0000;
// 0b00 左移30位 
public static final int EXACTLY = 0100 0000 0000 0000 0000 0000 0000 0000 ; 
//0b01 左移30位
public static final int AT_MOST = 1000 0000 0000 0000 0000 0000 0000 0000 ;
//0b10 左移30位

 

你就会问了,这样写有什么好处呢? 细心的人看了上面这几个方法就明白了,每个方法中都有一个 & 的操作,所以我们接下来看看这集几个方法的含义是什么,先从下往上看,先易后难
1、 public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } 顾名思义,通过measureSpec这个参数,获取size ,两个都是int类型,怎么通过一个int类型的数 获取另一个int类型的数。我们在学习java的时候知道,一个int类型是32位,任何int类型的数都是有 32位,比如一个int类型的数值3,它也是占有32位,只是高30位全部为0。google 也是利用这一点, 让这个int类型的measureSpec数存了两个信息,一个就是size,保存在int类型的低30位,另一个就 是mode,保存在int类型的高2位。前面我们看到了有几个成员变量,UNSPECIFIED,EXACTLY,AT_MOST 者就是mode的三种选择,目前也只有这三种选择,所以只需要2位就能实现。
2、 ` public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); }` 这也好理解,获取模式,但这些模式有啥用处呢?
1)、EXACTLY 模式: 准确的、精确的;这种模式,是最容易理解和处理的,可以理解为大小固定, 比如在定义layout_width的时候,定义为固定大小 10dp,20dp,或者match_parent(此时父控件是 固定的)这时候,获取出来的mode就是EXACTLY
2)、AT_MOST 模式: 最大的;这种模式稍微难处理些,不过也好理解,就是View的大小最大不能超 过父控件,超过了,取父控件的大小,没有,则取自身大小,这种情况一般都是在layout_width设为 warp_content时。
3)、UNSPECIFIED 模式:不指定大小,这种情况,我们几乎用不上,它是什么意思呢,就是View的 大小想要多大,就给多大,不受父View的限制,几个例子就好理解了,ScrollView控件就是。
3、
         public static int makeMeasureSpec(int size, int mode) {
              if (sUseBrokenMakeMeasureSpec) {
                  return size + mode;
              } else {
                  return (size & ~MODE_MASK) | (mode & MODE_MASK);
              }
          }
 
这个方法也好理解,封装measureSpec的值,在定义一个View的大小时,我们只是固定了大小,你
下次想要获取mode的时候,肯定无法拿到,所以就得自己把模式添加进去,这个方法,在自定义View中
,也基本不需要用到,他所使用的场所,是在设置子View的大小的时候需要用到,所以如果是自定义
ViewGroup的话,就需要用到。 感觉讲了这么多,还是不知道怎么使用,接下来就来重写onMeasure()方法,写完之后,你就明白
了。

 

  

@Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     //这里方法套路都是一样,不管三七 二十一,上来就先把mode 和 size 获取出来。

      int widthMode = MeasureSpec.getMode(widthMeasureSpec);
      int heightMode = MeasureSpec.getMode(heightMeasureSpec);
      int widthSize = MeasureSpec.getSize(widthMeasureSpec);
      int heightSize = MeasureSpec.getSize(heightMeasureSpec);
      //View 真正需要显示的大小
      int width = 0, height = 0;
      //这里是去测量字体大小
      measureText();
      //字体宽度加图片宽度取最大宽度,这里因为字体和图片是上下排列
      int contentWidth = Math.max(mBoundText.width(), mIconNormal.getWidth());
     // 我们渴望得到的宽度
      int desiredWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
      //重点来了,判断模式,这个模式哪里来的呢,就是在编写xml的时候,设置的layout_width
      switch (widthMode) {
      //如果是AT_MOST,不能超过父View的宽度
          case MeasureSpec.AT_MOST:
              width = Math.min(widthSize, desiredWidth);
              break;
              //如果是精确的,好说,是多少,就给多少;
          case MeasureSpec.EXACTLY:
              width = widthSize;
              break;
              //这种情况,纯属在这里打酱油的,可以不考虑
          case MeasureSpec.UNSPECIFIED://我是路过的
              width = desiredWidth;
              break;
      }
      int contentHeight = mBoundText.height() + mIconNormal.getHeight();
      int desiredHeight = getPaddingTop() + getPaddingBottom() + contentHeight;
      switch (heightMode) {
          case MeasureSpec.AT_MOST:
              height = Math.min(heightSize, desiredHeight);
              break;
          case MeasureSpec.EXACTLY:
              height = heightSize;
              break;
          case MeasureSpec.UNSPECIFIED:
              height = contentHeight;
              break;
      }
      //最后不要忘记了,调用父类的测量方法
      setMeasuredDimension(width, height);
  }

 

到这里,就算View的大小就已经完成了,自定义View的计算过程和以上方法基本类似。接着就是计算需要显示的图标和字体的位置。这里希望图片和字体垂直排列,并居中显示在View当中,因为当前的View的宽高已经测量好了,接下来的计算也就非常简单了,这里就放在onDraw()方法中计算

d)绘制图标和字体
绘制图标,可以用canvas.drawBitmap(Bitmap bitmap, int left, int top ,Paint paint)方法,bitmap 已经有了,如果不需要对图片作特殊处理 paint 可以传入null表示原图原样的绘制在白纸上,所以就差绘制的位置 left ,top前面已经分析过了,需要把图绘制在View的中间,当然这里还需包含字体,所以可以这样计算left 和top。

 

int left = (mViewWidth - mIconNormal.getWidth())/2 ;
int top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2 ;

 mViewWidth --->View的宽度,mIconNormal --->图片的宽度, mBoundText.height() --->字体的高度;绘制字体,绘制字体,就比绘制图片稍微麻烦点,因为绘制字体需要用到画笔Paint ,这里定义一个画笔Paint,直接new 一个出来

 

mTextPaintNormal = new Paint();
    //设置字体大小
mTextPaintNormal.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mTextSize, getResources().getDisplayMetrics()));
    //设置画笔颜色,也就是字体颜色
    mTextPaintNormal.setColor(mTextColorNormal);
    //设置抗锯齿
    mTextPaintNormal.setAntiAlias(true);

 这里也是调用Canvas的方法 canvas.drawText(mTextValue,x,y, mTextPaintNormal);mTextValue需要绘制的字体内容, mTextPaintNormal画笔,x,y需要绘制的位置

 

float x = (mViewWidth - mBoundText.width())/2.0f ;
    float y = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;

    整体来说代码还是相当少的。下面把onDraw的代码也贴出来
    @Override
    protected void onDraw(Canvas canvas) {
        drawBitmap(canvas) ;
        drawText(canvas) ;
    }
    private void drawBitmap(Canvas canvas) {
        int left = (mViewWidth - mIconNormal.getWidth())/2 ;
        int top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2 ;
        canvas.drawBitmap(mIconNormal, left, top ,null);
    }
    private void drawText(Canvas canvas) {
        float x = (mViewWidth - mBoundText.width())/2.0f ;
        float y = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;
        canvas.drawText(mTextValue,x,y, mTextPaintNormal);
    }

 总结:
onMeasure() 方法只要了解了 MeasureSpec 类就不是什么问题,而MeasureSpec 也很简单,onDraw() 方法就需要了解Canvas 类的绘制方法,并且通过简单的Api查询,就基本能实现我们所需的要求。对于自定义View,如果你会重写 测量 和 onDraw 方法,那么就具备了此技能,而如果需要了解更深,自定义有个性,更绚丽的View,就还得深入了解Canvas 、Paint等方法

 

分享到:
评论

相关推荐

    scratch少儿编程逻辑思维游戏源码-城堡战争.zip

    scratch少儿编程逻辑思维游戏源码-城堡战争.zip

    【Go语言编程】大厂Go工程师面试题集锦:涵盖并发、网络、数据库及算法设计要点

    内容概要:本文档汇集了来自字节跳动、腾讯、金山WPS、跟谁学和百度等大厂的Go工程师面试题,涵盖广泛的技术领域。主要包括Go语言特性(如goroutine调度、channel机制)、操作系统(进程间通信、线程调度)、计算机网络(TCP/IP协议栈、HTTP协议)、数据结构与算法(排序算法、LRU缓存)、数据库(MySQL索引优化、Redis内部机制)、分布式系统(负载均衡、服务发现)等方面的知识点。通过这些问题,不仅考察应聘者的理论基础,还测试其实际项目经验和技术深度。 适合人群:有一定Go语言编程经验和计算机基础知识的开发者,特别是准备应聘互联网大厂的中级及以上水平的后端工程师或全栈工程师。 使用场景及目标:①帮助求职者全面复习Go语言及其相关领域的核心概念;②为面试官提供有价值的参考题目,确保候选人具备解决复杂问题的能力;③指导工程师深入理解并掌握企业级应用开发所需的关键技能。 阅读建议:由于题目覆盖面广且难度较高,建议读者结合自身情况选择重点复习方向,同时配合实际编码练习加深理解。对于每个知识点,不仅要记住答案,更要理解背后的原理,这样才能在面试中灵活应对各种变体问题。

    scratch少儿编程逻辑思维游戏源码-堡垒之夜(吃鸡游戏).zip

    scratch少儿编程逻辑思维游戏源码-堡垒之夜(吃鸡游戏).zip

    少儿编程scratch项目源代码文件案例素材-派.zip

    少儿编程scratch项目源代码文件案例素材-派.zip

    scratch少儿编程逻辑思维游戏源码-Scratch 冒险.zip

    scratch少儿编程逻辑思维游戏源码-Scratch 冒险.zip

    2025 飞特舵机, Arduino版本

    2025 飞特舵机, Arduino版本

    scratch少儿编程逻辑思维游戏源码-躲避.zip

    scratch少儿编程逻辑思维游戏源码-躲避.zip

    PFC5.0纤维混凝土三点弯曲模拟:参数化建模与实验分析

    内容概要:本文详细介绍了利用PFC5.0进行纤维混凝土三点弯曲模拟的方法。首先,作者展示了如何通过定义纤维的体积含量、长度、半径和刚度等关键参数来构建纤维网络。接着,描述了三点弯曲加载的具体实现方式,包括加载速率控制和终止条件设定。最后,提供了后处理方法,如绘制并导出力-位移曲线图,以便于分析材料破坏机制。文中还给出了若干实用建议,如纤维半径的选择范围、加载速率的初始值以及不同类型纤维的接触模型选择。 适合人群:从事材料科学尤其是混凝土材料研究的专业人士,以及对离散元法和数值模拟感兴趣的科研工作者。 使用场景及目标:适用于希望深入了解纤维混凝土力学性能的研究人员,旨在帮助他们掌握PFC5.0软件的操作技巧,优化模拟参数设置,提高实验效率。 其他说明:文中提供的代码片段可以直接应用于实际项目中,同时附带了一些实践经验分享,有助于初学者快速入门并避免常见错误。

    少儿编程scratch项目源代码文件案例素材-生存V1(有BAG).zip

    少儿编程scratch项目源代码文件案例素材-生存V1(有BAG).zip

    少儿编程scratch项目源代码文件案例素材-披萨机器人.zip

    少儿编程scratch项目源代码文件案例素材-披萨机器人.zip

    少儿编程scratch项目源代码文件案例素材-气球滑雪板.zip

    少儿编程scratch项目源代码文件案例素材-气球滑雪板.zip

    少儿编程scratch项目源代码文件案例素材-使命召唤(苏联插旗).zip

    少儿编程scratch项目源代码文件案例素材-使命召唤(苏联插旗).zip

    可跨平台移植的模拟IIC实战项目STM32F407-TestIIC

    1. GPIO模拟I2C 实战项目,根据正点原子 STM32F407ZGT6 进行更改; 2. 可适配STM32、GD32、HC32等MCU;

    scratch少儿编程逻辑思维游戏源码-百米冲刺.zip

    scratch少儿编程逻辑思维游戏源码-百米冲刺.zip

    【蓝桥杯竞赛】历年试题精选与备考资源汇总:编程算法及硬件单片机试题解析与练习指导

    内容概要:本文档汇总了蓝桥杯历年试题及练习资源,涵盖编程类试题精选、硬件与单片机试题、练习资源与题库以及备考建议。编程类试题精选包括基础算法题(如数组求和、质因数分解)、经典算法案例(如最大子序列和、兰顿蚂蚁模拟)和数据结构应用(如字符全排列)。硬件与单片机试题主要涉及客观题考点,如BUCK电路和电源设计。练习资源与题库部分介绍了真题平台(如Dotcpp、CSDN专题)和专项训练包(如Python题库、Java百题集、C++真题解析)。备考建议分为分阶段练习(新手阶段、进阶提升)和模拟实战(如使用Dotcpp估分系统进行限时训练),强调按年份和组别分类练习,强化代码实现与调试能力。; 适合人群:准备参加蓝桥杯竞赛的学生及编程爱好者。; 使用场景及目标:①针对不同编程语言和难度级别的题目进行专项训练;②通过历年真题和模拟实战提高解题速度和准确性;③掌握算法设计、数据结构应用及硬件基础知识。; 阅读建议:此文档提供了丰富的试题和练习资源,建议根据自身水平选择合适的题目进行练习,并结合真题平台的估分系统和社区开源代码进行对比优化,逐步提升编程能力和竞赛水平。

    30kW储能PCS原理图设计:量产设计的关键要素与优化策略

    内容概要:本文详细介绍了30kW储能PCS(电力转换系统)原理图的设计要点及其量产化过程中需要注意的技术细节。首先阐述了储能PCS的基本概念和重要性,接着深入探讨了主拓扑结构的选择,特别是双级式结构的优势以及关键组件如IGBT的驱动时序配置。随后讨论了控制算法的智能化改进,包括加入前馈补偿以提高系统的稳定性。此外,还强调了EMC设计、PCB布局、元件选择等方面的注意事项,并分享了一些实际生产中遇到的问题及解决方案。最后提到了自动化测试方法和散热管理策略,确保产品在各种环境下的可靠运行。 适合人群:从事储能系统设计、电力电子产品研发的工程师和技术人员。 使用场景及目标:帮助读者掌握30kW储能PCS从原理图设计到量产实施的全流程关键技术,提升产品的性能和可靠性,避免常见错误。 其他说明:文中提供了具体的代码片段和实践经验,有助于理解和应用相关理论。

    少儿编程scratch项目源代码文件案例素材-喷气包多德.zip

    少儿编程scratch项目源代码文件案例素材-喷气包多德.zip

    机械工程中基于Python的齿轮啮合性能与动态响应分析

    内容概要:本文深入探讨了齿轮啮合性能及其动态特性,特别是直齿轮的基础参数计算、渐开线绘制以及接触力仿真的具体实现。首先介绍了齿轮的基本参数如模数、齿数、压力角等,并给出了具体的计算实例。接着详细讲解了如何利用Python进行渐开线的数学建模并绘图展示,强调了这种曲线对于确保齿轮平稳传动的重要性。然后讨论了齿轮在啮合过程中接触力的变化规律,提供了简化的Python代码来模拟这一现象。最后指出,在实际工程项目中应当借助专业的软件包如PyDy或ADAMS来进行更加精确的动力学分析,同时肯定了自行编写代码的价值在于能够更好地理解和排查问题。 适合人群:机械工程领域的研究人员、工程师以及相关专业的学生。 使用场景及目标:①帮助读者掌握齿轮基本理论知识;②指导读者运用Python编程技能完成简单的齿轮性能分析任务;③为后续深入研究提供思路和技术支持。 阅读建议:由于文中涉及较多的专业术语和数学公式,建议读者提前复习相关基础知识,并尝试运行提供的代码片段加深理解。此外,对于想要进一步探索该领域的读者来说,可以参考文末提到的专业工具包进行更复杂的研究。

    少儿编程scratch项目源代码文件案例素材-任务.zip

    少儿编程scratch项目源代码文件案例素材-任务.zip

    少儿编程scratch项目源代码文件案例素材-时光大盗.zip

    少儿编程scratch项目源代码文件案例素材-时光大盗.zip

Global site tag (gtag.js) - Google Analytics