自定义控件初步
有时或许为了与众不同吸引别人的目光,也许是需要实现特殊的功能样式,默认的控件已经满足不了我们,我们就会进行自定义控件,这个并不简单,官方默认的控件还会出一些bug,更不要说我们自己进行改造的了,并且使用这项技能,还需要了解一定的UI绘制基础,这里也只是先暂时了解下,等看完UI绘制相关再回来深入。
这部分大部分人都认为是有难度的,也是通向高手的必经之路,在这方面要多下工夫呢,现在我就开始入门阶段啦….
开始
自定义view首先还是要先继承view的,一些所必需的依赖官方已经帮我们写好了,所以直接继承拿来用就好,在此基础上进行修改定制。
定义一个SketchView来了解下构造方法的作用
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class SketchView extends View{
public SketchView(Context context) {
super(context);
}
public SketchView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SketchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
|
-
第一个构造方法就是我们普通在代码中新建一个view用到的方法,例如
1SketchView sketchView = new SketchView(this);就这样,一个自定义的view就被新建出来了,然后可以根据需求添加到布局里(获取一个ViewGroup调用addView()方法)
-
第二个构造方法就是我们一般在xml文件里添加一个view
12345<me.shaohui.androidpractise.widget.SketchViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginRight="16dp"android:layout_marginTop="16dp" />这样,我们就把一个SketchView添加到布局文件里,并且加了一些布局属性,宽高属性以及margin属性,这些属性会存放在第二个构造函数的AttributeSet参数里
-
第三个构造函数比第二个构造函数多了一个int型的值,名字叫defStyleAttr,从名称上判断,这是一个关于自定义属性的参数,实际上我们的猜测也是正确的,第三个构造函数不会被系统默认调用,而是需要我们自己去显式调用,比如在第二个构造函数里调用调用第三个函数,并将第三个参数设为0。
关于第三个参数defStyleAttr,其实也可以拿出来说一整篇文章,有想详细了解的读者可以去看下本篇文章最后的第三个参考链接,我在这里只是简单的说一下:defStyleAttr指定的是在Theme style定义的一个attr,它的类型是reference,主要生效在obtainStyledAttributes方法里,obtainStyledAttributes方法有四个参数,第三个参数是defStyleAttr,第四个参数是自己指定的一个style,当且仅当defStyleAttr为0或者在Theme中找不到defStyleAttr指定的属性时,第四个参数才会生效,这些指的都是默认属性,当在xml里面定义的,就以在xml文件里指定的为准,所以优先级大概是:xml>style>defStyleAttr>defStyleRes>Theme指定,当defStyleAttr为0时,就跳过defStyleAttr指定的reference,所以一般用0就能满足一些基本开发。
关于绘制view流程
在学会如何写一个自定义控件之前,了解一个控件的绘制流程是必要的,在Android里,一个view的绘制流程包括:Measure,Layout和Draw,通过onMeasure知道一个view要占界面的大小,然后通过onLayout知道这个控件应该放在哪个位置,最后通过onDraw方法将这个控件绘制出来,然后才能展现在用户面前。
onMeasure 测量
通过测量知道一个一个view要占的大小,方法参数是两个int型的值,我们都知道,在java中,int型由4个字节(32bit)组成,在MeasureSpce中,用前两位表示mode,用后30位表示size。
MeasureSpce的mode有三种:EXACTLY, AT_MOST,UNSPECIFIED,除却UNSPECIFIED不谈
需要明白的一点是 ,测量所得的宽高不一定是最后展示的宽高,最后宽高确定是在onLayout方法里,layou(left,top,right,bottom),不过一般都是一样的。
更多内容我还写了一篇:Android控件架构,虽然我感觉这个系列写的乱七八糟的
onLayout
我们在自定义SketchView的时候是没有重写onLayout方法的,因为SketchView只是一个单纯的view,它不是一个view容器,没有子view,而onLayout方法里主要是具体摆放子view的位置,水平摆放或者垂直摆放,所以在单纯的自定义view是不需要重写onLayout方法,不过需要注意的一点是,子view的margin属性是否生效就要看parent是否在自身的onLayout方法进行处理,而view得padding属性是在onDraw方法中生效的。
第一个参数boolean:changed,标示这个view的大小是否发生改变,
onDraw
终于说到了重头戏,一般自定义控件耗费心思最多的就是这个方法了,需要在这个方法里,用Paint在Canvas上画出你想要的图案,这样一个自定义view才算结束。下面会详细讲如何在画布上画出自己想要的图案。
关于onDraw方法,在补充一句,如果是直接继承的View,那么在重写onDraw的方法是时候完全可以把super.ondraw(canvas)删掉,因为它的默认实现是空。
关于view的绘制流程,我是打算拿出时间专门的去学习,待续…
自绘相关
自定义view的时候,我们通常会去重写onDraw()方法来绘制view的显示内容,如果该view还需要wrap_content属性,那么我们还要重写onMeasure()方法。
下面说下比较重要的回调方法:
- onFinishInflate(): 从XML加载组件后回调
- onSizeChanged(): 组件大小改变的时候回调
- onMeasure(): 回调该方法进行测量
- onLayout(): 回调该方法确定显示位置
- onTouchEvent(): 监听到触摸事件时回调
对现有控件进行拓展,这也是我们经常搞的,如果我们不重写onDraw那么就不会有任何变化,大体是下面所示:
|
1
2
3
4
5
6
|
protected void onDraw(Canvas canvas) {
//回调父类方法前实现自己的逻辑,绘制内容前 就是画东西啦
super.onDraw(canvas);
//回调父类方法后,绘制内容后 一般是添加一些效果
}
|
这里确实是有些复杂的,又要先屯起来了…..我还年轻,慢慢来!
自定义属性设置
自定义view很多情况是配合自定属性使用的,要给view支持自定义属性,需要在values/attrs.xml 文件里定义一个name为自己定义view名字(当然不是强制的,在自定义view的构造方法中获取的时候会用到)的declare-styleable。关于自定义属性的格式介绍见补充。
|
1
2
3
4
5
6
|
<resources>
<declare-styleable name="SketchView">
<attr name="background_color" format="color"/>
<attr name="size" format="dimension"/>
</declare-styleable>
</resources>
|
这样就可以在XML文件里进行使用了,如果你问那个app/Android是什么意思可以去看看补充。
|
1
2
3
4
5
6
7
8
9
|
<me.androidpractise.widget.SketchView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
//下面是自定义属性
app:background_color="@color/colorPrimary"
app:size="24dp"/>
|
如果要让自定义的属性生效,还是有点麻烦的,总体就是在构造函数中(还记得第三个构造函数么)使用TypedArray获取。
|
1
2
3
4
5
6
7
8
9
10
|
public SketchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SketchView, defStyleAttr, R.style.AppTheme);
custom_size = a.getDimensionPixelSize(R.styleable.SketchView_size, SIZE);
custon_background = a.getColor(R.styleable.SketchView_background_color, DEFAULT_COLOR);
a.recycle();
}
|
下面我们来分析下
先看AttributeSet这个参数,一看就是一个集合,实际上确实是,它保存了所有的定义的属性信息,以键值对的形式保存,可以用attrs.getAttributeName(i)、attrs.getAttributeValue(i);这两个方法进行获取。
然后就是TypedArray这个东西了,你可能发现了,如果属性值是引用类型的话,用AttributeSet获取到的是“@数字”这样的字符串,如果要获得引用的资源那么就要第一步拿到资源id,第二步再去解析id。很麻烦,用TypedArray就简单多了,直接就可以获取到,其实就是起到了资源映射的作用。
用完之后记得调用 recycle() 函数回收资源。
更多更详细的介绍请移步这里
然后说以下declare-styleable标签,它的作用就是为了简化代码,让R.styleable.SketchView_background_color这种形式可用,具体的原理可以去问谷狗。
刷新view的方法
这里可以参考下我以前挖的坑关于事件的简单说明点这里
这时候就会有三个方法供我们选择:requestLayout()、invalidate()、postInvalidate(),其实后两个方法invalidate和postInvalidate这两个方法作用是一样的,唯一不同的是invalidate用在主线程,而postInvalidate用在异步线程,下面对比一下它们的内部实现:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
|
简单说下结论:
- requestLayout会调用measure和layout 等一系列操作,然后根据布局是否发生改变,surface是否被销毁,来决定是否调用draw,也就是说requestlayout肯定会调用measure和layout,但不一定调用draw。
- invalidate 只会调用draw,而且肯定会调,即使什么都没有发生改变,它也会重新绘制。
所以如果有布局需要发生改变,需要调用requestlayout方法,如果只是刷新动画,则只需要调用invalidate方法。
补充
关于在XML里的引用
至于Android: / app: 代表了什么,上面其实有几行关于引用空间的代码应该贴出来的,不过通过下面的两张图应该就会明白了。
dp、px等单位介绍
-
dpi
dpi的全称是Dots Per Inch,即点每英寸,一般被称为像素密度,它代表了一英寸里面有多少个像素点。计算方法为屏幕总像素点(即分辨率的乘积除以屏幕大小),常见的取值有120,160,240。
-
density
density直译为密度,它的计算公式为屏幕dpi除以160点每英寸,由于单位除掉了,故density只是一个比值,常见取值为1.0,1.5等。在Android中我们可以通过下面代码获取当前屏幕的density:
getResources().getDisplayMetrics().density; -
dp(dip)
dp,也叫做dip,全称为Density independent pixels,叫做设备独立像素。他是Android为了解决众多手机dpi不同所定义的单位,谷歌官方的解释如下:
Density-independent pixel (dp)
A virtual pixel unit that you should use when defining UI layout, to express layout dimensions or position in a density-independent way.The density-independent pixel is equivalent to one physical pixel on a 160 dpi screen, which is the baseline density assumed by the system for a “medium” density screen. At runtime, the system transparently handles any scaling of the dp units, as necessary, based on the actual density of the screen in use. The conversion of dp units to screen pixels is simple: px = dp * (dpi / 160). For example, on a 240 dpi screen, 1 dp equals 1.5 physical pixels. You should always use dp units when defining your application’s UI, to ensure proper display of your UI on screens with different densities.
从上文我们可以看出,dp是一种虚拟抽象的像素单位,他的计算公式为:px = dp (dpi / 160) = dp density。因此在dpi大小为160的手机上,1dp = 1px,而在dpi大小为320的手机上,1dp = 2px,即在屏幕越大的手机上,1dp代表的像素也越大。因此我们定义控件大小的时候应该使用dp代替使用px。
关于他们之间的转换,Java代码的实现如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class DensityUtil {
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
|
自定义属性的格式
reference:参考某一资源ID。
(1)属性定义:
1234 > <declare-styleable name = "名称">> <attr name = "background" format = "reference" />> </declare-styleable>>
>
(2)属性使用:
12345 > <ImageView> android:layout_width = "42dip"> android:layout_height = "42dip"> android:background = "@drawable/图片ID"/>>
>
>
color:颜色值。
(1)属性定义:
1234 > <declare-styleable name = "名称">> <attr name = "textColor" format = "color" />> </declare-styleable>>
>
(2)属性使用:
12345 > <TextView> android:layout_width = "42dip"> android:layout_height = "42dip"> android:textColor = "#00FF00"/>>
>
>
boolean:布尔值。
(1)属性定义:
1234 > <declare-styleable name = "名称">> <attr name = "focusable" format = "boolean" />> </declare-styleable>>
>
(2)属性使用:
12345 > <Button> android:layout_width = "42dip"> android:layout_height = "42dip"> android:focusable = "true"/>>
>
>
dimension:尺寸值。
(1)属性定义:
1234 > <declare-styleable name = "名称">> <attr name = "layout_width" format = "dimension" />> </declare-styleable>>
>
(2)属性使用:
1234 > <Button> android:layout_width = "42dip"> android:layout_height = "42dip"/>>
>
>
float:浮点值。
(1)属性定义:
12345 > <declare-styleable name = "AlphaAnimation">> <attr name = "fromAlpha" format = "float" />> <attr name = "toAlpha" format = "float" />> </declare-styleable>>
>
(2)属性使用:
1234 > <alpha> android:fromAlpha = "1.0"> android:toAlpha = "0.7"/>>
>
>
integer:整型值。
(1)属性定义:
123456789 > <declare-styleable name = "AnimatedRotateDrawable">> <attr name = "visible" />> <attr name = "frameDuration" format="integer" />> <attr name = "framesCount" format="integer" />> <attr name = "pivotX" />> <attr name = "pivotY" />> <attr name = "drawable" />> </declare-styleable>>
>
(2)属性使用:
12345678 > <animated-rotate> xmlns:android = "http://schemas.android.com/apk/res/android"> android:drawable = "@drawable/图片ID"> android:pivotX = "50%"> android:pivotY = "50%"> android:framesCount = "12"> android:frameDuration = "100"/>>
>
>
string:字符串。
(1)属性定义:
1234 > <declare-styleable name = "MapView">> <attr name = "apiKey" format = "string" />> </declare-styleable>>
>
(2)属性使用:
12345 > <com.google.android.maps.MapView> android:layout_width = "fill_parent"> android:layout_height = "fill_parent"> android:apiKey = "0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g"/>>
>
>
fraction:百分数。
(1)属性定义:
123456789 > <declare-styleable name="RotateDrawable">> <attr name = "visible" />> <attr name = "fromDegrees" format = "float" />> <attr name = "toDegrees" format = "float" />> <attr name = "pivotX" format = "fraction" />> <attr name = "pivotY" format = "fraction" />> <attr name = "drawable" />> </declare-styleable>>
>
(2)属性使用:
1234567891011 > <rotate> xmlns:android="http://schemas.android.com/apk/res/android"> android:duration="5000"> android:fromDegrees="0"> android:interpolator="@anim/动画ID"> android:pivotX="200%"> android:pivotY="300%"> android:repeatCount="infinite"> android:repeatMode="restart"> android:toDegrees="360" />>
>
>
enum:枚举值。
(1)属性定义:
1234567 > <declare-styleable name="名称">> <attr name="orientation">> <enum name="horizontal" value="0" />> <enum name="vertical" value="1" />> </attr>> </declare-styleable>>
>
(2)属性使用:
12345678 > <LinearLayout> xmlns:android = "http://schemas.android.com/apk/res/android"> android:orientation = "vertical"> android:layout_width = "fill_parent"> android:layout_height = "fill_parent"> >> </LinearLayout>>
>
>
flag:位或运算。
(1)属性定义:
123456789101112131415 > <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>>
>
(2)属性使用:
12345678910 > <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>>注意:
属性定义时可以指定多种类型值。
(1)属性定义:
1234 > <declare-styleable name = "名称">> <attr name = "background" format = "reference|color" />> </declare-styleable>>
>
(2)属性使用:
12345 > <ImageView> android:layout_width = "42dip"> android:layout_height = "42dip"> android:background = "@drawable/图片ID|#00FF00"/>>



浙公网安备 33010602011771号