记录学习Android基础的心得09:常用控件(高级篇)

Posted 搬砖工人_0803号

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记录学习Android基础的心得09:常用控件(高级篇)相关的知识,希望对你有一定的参考价值。



活着就要做有意义的事;有意义的事就是好好活着。–《士兵突击》


前言

高级篇是系统总结常用控件系列四部曲的最后一章,内容包括:屏幕显示,自定义控件,页面布局优化,自定义通知栏,碎片。关于控件的更多知识可参考专业的工具书,当然,更高级的技巧也不像本系列文章的大白话一样,肯定涉及到复杂的系统代码和数学计算,学习起来也困难得多,好了,不多 BB,进入本文正题。

一、屏幕显示

1.显示屏的硬件参数

大部分人都知道,显示屏是由像素阵列组成的,用英寸表示显示屏的尺寸大小,用分辨率表示显示屏的成像质量。可能还有部分人对这些概念还不太了解,那么接下来就系统总结一下关于显示屏的各个参数含义:
(1) 像素
显示屏的像素指一个最小的发光单元,即便尺寸相同的显示屏的像素长宽值也会由于分辨率的不同而不同。如图是 OLED显示屏的像素结构:

(2) 分辨率
显示屏的分辨率指“行像素值 x 列像素值”,如小米6屏幕的分辨率为1920x1080表示该显示屏每一行有1080个像素,每一列有1920个像素。显然,相同尺寸的显示屏的分辨率越大,那么它的发光单元越多,显示的图像就越清晰。
(3) 色彩深度
色彩深度指显示屏的一个像素发光的颜色有多少种,一般用“位”(bit)来表示。如单色屏的每个像素有亮或灭两种状态(即2种颜色),那么用1个数据位就可以表示该像素的所有状态,所以它的色彩深度为1bit,其它常见的显示屏色深为16bit、24bit。
(4) 显示屏尺寸
显示屏的大小一般以英寸(1英寸=2.54厘米)表示,这个长度是指屏幕对角线的长度,通过屏幕的对角线长度及长宽比即可确定屏幕的实际长宽尺寸。
(5) 点距
点距指两个相邻像素之间的距离,它会影响画质的细腻度及观看距离,点距越小,画质越细腻。如LED点阵显示屏的点距一般都比较大,所以适合远距离观看。

2.android系统对屏幕参数的管理

(1) Android的尺寸单位
获取手机屏幕的尺寸信息需使用 DisplayMetrics,它的常用属性有:
①heightPixels:计算屏幕的高度值(以像素px为单位)。
②widthPixels:计算屏幕的宽度值(以像素px为单位)。
③density:像素密度,表示1dp单位包含多少个px单位。
比如,获取屏幕的宽度(像素点数)可通过以下方式(其他属性的获取同理):

    // 获得屏幕的宽度
    public int getScreenWidth(Context ctx) {
        // 从系统服务中获取窗口管理器
        WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        // 从窗口管理器中获取显示参数保存到dm对象中
        wm.getDefaultDisplay().getMetrics(dm);
        return dm.widthPixels; // 返回屏幕的宽度
    }

上面提到的dp是大家在XML文件中经常使用的,它是一种与具体屏幕分辨率无关的尺寸单位,只与屏幕自身e的尺寸大小有关。尺寸相同,分辨率不同的屏幕,以dp为单位计量的图形最终显示的尺寸相同。通常Android中类有关尺寸的方法采用的是px单位,而XML文件使用的是dp单位,故有时需要使用DisplayMetrics的density属性进行单位换算:当density=1,表示1dp=1px,density=1.5,表示2dp=3px,density=2,表示1dp=2px,具体代码如下:

    // 从 dp 单位 转成为 px(像素)
    public int dip2px(Context context, float dpValue) {
        // 获取当前手机的像素密度
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f); // 四舍五入取整
    }

    // 从 px(像素) 单位 转成为 dp
    public int px2dip(Context context, float pxValue) {
        // 获取当前手机的像素密度
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f); // 四舍五入取整
    }

Android还支持的尺寸单位有:in(英寸),mm(毫米),pt(磅,1pt=1/72 in),sp(文字尺寸),其中sp是专门用于设置文字尺寸的单位,被设置成该单位的文字会随着系统设置的字体大小而变大或变小(若使用其他单位设置字体大小,则不会随系统设置变化而变化)。
(2)Android像素的颜色
像素作为一个基本的发光单元,它可以显示由不同光强的红,绿,蓝三原色混合而成的不同颜色。在Android中,颜色值由透明度AA,三原色RGB组成,有6位十六进制(RRGGBB),8位十六进制(AARRGGBB)两种编码。透明度AA的值为FF时,表示完全不透明,为00时表示完全透明,三原色数值(00-FF)越大,则对应的光成分占比越多,数值越小则占比越少,当三原色的数值都相等但不为最大值或最小值时,会变成灰色光。
在XML文件中使用十六进制的颜色值需要添加前缀"#",即"#(AA)RRGGBB",在代码中直接使用颜色数值需要注意:只能使用8位的颜色编码,6位的十六进制颜色默认是完全透明的,故相当于没有效果。

二、自定义控件

当Android提供的原生UI控件不能满足使用需求时,开发者往往需要自定义控件。比如,上一篇文章中自定义了一个能同时绘制矩形和圆形的控件(虽然没卵用)。个人觉得自定义控件涉及到的知识应该是Android基础知识中最难的一部分了,其次是四大组件中的ContentProvider,其实我自己对于自定义控件也不是很熟练。

自定义控件通常分为两种情况:(1)基于现有的控件,只优化部分外观和功能,优化后的控件保留着原有控件的大部分特征。比如前文使用过的翻页标题栏 PagerTabStrip不支持在XML文件中设置标题的文字样式,那么完全可以继承PagerTabStrip类,在它的基础上添加一些方法来支持XML文件中的文字样式设置。
(2)基于View或者ViewGroup,完全由开发者自己绘制控件的外观,处理控件的回调事件。这种自定义控件往往有特殊的外观和功能,比如,显示信号的示波器控件,控制方向的摇杆控件等根据需求定制的控件。

按照我自己的理解,自定义视图的流程通常分为六个步骤:分析控件,声明属性,构造对象,测量尺寸,定位坐标,绘制控件。 接下来自定义一个摇杆控件的例子来熟悉一下自定义控件的流程,先看一下效果动图(下图可能有点模糊,这是由于我先录屏然后再转成gif格式的图片。。):

1.分析控件

(1)外观分析
肉眼望去,本例子中摇杆外观可分为四个部分组成:摇杆的正方形底盘(方向背景贴图),摇杆的杆,摇杆的球,和球所处的高亮扇形区域。本摇杆控件以正方形为边界,在控件里面绘制了方向背景贴图,摇杆的中间是一个可以拽动的小球,当手指触摸滑动摇杆控件所处的区域时,小球会追踪手指轨迹,并在正方形的内切圆边上移动,小球的圆心和控件的中心还会绘制一条一定宽度的线段(摇杆的杆),同时,会高亮显示小球所处的区域。可以看到,这两个摇杆控件的四个组成部分都不相同,表示此控件的这四个部分是可以自定义的。那么这里先自己绘制两个(丑陋的)方向背景贴图:

(2)功能分析
最基本的功能是作为一个实时跟踪手指移动轨迹的自定义控件,其次外界可以获取本控件的高亮区域。
显然,这种外观由几个简单图形组成,功能单一的自定义控件,继承View来开发就完全可以了,那么同时定义摇杆控件的类名就叫RockerView吧(和系统UI控件采用相同风格命名)。

2.声明属性

控件的自定义属性大多与控件的外观有关,声明属性有两个方面:①在XML属性资源文件中声明属性。②在自定义控件类中声明属性。
一般来说,XML文件中自定义的属性在类中都要有一一对应的变量,此外,在自定义的控件类中还需要一些其他的属性。这样,我们不仅可以在XML布局文件中创建该控件,也能在Java代码中创建。
由于是继承自View,故View中有用的通用属性我们不需要重新声明,我们只需抽离出自定义控件的特有属性即可,比如,摇杆控件的尺寸大小完全可用View的layout_xxx属性设置,但特有属性:摇杆的方向背景贴图,摇杆的杆的粗细,杆的颜色,摇杆的球的大小,球的颜色,控件均分的扇形区域数量,扇形区域的高亮颜色需要我们自定义(本例只做演示功能,后来者可以基于实际情况定义更多的属性,让摇杆有更漂亮的外观)。
(1)在XML属性资源文件中声明属性
首先,在res/values目录下新建一个attrs.xml的属性资源文件,指定文件的根标签为resources,resources标签可以添加两个子标签:
①attr:声明控件的一个属性,attr标签可以指定name表示属性的名称,format表示属性的值的格式(数据类型)。
②declare-styleable:定义一个styleable对象,它是一组attr标签的集合,用于组合多个属性,此标签的name属性通常设置为自定义控件的类名。
那么从以上的控件分析很容易得到摇杆控件的属性如下:

<resources>
    <declare-styleable name="RockerView">
        <attr name="rocker_bar_color" format="color" /><!-->摇杆的杆的颜色<-->
        <attr name="rocker_bar_width" format="integer" /><!-->摇杆的杆的宽度<-->
        <attr name="rocker_ball_color" format="color" /><!-->摇杆的球的颜色<-->
        <attr name="rocker_ball_radius" format="integer" /><!-->摇杆的球的半径<-->
        <attr name="rocker_plate_background" format="reference" /><!-->摇杆的底盘的背景贴图<-->
        <attr name="rocker_sector_num" format="integer" /><!-->摇杆的底盘平均分为多少个扇形区域<-->
        <attr name="rocker_sector_color" format="color" /><!-->摇杆的扇形区域的颜色<-->
    </declare-styleable>
</resources>

在自定义好属性文件之后,怎么使用属性资源文件所定义的属性,取决于自定义控件类的方法实现,即如何从布局文件中获取控件的自定义属性的值呢?答案是可以在自定义控件的构造方法中通过它的参数 AttributeSet获取在XML布局文件中设置的这些属性值。
(2)在自定义控件类中声明属性
要在自定义控件类中创建与自定义的属性一一对应的变量,并添加一些额外的必要属性变量。如摇杆控件中属性变量如下:

public class RockerView extends View {
    private Context mContext; // 声明一个上下文对象
    //!!摇杆的背景贴图,扇形区域,杆,球的各种属性,如果在XML文件中未指定这些属性,则使用以下默认值:
    private Bitmap rockerPlate; // 摇杆的底盘的背景贴图
    private Region[] sectorRegions;//保存摇杆均分的每个区域
    private int rockerSectorNum = 8;//摇杆的底盘默认平均分为8个扇形区域
    private int rockerSectorColor = Color.CYAN;// 摇杆的球所落在区域的颜色
    private int rockerBarColor = Color.GREEN; // 摇杆的杆的颜色
    private int rockerBarWidth = 30; // 摇杆的杆的宽度
    private int rockerBallColor = Color.RED; // 摇杆的球的颜色
    private int rockerBallRadius = 50; // 摇杆的球的半径(即小圆的半径:r )
    //!!
    private Matrix rockerPlateMatrix = new Matrix();//此矩阵用于背景贴图的缩放变换以填满本视图
    private Paint rockerPlatePaint = new Paint();//绘制背景贴图的画笔
    private Paint rockerSectorPaint = new Paint();//绘制扇形区域的画笔
    private Paint rockerBarPaint = new Paint();//绘制摇杆的杆的画笔
    private Paint rockerBallPaint = new Paint();//绘制摇杆的球的画笔

}

3.构造对象

在大脑中构想好控件的外观和功能后,需要在类中通过方法实现出来,首先重写构造方法获取控件的属性值和初始化控件的各种变量,开发者一般重写三个不同参数的构造方法:
①只带一个参数(Context)的方法,此方法在从代码中生成控件时被调用。
②带两个参数(Context,AttributeSet)的方法,此方法在从XML布局文件中生成控件时被调用。参数AttributeSet是从XML布局文件中获取的该控件已经设置好的属性集合。
③带三个参数(Context,AttributeSet,int)的方法,在方法②的基础上,并且还要从代码中指定默认的风格生成控件时,一般可以不重写该方法。

要获取控件已经设置好的属性的值,需要用到Context的方法先获取TypedArray对象:
public final TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs):第一个参数是在XML布局文件设置的该控件的所有属性集(AttributeSet),第二个参数表示描述该控件自定义属性的文件ID(R.styleable.xxx,即第二步中自定义的属性文件)。
从布局文件中获取属性数组 TypedArray后,然后用该对象的getxxx方法获取各种属性的值,最后回收属性数组。
TypedArray的getxxx方法用于获取属性集中指定属性名称的值,第一个参数为R.styleable.属性文件名_属性名,这种命名方式是Android SDK自动生成的,开发者不必奇怪。第二个参数是指定属性为空时使用的默认值。
不同数据类型的属性值对应的获取方法如下:
boolean:布尔,获取方法为getBoolean;
integer:整型,获取方法为getInteger;
float:小数,获取方法为getFloat;
string:字符串,获取方法为getString;
eum:枚举值,获取方法为getInt;
flag:标志位,获取方法为getInt;
color:颜色值,取值为开头带#的6或8位的十六进制数,获取方法为getColor;
dimension:尺寸,取值为末尾带尺寸单位的值,获取方法为getDimension;
fraction:百分数,取值为末尾带%的数,获取方法为getFraction;
reference:资源目录下的文件引用,获取此ID的方法为getResourceId。

获取控件的属性值之后,接着初始化控件的各种变量。那么写出自定义的摇杆控件的构造方法如下:

	public RockerView(Context context) {
        super(context);
    }

    //在含有两个参数的构造函数中获取XML文件中设置该控件的属性值
    public RockerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        if (attrs != null) {
            // 根据RockerView的属性定义,从布局文件中获取属性数组
            TypedArray attrArray = mContext.obtainStyledAttributes(attrs, R.styleable.RockerView);
            // 获取布局文件中的摇杆的底盘的背景贴图
            rockerPlate = BitmapFactory.decodeResource(mContext.getResources(),
                    attrArray.getResourceId(R.styleable.RockerView_rocker_plate_background, R.drawable.rocker_plate_background1));
            // 获取布局文件中的摇杆平均划分的扇形区域
            rockerSectorNum = attrArray.getInteger(R.styleable.RockerView_rocker_sector_num, rockerSectorNum);
            // 获取布局文件中的摇杆的扇形区域的颜色
            rockerSectorColor = attrArray.getColor(R.styleable.RockerView_rocker_sector_color, rockerSectorColor);
            // 获取布局文件中的摇杆的杆的颜色
            rockerBarColor = attrArray.getColor(R.styleable.RockerView_rocker_bar_color, rockerBarColor);
            // 获取布局文件中的摇杆的杆的宽度
            rockerBarWidth = attrArray.getInteger(R.styleable.RockerView_rocker_bar_width, rockerBarWidth);
            // 获取布局文件中的摇杆的球的颜色
            rockerBallColor = attrArray.getInteger(R.styleable.RockerView_rocker_ball_color, rockerBallColor);
            // 获取布局文件中的摇杆的球的半径
            rockerBallRadius = attrArray.getInteger(R.styleable.RockerView_rocker_ball_radius, rockerBallRadius);

            // 回收属性数组
            attrArray.recycle();
        }
        //根据从XML总获取的属性值设置相关画笔,一般绘制图片的画笔不需要特别的设置参数
        rockerBarPaint.setAntiAlias(true); // 设置画笔为无锯齿
        rockerBarPaint.setDither(true); // 设置画笔为防抖动
        rockerBarPaint.setColor(rockerBarColor); // 设置画笔的颜色
        rockerBarPaint.setStrokeWidth(rockerBarWidth); // 设置画笔的线宽
        rockerBarPaint.setStrokeCap(Paint.Cap.ROUND); //设置线段的端点形状
        rockerBarPaint.setStyle(Paint.Style.FILL); // 设置画笔的类型:STROKE表示空心,FILL表示实心
        rockerBallPaint.setAntiAlias(true);
        rockerBallPaint.setDither(true);
        rockerBallPaint.setColor(rockerBallColor);
        rockerBallPaint.setStyle(Paint.Style.FILL);
        rockerSectorPaint.setColor(rockerSectorColor);
        rockerSectorPaint.setStyle(Paint.Style.FILL);
    }

注意: 在XML布局文件中使用自定义控件时,需要在布局文件的根标签中添加命名空间的声明:xmlns:app="http://schemas.android.com/apk/res-auto"
这里xmlns:后面的app为命名空间的简短别名前缀,开发者可以自定义该名称。在布局文件中添加自定义控件时,必须使用该控件的全路径名称(再说一遍,开发者只需输入关键字母,AS会弹出备选框供我们选择,十分方便)。

要实现动图中的布局效果,页面布局文件的代码如下:

<FrameLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	
	xmlns:app="http://schemas.android.com/apk/res-auto"
	
	android:layout_width="match_parent"
	android:layout_height="match_parent">
	<com.example.myapplication.widget.RockerView
		android:id="@+id/rockerView1"
		android:layout_width="400px"
		android:layout_height="400px"
		android:layout_marginLeft="50px"
		android:layout_marginTop="50px"
		app:rocker_ball_color="@color/red"
		app:rocker_ball_radius="40"
		app:rocker_bar_color="@color/green"
		app:rocker_bar_width="30"
		app:rocker_sector_num="8"
		app:rocker_sector_color="@color/white"
		app:rocker_plate_background="@drawable/rocker_plate_background1">
	</com.example.myapplication.widget.RockerView>
	<com.example.myapplication.widget.RockerView
		android:id="@+id/rockerView2"
		android:layout_width="500px"
		android:layout_height="500px"
		android:layout_marginLeft="50px"
		android:layout_marginTop="1000px"
		app:rocker_ball_color="@color/blue"
		app:rocker_ball_radius="60"
		app:rocker_bar_color="@color/purple"
		app:rocker_bar_width="40"
		app:rocker_sector_num="16"
		app:rocker_sector_color="@color/black"
		app:rocker_plate_background="@drawable/rocker_plate_background2">
	</com.example.myapplication.widget.RockerView>
</FrameLayout>

4.测量尺寸

重写onMeasure测量方法,计算控件的宽和高。众所周知,在布局文件中对控件的宽和高有三种赋值方式:match_parent,wrap_content和具体带单位的尺寸值,在Java代码中分别对应布局参数 ViewGroup.LayoutParams的MATCH_PARENT,WRAP_CONTENT和具体整型数值。其中控件如果被设置为match_parent和具体的数值的话,都很容易计算出控件的尺寸:被设置为具体带单位的尺寸值的话,就直接获取该值就行了。被设置为match_parent时,就是与父控件的尺寸相同,当前控件就不需要计算自己的尺寸了。至于wrap_content的情况则需要开发者自己计算本控件的尺寸。
一般来说,自定义控件的内部中的主要内容有三类:文字,图片,子控件。不同内容的测量方式如下:
(1)文字
文字的宽度使用Paint类的measureText方法测得,至于文字的高度则稍微复杂点,大家都知道我们在学习写英文字母的时候,使用的是四线格来练习的:

四线格从上往下的第三条线称作基线,使用四线格可以规范字母的位置,比如确定了一行文字基线的位置,那么该行文字的位置也就确定了。而在Android中对文字的定位正是以基线为参考线的(比如使用Canvas的drawText方法绘制文字时,其参数Y坐标就是基线在屏幕的Y坐标),如图:

除了基线外,还有四条辅助线:
top: 文字所在行的最高高度所在线。
ascent: 单个文字的最高高度所在线。
descent:单个文字的的最低高度所在线。
bottom: 文字所在行的最低高度所在线。

字体尺寸 Paint.FontMetrics提供了与这几条线相关的属性:
top,行顶与基线的距离。
ascent,字符顶与基线的距离。
descent,字符低与基线的距离。
bottom,行低与基线的距离。
leading,行间距。
注意,top,ascent,descent,bottom的值都是相对于基线的距离而得到的,并不是在屏幕坐标系下的Y坐标的值,即只有确定了基线的Y坐标,这四条辅助线的Y坐标才能确定。

那么要得到文字自身的高度,可用descent减去ascent,要得到文字所在行的高度,可用bottom减去top,再加上leading。测量文本尺寸的代码如下:

 // 获取文本的宽度
    public float getTextWidth(String text, float textSize) {
        if (TextUtils.isEmpty(text)) {
            return 0;
        }
        Paint paint = new Paint(); // 创建一个画笔对象
        paint.setTextSize(textSize); // 设置画笔的文本大小
        return paint.measureText(text); // 利用画笔丈量指定文本的宽度
    }
    // 获取文本的高度
    public float getTextHeight(String text, float textSize) {
        Paint paint = new Paint(); // 创建一个画笔对象
        paint.setTextSize(textSize); // 设置画笔的文本大小
        FontMetrics fm = paint.getFontMetrics(); // 获取画笔默认字体的度量衡
        return fm.descent - fm.ascent; // 返回文本自身的高度
        //return fm.bottom - fm.top + fm.leading;  // 返回文本所在行的行高
    }

(2)图形尺寸的测量:若图形是用位图对象Bitmap表示的,可通过位图对象的getWidth和getHeight方法获取宽高。若图形是Drawable对象表示的,则可通过它的getIntrinsicWidth和getIntrinsicHeight方法获取宽高。

(3)子控件的测量
自定义控件中可能含有许多其他的子控件,如果一个个去测量这些子控件的尺寸的话,那开发者就太难受了,好在View默认提供了一种对所有子控件的测量思路:实现了在整个控件树中从父控件向子控件的遍历测量,每个控件在遍历过程中将自身的尺寸信息保存起来,然后向下传递,这样遍历一次整个控件树,就得到了所有控件的尺寸信息。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 是该测量思路的主要实现方法,它的两个参数是从父控件传过来的值,是父控件想让子控件的宽高满足的建议值,这种值由mode + size两部分组成:
①mode的获取通过MeasureSpec的getMode方法获取,mode的取值有三种:
MeasureSpec.UNSPECIFIED:对应在XML布局文件中将该控件的尺寸设置为wrap_content的情况,这时父控件没有办法给出适当的建议尺寸值,故需要开发者自己计算本控件的尺寸。
MeasureSpec.EXACTLY:父控件给出具体的建议尺寸数值。
MeasureSpec.AT_MOST:父控件给出当前控件可以被设置的最大宽高。
②size的获取通过MeasureSpec的getSize方法。当mode取值为EXACTLY或者AT_MOST时,可得到具体的size。
当开发者在XML布局文件中将控件尺寸设置好之后,然后在onMeasure中计算好控件的宽和高之后,还需要在onMeasure方法中最后调用setMeasuredDimension方法设置控件最终的尺寸。

本例中摇杆控件尺寸的测量过程如下:

    private final int ROCKER_VIEW_SIZE = 400;//本视图默认的尺寸大小

    //通过onMeasure函数测量本视图大小
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.android基础篇学习心得

Android Studio应用基础,手把手教你从入门到精通(小白学习)总结1 之 基础介绍 + intent + 常用控件

android 基础UI控件学习总结

简单的学习心得:网易云课堂Android开发第三章自定义控件

Android学习笔记 布局基础

Android零基础入门第31节:几乎不用但要了解的AbsoluteLayout绝对布局