[Android] Android自定义View修炼之路(1)_Glemontree_的博客-程序员秘密

技术标签: 自定义view  android  android进阶  

前言

尼玛一直觉得自定义View好难,但是呢自定义View在Android开发过程中又是无法逃避的坎,一个App我觉得除了数据外,就属外观了,漂亮的界面总是让人心旷神怡。每每看到别人做的酷炫的控件都好羡慕,想知道他们是怎么实现的,相信很多初学者都有和我一样的苦恼。但是呢,学习是一个不断积累的过程,不能指望着一步登天,所以还是需要先打好基础,一步一步来,下面让我们跟着大神一步步学习自定义View吧(ps:我不是大神==,我也只是个菜鸟,我说的大神是洋神, 我也只是跟着他的步骤将他博客里的案例做一下,顺便做些自己的笔记)。

今天是第一课,是阅读洋神的 Android 自定义View (一) 文章做的笔记。

自定义View的步骤

一般来说,自定义View需要有以下四个步骤,视实际的情况某些步骤可有可无:

  • 自定义View的属性
  • 在View的构造方法中获得自定义的属性
  • 重写onMeasure
  • 重写onDraw

自定义View的属性

通常来说,我们在做自定义View的时候,都需要自定义View的属性,比如字体的大小、字体的颜色等等属性,我们希望可以像Android内置的控件一样可以在XML文件中配置这些属性,这时候就需要自定义View的属性,自定义View的属性需要在工程目录下的res/values子目录下建立一个attr.xml文件,在里面定义我们需要的属性,比如:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="titleText" format="string"></attr>
    <attr name="titleTextColor" format="color"></attr>
    <attr name="titleTextSize" format="dimension"></attr>

    <declare-styleable name="CustomeTitleTextView">
        <attr name="titleText"/>
        <attr name="titleTextColor"/>
        <attr name="titleTextSize"/>
    </declare-styleable>
</resources>

在这里我们定义了三个属性,分时是titleTexttitleTextColor以及titleTextSize,其实还有另外一种写法如下,两者的效果是一样的:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomeTitleTextView">
        <attr name="titleText" format="string"/>
        <attr name="titleTextColor" format="color"/>
        <attr name="titleTextSize" format="dimension"/>
    </declare-styleable>
</resources>

相信大家也看到了,我们在声明属性时有一个format属性,format其实是该属性的取值类型,一共有string、color、dimension、integer、enum、reference、float、boolean、fraction、flag:

  • string:取值类型为字符串

  • color:取值类型为颜色值,比如app:color="#ffffff"

  • dimension:取值类型为尺寸值,比如app:dimension="30dp"

  • integer:取值类型为整数,比如app:count="12"

  • enum:取值类型为枚举,比如

    <declare-styleable name="名称">
    <attr name="属性名称">
        <enum name="horizontal" value="0"/>
        <enum name="vertical" value="1"/>
    </attr>
    </declare-styleable>

    那么在xml文件中就可以这样使用:

    app:属性名称="horizontal"
  • reference:取值类型为某一资源ID,比如app:background="@drawable/picture"

  • float:取值类型为浮点数,比如app:fromAlpha="0.1"

  • boolean:取值类型为布尔值,比如app:focusable=true

  • fraction:取值类型为百分数,比如app:pivotX="200%"

  • flag:取值类型为位或运算,比如

    <declare-styleable name="名称">
    <attr name="windowSoftInputMode">
        <flag name = "stateUnspecified" value = "0" />
        <flag name = "stateUnchanged" value = "1" />
          <flag name = "stateHidden" value = "2" />
          <flag name = "stateAlwaysHidden" value = "3" />
          <flag name = "stateVisible" value = "4" />
          <flag name = "stateAlwaysVisible" value = "5" />
          <flag name = "adjustUnspecified" value = "0x00" />
          <flag name = "adjustResize" value = "0x10" />
          <flag name = "adjustPan" value = "0x20" />
          <flag name = "adjustNothing" value = "0x30" />
      </attr>         
    </declare-styleable>

    那么就可以这样使用:

    <activity
    android:name = ".StyleAndThemeActivity"
      android:label = "@string/app_name"
      android:windowSoftInputMode = "stateUnspecified | stateUnchanged | stateHidden">
          <intent-filter>
               <action android:name = "android.intent.action.MAIN" />
               <category android:name = "android.intent.category.LAUNCHER" />
          </intent-filter>
    </activity>

接下来我们就可以在布局文件中声明我们的自定义View:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.glemontree.customview01.MainActivity">

    <com.glemontree.customview01.CustomView
        android:layout_width="200dp"
        android:layout_height="100dp"
        app:titleText="3712"
        app:titleTextSize="40sp"
        app:titleTextColor="#ff0000"/>
</RelativeLayout>

你会发现,在使用自定义属性的时候有个前缀app,其实这个不是必须得是app,但是必须得和布局文件开始处xmlns:app="http://schemas.android.com/apk/res-auto"中的xmlns后面的名称相同。

在View的构造方法中获得自定义的属性

接下来,我们就需要在我们自定义View的构造方法中获得自定义属性,代码实现如下:

public CustomView(Context context) {
    this(context, null);
}

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

public CustomView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomeTitleTextView, defStyle, 0);
    int n = a.getIndexCount();
    for (int i = 0; i < n; i++)
    {
        int attr = a.getIndex(i);
        switch (attr)
        {
            case R.styleable.CustomeTitleTextView_titleText:
              mTitleText = a.getString(attr);
              break;
            case R.styleable.CustomeTitleTextView_titleTextColor:
              // 默认颜色设置为黑色
              mTitleTextColor = a.getColor(attr, Color.BLACK);
              break;
            case R.styleable.CustomeTitleTextView_titleTextSize:
              // 默认设置为16sp,TypeValue也可以把sp转化为px
              mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
              break;
        }
    }
    a.recycle();
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setTextSize(mTitleTextSize);

    mBound = new Rect();
    mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);

首先呢,我们发现我们在自定义View时实现了三个构造方法,那么这三个构造方法有什么区别呢(参考自文章android View的三个构造方法 简单总结)?

其实,第一个构造方法是提供给我们在代码中生成控件使用的,比如:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(new CustomView(this));
}

在上面的代码中,我们直接通过new CustomView(this)生成了控件,没有涉及到属性的添加,不能在布局文件中使用,如果要在布局文件中使用,必须添加第二个构造方法。

第二个方法是在XML布局文件中插入控件使用的,其中attrs参数就是我们在XML文件中自定义控件的属性,其实第二个构造函数也是调用第三个构造函数,这里我在第三个参数传入R.attr.customViewStyle

第三个方法的第三个参数defStyleAttr的意义是从APP或者Activity的Theme中设置的该控件的属性的默认值,比如我在attr.xml文件中定义了自定义属性:

<attr name="customViewStyle" format="reference"></attr>

然后我们在App或者Activity的Theme中设置它的值:

<style name="AppTheme" parent="AppBaseTheme">  
       <!-- All customizations that are NOT specific to a particular API-level can go here. -->  
       <item name="customViewStyle">@style/custom_view_style</item>  
</style> 

其中的custom_view_style:

<style name="custom_view_style">  
       <item name="circleWidth">8dp</item>  
</style>  

这样当你的XML文件中没有给该控件的circleWidth定义值的时候,默认值就是8dp。需要注意的是:

TypedArray array=context.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);  

该方法的第四个参数defStyleRes,可以直接传入自定义的style,如果defStyleAttr为0,defStyleRes才会起作用。
android控件获取属性值的优先顺序:

  • 在XML文件中直接定义;
  • 在XML文件引用的style;
  • 就是从如上所说的defStyleAttr中取值;
  • 从defStyleRes取值;
  • 从Activity或者Application的Theme中取值;

在第三个构造方法中获取自定义属性的取值时还有另外一种方法如下:

public CustomView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomeTitleTextView, defStyle, 0);
    mTitleText = a.getString(R.styleable.CustomeTitleTextView_titleText);
    mTitleTextSize = a.getDimensionPixelSize(R.styleable.CustomeTitleTextView_titleTextSize,
                            (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16,                                                         getResources().getDisplayMetrics()));
    mTitleTextColor = a.getColor(R.styleable.CustomeTitleTextView_titleTextColor, Color.BLACK);
    a.recycle();
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setTextSize(mTitleTextSize);

    mBound = new Rect();
    mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
}

我个人更加倾向于后面这种方法,这里我对构造方法中的一些函数进行介绍:

  • TypedVlaue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources.getDisplayMetrics())

    该函数的原型为TypedValue.applyDimension(int unit, float value, DisplayMetrics metrics),用来进行单位的转换,其中第一个参数是第二个参数的单位,并将该单位的值转换为px(像素),DisplayMetrics是一个获取屏幕信息的类,density是设备密度,dp = px / density,因此该方法就是一个将各种单位的值转换为像素的方法。

  • getDimensionPixelSize是获取某个dimen的值,如果单位是dpsp,则需要将其乘以density,终归来说,这个函数的返回值得到的是像素值。

onMeasure方法这里我们没有进行重写,在onMeasure方法中直接调用父类的onMeasure方法,因此这里暂时不加以介绍。下面我们看看onDraw方法!

重写onDraw

onDraw方法顾名思义就是进行绘图的方法,这里我们在onDraw方法中绘制了一个矩形和一行文字,如下:

@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.YELLOW);
    canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

    mPaint.setColor(mTitleTextColor);
    canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 +       mBound.height() / 2, mPaint);
}

这里其实还有个坑,就是drawText()方法,大家有兴趣的可以去研究一下。

现在基本上已经有效果了,运行程序,结果如下:

按照洋神的思路(/捂脸),改下布局文件中CustomView的layout_width和layout_height属性,将CustomView的layout_width和layout_height属性均改为wrap_content,此时运行程序,结果如下:

这里因为我们没有重写onMeasure方法,而是直接调用的父类的onMeasure方法,当我们在布局文件中明确设置CustomView的宽度和高度时,系统帮我们测量的结果就是我们设置的值,但是但我们设置成wrap_content或者match_parent时,系统帮我们测量的结果就是match_parent的值,因此,一般我们都需要重写onMeasure方法,并且在onMeasure方法中根据实际的情况进行不同的设定。

重写onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width = 0, height = 0;
    if (width == MeasureSpec.EXACTLY) {
      width = widthSize;
    } else {
      mPaint.setTextSize(mTitleTextSize);
      mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
      width = getPaddingLeft() + getPaddingRight() + mBound.width();
    }
    if (height == MeasureSpec.EXACTLY) {
      height = heightSize;
    } else {
      mPaint.setTextSize(mTitleTextSize);
      mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
      height = getPaddingTop() + getPaddingBottom() + mBound.height();
    }
    setMeasuredDimension(width, height);
}

并且修改布局文件,给CustomView添加上padding属性以及layout_centerInParent属性,运行结果如下:

总结

本文是Android自定义View修炼之路的开篇,虽然大量参考了洋神的博客,但是在自己动手操作的过程中对其中自定义View的过程有了更加深刻的了解,收获还是很大的,感谢洋神的分享!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u010216743/article/details/77744597

智能推荐

2021-04-11_在类外定义一个对象s1并初始化 5、利用s1对象直接访问count并输出其值_该取啥名儿的博客-程序员秘密

PTA-Python刷题5-5:输出给定字符S2在给定字符串S1中出现的次数。S1.count(S2)统计列表中元素个数列表名.count(元素值)第5章-7 列表去重输入一个列表,去掉列表中重复的数字,按原来次序输出!思路:不能用set()函数强制转换成集合 因为会打乱顺序 一开始想着输入的时候就直接去掉逗号和【】存入列表 但是发现不能split()后不能再接split()或者strip() 两个strip()连续用好像第二个也不会起作用 所以就想着就看作一整个字符串好了 再遍历判

sudo dpkg --configure -a的作用_悟道-欲的博客-程序员秘密

sudo dpkg --configure -a的作用昨天下载non-free flash的时候误以为卡住了,于是强行关闭KpackageKit,导致从此以后无法使用它安装任何程序,因为在程序的记录当中non-free flash的安装并没有完成。怎么都解决不了,于是在网上找,最后在ubuntuforums.org上找到了解决方法,帖子的标题是:《  Only one softwa

python获取图像坐标_关于python:如何在Sikuli中获取图像的坐标?_weixin_40001805的博客-程序员秘密

我在我们的项目上有这个GUI(请忍受我的插图,我不允许在工作时截取屏幕截图)----------------------------------------------(1) Header |----------------------------------------------|(2) Files ...

D. Playoff Tournament_thusloop的博客-程序员秘密

D. Playoff TournamentExampleinputCopy30110?1165 16 ?7 ?1 ?5 ?1 1outputCopy123354线段树nlogn遇到 ? 该节点的值为左子树加右子树遇到 1 该节点的值为右子树遇到 0 该节点的值为左子树#include&lt;bits/stdc++.h&gt;#define int long long#define IOS ios::sync_with_stdio(false);cin.tie

如何快速写一款小而美的“上滑无限加载的控件”?| 博文精选_CSDN资讯的博客-程序员秘密

作者 |ShuSheng007责编 | 郭芮出品 | 程序员秘密在日常从事Android开发工作时,经常会遇到下拉刷新列表页面,上拉自动加载列表的需求, GitHub上已经有很多关于...

hdrcnn环境配置以及踩坑_hdrcnn代码复现_CCB_307的博客-程序员秘密

文章:《HDR image reconstruction from a single exposure using deep CNNs》代码:https://github.com/gabrieleilertsen/hdrcnn关于python3运行OpenEXR的坑使用sudo apt-get install libopenexr-dev安装的是1.2.0的版本,不过这个只能在python2...

随便推点

java.lang.UnsatisfiedLinkError: /mnt/jdk1.8/jre/lib/amd64/libawt_xawt.so: libXrender.so.1_能量守恒洛的博客-程序员秘密

AWT is not properly configured on this server. Perhaps you need to run your container with "-Djava.awt.headless=true"? See also: https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+got+java.awt.headl...

StaggeredGridLayoutManager瀑布流错乱和顶部空白问题解决_staggeredgridlayoutmanager 上面空_春暖花开_love的博客-程序员秘密

 参考: https://blog.csdn.net/maplelove1211/article/details/78394852 https://blog.csdn.net/chen_lian_/article/details/80239028  闪烁问题https://blog.csdn.net/lf0814/article/details/52884569...

房地产库存两极分化态势明显_Shawn.Hu的博客-程序员秘密

http://news.ifeng.com/a/20160810/49750364_0.shtml3日,发改委官网刊文称更好发挥投资对经济增长的作用,建议未来因城施策加大调控力度,进一步去库存;鼓励先租后售、与政府共有产权等方式促进居民购房,鼓励品牌房地产开发企业收购现有地产项目。上海易居房地产研究院最新报告显示,上半年库存削减态势明显,保持了连续10个月库存同比下滑的态势。但值得注意

c语言中文定义什么类型,typedef在c语言中是什么意思?_陈炳好的博客-程序员秘密

在c语言中,typedef的意思是将一个自己命名的类型用已经有的类型来代替。C语言允许用户使用 typedef 关键字来定义自己习惯的数据类型名称,来替代系统默认的基本类型名称、数组类型名称、指针类型名称与用户自定义的结构型名称、共用型名称、枚举型名称等。一旦用户在程序中定义了自己的数据类型名称,就可以在该程序中用自己的数据类型名称来定义变量的类型、数组的类型、指针变量的类型与函数的类型等。例如,...

Vmware虚拟机ip为127.0.0.1的解决办法,修改虚拟机IP的详细步骤_虚拟机127解决办法_csdn4006600的博客-程序员秘密

跟着慕课网的linux教程学习,很快就来到了xshell连接虚拟机这一节。但是慕课网的教程是有问题的,在这里卡住了。我在网上找了很多办法,总共花了七八个小时才解决。网上有这类问题的解决办法,但是都不是给我这种小白看的。我相信有很多像我这样的小白需要这样一篇详细的教程。在Linux命令行输入这个命令。直接复制就可以。vi /etc/sysconfig/network-scripts/ifcfg-...

端口介绍_weixin_34375054的博客-程序员秘密

端口包括两种,一种是硬件的,就是插网线的那个口。一种是软件的,是一种虚拟的口。一个应用程序需要一个虚拟的连接口。比如你上网页所需要的HTTP协议。它所使用的就是80端口。你文件传输所使用的FTP用的就是22、23两个端口。比如在TCP/IP协议中,运输层与应用层的连接就是靠端口来实现的.端口其实是一种虚拟的东西!在Internet上,各主机间通过...

推荐文章

热门文章

相关标签