- 自定义View的流程 ⭐⭐⭐⭐⭐
- 自定义View需要重写哪些函数?说说你在自定义View时常常重写的一些方法? ⭐⭐⭐⭐
- 自定义View的种类有哪些?给我说说你之前项目中的案例。⭐⭐⭐⭐
- 说说自定义View中如何自定义属性?⭐⭐⭐
- 自定义View如何处理padding?⭐⭐
- 自定义View效率高于xml布局文件吗?⭐⭐
- 自定义View什么时候需要处理wrap_content属性?怎么处理?⭐
什么是自定义View
View的绘制有测量 -〉布局 -〉绘制,这三大步骤
自定义View和自定义ViewGroup
- 自定义View:如果官方提供现成的View控件无法达到符合自己预期的View的样式,那就需要自己来实现,一般需要重写onDraw()方法来设置绘制的样式,这就是自定义View;
- 自定义ViewGroup:如果希望将一个或多个现有的View按照特定的布局方式,组装形成一个新的组件,这就是自定义ViewGroup。
自定义View基础知识
坐标系
在安卓系统中,屏幕左上角为原点,往右边是X轴正向,往下边是Y轴正向。常见API函数如下:
1 2 3 4 5 6 7 8 9
| getHeight() getWidth() getTop() getLeft() getBottom() getRight() getBottom() - getTop() = 子View 的高 getRight() - getLeft() = 子View 的宽
|
1 2 3 4 5
| event.getX() event.getY() event.getRawX() event.getRawY()
|
详细可参考下图(抄录于参考文档1),其中绿色方块为子View,子View里面的蓝色小圆圈代表触摸点,子View外边依次是父View和屏幕坐标。
![image]()
颜色
Android系统支持的颜色模式有以下三种:
颜色模式 |
备注 |
ARGB8888 |
四通道高精度(32位) |
ARGB4444 |
四通道低精度(16位) |
RGB565 |
屏幕默认模式(16位) |
其中A代表透明度,RGB分别代表红绿蓝三种原色,后面的数值代表该模式用多少位二进制数来表示,比如:
1 2 3 4
| #0xF00 #0xAF00 #0xFF0000 #0xAAFF0000
|
触摸事件
既然是View,那就离不开触摸事件,常见的触摸事件如下:
事件 |
简介 |
ACTION_DOWN |
手指初次接触屏幕时触发 |
ACTION_MOVE |
手指在屏幕上滑动时触发,会多次触发 |
ACTION_UP |
手指离开屏幕时触发 |
ACTION_CANCEL |
事件被上层拦截时触发 |
margin和padding
在开发中,经常可以看到这两个,在此再次介绍下:
- margin:子控件与父控件的距离,也就是“外边距”;
- padding:控件里内容和控件的边界之间的距离,也就是“内边距”。在使用系统自带的控件时,只要在xml布局文件设置好padding即刻生效,但在自定义View则不会生效,需要手动在onDraw()方法里处理。
自定义View效率高于xml布局文件吗?
自定义View效率高于xml定义,原因如下:
- 自定义View少了解析xml;
- 自定义View 减少了ViewGroup与View之间的测量,包括父量子,子量自身,子在父中位置摆放,当子view变化时,父的某些属性都会跟着变化。
自定义View的流程
自定义View有一个通用的流程:
![image]()
onMeasure()
在Measure阶段,需根据需要重写onMeasure()方法,即使在xml布局文件里面设置了View的宽高。因为一个子View的宽高不止受自身参数决定,还需要受到父控件的影响。
onLayout()
确定布局可以用onLayout()方法,在自定义View中,一般不需要重写该方法。但在自定义ViewGroup中可能需要重写,一般做法是循环取出子View,并计算每个子View位置等坐标值,然后使用child.layout()方法设置子View的位置,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int left = 0; View child; for (int i = 0; i < childCount; i++) { child = getChildAt(i); if (child.getVisibility() != View.GONE) { int width = child.getMeasuredWidth(); childWidth = width; child.layout(left, 0, left + width, child.getMeasuredHeight()); left += width; } } }
|
onDraw()
Canvas(画布)
这是实际绘制的部分,使用Canvas(画布)进行绘制常见的Canvas API函数如下:
操作类型 相关 API 备注
|
|
|
绘制颜色 |
drawColor、drawRGB、drawARGB |
使用单一颜色填充整个画布 |
绘制基本图形 |
drawPoint、drawPoints、drawLine、drawLines、drawRect、drawRoundRect、drawOval、drawCircle、drawArc |
绘制点、线、矩形、圆角矩形、椭圆、圆、圆弧 |
绘制图片 |
drawBitmap、drawPicture |
绘制位图和图片 |
绘制路径 |
drawPath |
绘制路径,绘制贝塞尔曲线 |
画布裁剪 |
clipPath、clipRect |
设置画布的显示区域 |
画布变换 |
translate、scale、rotate、skew |
位移、缩放、旋转、错切 |
Paint(画笔)
Paint(画笔)在自定义View的实现也是非常常见的,所以需要了解常见的API函数,详情可见:Android开发手册-Paint
以下是Paint常用API函数:Android Paint API总结和使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| void reset(); void set(Paint src); void setCompatibilityScaling( float factor); void setBidiFlags( int flags); void setFlags( int flags); void setHinting( int mode);
void setAntiAlias( boolean aa);
void setDither( boolean dither);
void setLinearText( boolean linearText);
void setSubpixelText( boolean subpixelText);
void setUnderlineText( boolean underlineText);
void setStrikeThruText( boolean strikeThruText);
void setFakeBoldText( boolean fakeBoldText);
void setFilterBitmap( boolean filter);
void setStyle(Style style); void setColor( int color);
void setAlpha( int a);
void setARGB( int a, int r, int g, int b);
void setStrokeWidth( float width); void setStrokeMiter( float miter);
void setStrokeCap(Cap cap);
void setStrokeJoin(Join join);
Shader setShader(Shader shader);
ColorFilter setColorFilter(ColorFilter filter);
Xfermode setXfermode(Xfermode xfermode);
PathEffect setPathEffect(PathEffect effect);
MaskFilter setMaskFilter(MaskFilter maskfilter);
Typeface setTypeface(Typeface typeface);
Rasterizer setRasterizer(Rasterizer rasterizer);
void setShadowLayer( float radius, float dx, float dy, int color);
void setTextAlign(Align align);
void setTextSize( float textSize);
void setTextScaleX( float scaleX);
void setTextSkewX( float skewX);
|
Path(路径)
Path类封装了由直线段,二次曲线和三次曲线组成的复合(多轮廓)几何路径。 它可以用canvas.drawPath(path,paint)绘制,可以是填充或描边(基于绘制的样式),也可以用于剪裁或在路径上绘制文本。 详情见:Android开发手册-Path
常见的自定义View类型
如1.1小节说的,自定义View主要可以分为自定义View和自定义ViewGroup两种,常见的自定义View分为以下4种类型:
- 继承系统提供的现有控件的自定义View;
- 继承View类的自定义View;
- 将多个单一的View合成复杂的自定义组合View;
- 继承ViewGroup类的自定义View(引导);
接下来根据每种类型依次介绍。
继承系统提供的现有控件的自定义View
继承系统控件,一般是为了在系统控件上增加新的特性,可以在onDraw()方法里进行处理即可。系统控件TextView可以正常设置背景颜色和文本内容,但如果需要增加背景线条等特殊操作,正常的TextView的API函数是无法做到的。这时候就可以通过自定义View来实现了:
![image]()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class MyTextView extends TextView { private Paint mPaint = new Paint(Paint.DITHER_FLAG);
public MyTextView(Context context) { super(context); initDraw(); }
public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); initDraw(); }
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initDraw(); }
private void initDraw() { mPaint.setColor(Color.BLUE); mPaint.setStrokeWidth((float) 1.5); }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = getWidth(); int height = getHeight(); canvas.drawLine(0, 0, width, height, mPaint); canvas.drawLine(0, height, width, 0, mPaint); } }
|
通过以上的代码,就自定义了一个继承TextView的自定义View,这时候只要在xml文件里直接引用该控件即可:
1 2 3 4 5 6 7 8 9
| <com.example.android.MyTextView android:id="@+id/iv_text" android:layout_width="250dp" android:layout_height="150dp" android:textSize="15sp" android:background="@android:color/blue" android:layout_centerHorizontal="true" android:text="自定义TextView" />
|
继承View类的自定义View
注意事项
上面继承系统控件相对简单,如果是继承View类的自定义View,就不仅要重写onDraw()方法,还要考虑以下几点:
- padding属性处理;
- wrap_content属性处理;
- 提供自定义属性,方便自定义View的属性配置;
- 如果涉及触摸操作,还需要重写onTouchEvent()方法来处理触摸事件;
既然是继承View类来创造一个新的View控件,我们画一个贝塞尔曲线,就是仿手机边缘滑动时的曲线图:
![image]()
直接上代码(为方便展示,以下是最终版本的代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| public class BezierView extends View { private Path bezierPath; private Paint paint; private int mColor=Color.BLACK;
public BezierView(Context context) { super(context); initDraw(); }
public BezierView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray mTypedArray=context.obtainStyledAttributes(attrs,R.styleable.BezierView); mColor=mTypedArray.getColor(R.styleable.bezier_color,Color.BLACK); mTypedArray.recycle(); initDraw(); }
public BezierView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initDraw(); }
private void initDraw() { bezierPath = new Path(); paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL); paint.setColor(Color.RED); paint.setStrokeWidth((float) 1.5); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int defaultValue = 700; int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec); if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(defaultValue,defaultValue); }else if(widthSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(defaultValue,heightSpecSize); }else if(heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,defaultValue); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float currentWidth = 200; float height = getHeight(); int maxWidth = getWidth(); float centerY = height / 2; float progress = currentWidth / maxWidth; if (progress == 0) { return; }
paint.setColor(mColor); paint.setAlpha((int) (500 * progress));
float bezierWidth = currentWidth / 2; float coordinateX = 0;
bezierPath.reset(); bezierPath.moveTo(coordinateX, 0); bezierPath.cubicTo(coordinateX, height / 4f, bezierWidth, height * 3f / 8, bezierWidth, centerY); bezierPath.cubicTo(bezierWidth, height * 5f / 8, coordinateX, height * 3f / 4, coordinateX, height); canvas.drawPath(bezierPath, paint); } }
|
上述代码都做了详细的注释,先是初始化画笔,接着在onDraw()方法按照我们自己的思路,绘制贝塞尔曲线,接着只要在xml布局文件引用该自定义View即可(这几行代码下文多次讲到,记得回来这里看):
1 2 3 4 5 6 7 8 9 10
| <com.example.android.BezierView xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/rv_bezier" android:layout_width="match_parent" <!-- 3 --> android:layout_height="200dp" android:layout_below="@id/iv_text" android:layout_centerHorizontal="true" android:layout_marginTop="50dp" android:padding="10dp" <!-- 4 --> app:bezier_color="@android:color/black" /> <!-- 5 -->
|
处理padding
上面[注释4]
指定的padding(内边距),会发现无论修改到任何数值,绘制出来的View都不受影响,这是因为我们需要在onDraw()方法里做特殊处理才能显示出效果。至于系统控件设置padding数值可以生效,正是系统帮我们处理好了。
因此,在BezierView的onDraw()中的[注释2]
需要做如下修改:
1 2 3 4 5 6
| float coordinateX = 0;
int paddingLeft = getPaddingLeft(); float coordinateX = paddingLeft;
|
其中coordinateX是贝塞尔曲线横坐标的开始点,我们需要手动获取xml布局文件设置的偏差值,手动的修改coordinateX,如此就可以使padding值生效。效果图如下,可以发现图像确实往中间偏移了一些:
![image]()
wrap_content属性处理
在xml布局文件修改android:layout_width属性为match_parent或者wrap_content,最终发现效果都是一样的。导致这个原因,在参考文档3的2.3小节有介绍,我们需要在onMeasure()方法里给wrap_content属性设置默认宽高值,代码已经在上面BezierView类写清楚了,即[注释1]
。接着,看看android:layout_width属性设置为match_parent或者wrap_content的区别(为了更直观看出差别,直接给BezierView整个控件设置了背景颜色):
- android:layout_width=match_parent或者android:layout_width=wrap_content但没有重写onMeasure()
![image]()
- android:layout_width=wrap_content并且重写onMeasure()
![image]()
添加自定义属性
我们看5.1小节最后的代码,以android:
开头的都是系统自带的属性,而[注释5]
:app:bezier_color,是自定义的属性。只要在values目录下创建attrs.xml文件:
1 2 3 4 5 6
| <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="BezierView"> <attr name="bezier_color" format="color" /> </declare-styleable> </resources>
|
在这个文件我们设置了一个名为BezierView的自定义属性组合,里面可以有多个属性,目前只需要一个颜色格式的属性:bezier_color。可以根据需要添加多个属性。创建好后,我们看看如何使用,有两个地方需要修改:
自定义属性需要添加:
1 2
| xmlns:app="http://schemas.android.com/apk/res-auto" <!-- 6 --> app:bezier_color="@android:color/black" /> <!-- 7 -->
|
使用自定义属性,都要添加[注释6]
。其中app
是自定义的名字,可以改为其他的。最后在[注释7]
配置为黑色。
- Step2:代码修改 在上面BezierView的代码里有如下构造函数:
1 2 3 4 5 6 7 8 9 10
| public BezierView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray mTypedArray=context.obtainStyledAttributes(attrs,R.styleable.BezierView); mColor=mTypedArray.getColor(R.styleable.bezier_color,Color.BLACK); mTypedArray.recycle(); initDraw(); }
|
将多个单一的View合成复杂的自定义组合View
Android系统自带的AlertDialog比较丑,因此想要自定义一个MyDialogView,如下图:
![image]()
可以简单的看出,这个自定义组合View包含至少2个TextView、2个Button、1个ImageView。还是先上最终完整代码,再进行解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
| public class MyDialogView extends Dialog {
public MyDialogView(Context context) { super(context); }
public MyDialogView(Context context, int theme) { super(context, theme); }
public static int px2dip(Context context, float pxValue) { float scale = context.getResources().getDisplayMetrics().density; return (int) (pxValue / scale + 0.5f); }
@Override protected void onStart() { super.onStart(); getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); getWindow().setLayout(px2dip(getContext(), 2010), LinearLayout.LayoutParams.WRAP_CONTENT); }
public static class Builder { private final Context context; private String title; private String message; private String positiveButtonText; private String negativeButtonText; private DialogInterface.OnClickListener positiveButtonClickListener; private DialogInterface.OnClickListener negativeButtonClickListener; private Drawable mIcon;
public Builder(Context context) { this.context = context; }
public void setIcon(Drawable icon) { mIcon = icon; }
public Builder setMessage(String message) { this.message = message; return this; }
public Builder setMessage(int message) { this.message = (String) context.getText(message); return this; }
public Builder setTitle(int title) { this.title = (String) context.getText(title); return this; }
public Builder setTitle(String title) { this.title = title; return this; }
public Builder setPositiveButton(int positiveButtonText, DialogInterface.OnClickListener listener) { this.positiveButtonText = (String) context .getText(positiveButtonText); this.positiveButtonClickListener = listener; return this; }
public Builder setPositiveButton(String positiveButtonText, DialogInterface.OnClickListener listener) { this.positiveButtonText = positiveButtonText; this.positiveButtonClickListener = listener; return this; }
public Builder setNegativeButton(int negativeButtonText, DialogInterface.OnClickListener listener) { this.negativeButtonText = (String) context .getText(negativeButtonText); this.negativeButtonClickListener = listener; return this; }
public Builder setNegativeButton(String negativeButtonText, DialogInterface.OnClickListener listener) { this.negativeButtonText = negativeButtonText; this.negativeButtonClickListener = listener; return this; }
public MyDialogView create() { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); final MyDialogView dialog = new MyDialogView(context); View layout = inflater.inflate(R.layout.my_alert_dialog, null); dialog.addContentView(layout, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (mIcon != null) { layout.findViewById(R.id.my_alert_dialog_icon).setVisibility(View.VISIBLE); ((ImageView) layout.findViewById(R.id.my_alert_dialog_icon)).setImageDrawable(mIcon); } else { layout.findViewById(R.id.my_alert_dialog_icon).setVisibility(View.GONE); }
if (title != null) { ((TextView) layout.findViewById(R.id.my_alert_dialog_title)).setText(title); ((TextView) layout.findViewById(R.id.my_alert_dialog_title)).setVisibility(View.VISIBLE); } else { layout.findViewById(R.id.my_alert_dialog_title).setVisibility(View.GONE); }
if (positiveButtonText != null) { ((TextView) layout.findViewById(R.id.my_alert_dialog_button_positive)) .setVisibility(View.VISIBLE); ((TextView) layout.findViewById(R.id.my_alert_dialog_button_positive)) .setText(positiveButtonText); if (positiveButtonClickListener != null) { layout.findViewById(R.id.my_alert_dialog_button_positive) .setOnClickListener(new View.OnClickListener() { public void onClick(View v) { positiveButtonClickListener.onClick(dialog, DialogInterface.BUTTON_POSITIVE); } }); } } else { layout.findViewById(R.id.my_alert_dialog_button_negative).setVisibility( View.GONE); }
if (negativeButtonText != null) { ((TextView) layout.findViewById(R.id.my_alert_dialog_button_negative)) .setVisibility(View.VISIBLE); ((TextView) layout.findViewById(R.id.my_alert_dialog_button_negative)) .setText(negativeButtonText); if (negativeButtonClickListener != null) { layout.findViewById(R.id.my_alert_dialog_button_negative) .setOnClickListener(new View.OnClickListener() { public void onClick(View v) { negativeButtonClickListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE); } }); } } else { layout.findViewById(R.id.my_alert_dialog_button_negative).setVisibility( View.GONE); }
if (message != null) { ((TextView) layout.findViewById(R.id.my_alert_dialog_message)).setVisibility(View.VISIBLE); ((TextView) layout.findViewById(R.id.my_alert_dialog_message)).setText(message); } else { ((TextView) layout.findViewById(R.id.my_alert_dialog_message)).setVisibility(View.GONE); }
dialog.setContentView(layout); return dialog; } } }
|
在MainActivity里面可以这么使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MyDialogView.Builder builder = new MyDialogView.Builder(MainActivity.this); builder.setIcon(getDrawable(R.drawable.mydialog)); builder.setMessage("这是消息内容"); builder.setTitle("这是一个标题"); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); builder.setNegativeButton("取消", new android.content.DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); builder.create().show(); }
|
上面的代码基本每个函数都做了注释。自定义组合View的关键是先写好xml布局文件,然后在代码里去动态
的操作xml布局文件里的各种控件。先附上xml布局文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/install_RelativeLayout" android:layout_width="760dp" android:layout_height="wrap_content" android:background="@drawable/my_alertdialog_blackground">
<ImageView android:id="@+id/my_alert_dialog_icon" android:layout_width="64dp" android:layout_height="64dp" android:layout_centerHorizontal="true" android:layout_marginTop="32dp" android:scaleType="fitCenter" />
<TextView android:id="@+id/my_alert_dialog_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/my_alert_dialog_icon" android:layout_gravity="center" android:lineSpacingMultiplier="1.2" android:layout_marginTop="8dp" android:gravity="center" android:layout_marginStart="32dp" android:layout_marginEnd="32dp" android:textColor="#CC000000" android:textSize="20sp" />
<TextView android:id="@+id/my_alert_dialog_message" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/my_alert_dialog_title" android:lineSpacingMultiplier="1.2" android:layout_gravity="center" android:layout_marginStart="32dp" android:layout_marginTop="8dp" android:gravity="center" android:layout_marginEnd="32dp" android:textColor="#60000000" android:textSize="16sp" />
<View android:id="@+id/divider1" android:layout_marginTop="32dp" android:layout_width="match_parent" android:layout_height="1dp" android:layout_below="@id/my_alert_dialog_message" android:background="#20000000" />
<LinearLayout android:id="@+id/test_info_bottom_button" android:layout_width="match_parent" android:layout_height="48dp" android:layout_below="@id/divider1"
android:orientation="horizontal">
<Button android:id="@+id/my_alert_dialog_button_negative" android:layout_width="0dp" android:layout_height="48dp" android:layout_weight="1" android:textColor="#FF000000" android:textSize="16sp" android:paddingLeft="8dp" android:paddingRight="8dp" android:background="?android:attr/selectableItemBackground"/>
<View android:id="@+id/divider2" android:layout_width="1dp" android:layout_height="match_parent" android:background="#20000000" />
<Button android:id="@+id/my_alert_dialog_button_positive" android:layout_width="0dp" android:layout_height="48dp" android:layout_weight="1" android:background="?android:attr/selectableItemBackground" android:paddingLeft="8dp" android:paddingRight="8dp" android:textColor="#FF000000" android:textSize="16sp" /> </LinearLayout> </RelativeLayout>
|
上面的xml布局文件就确定了自定义组合View的样式和里面包含什么View控件,接着在MyDialogView类里的create()方法的Step 1,即[注释9]
,通过 View layout = inflater.inflate(R.layout.my_alert_dialog, null);
动态的加载布局文件,并在代码里逐一的操作各个控件,对应代码里Step2-Step6。
代码虽然比较长,但是逻辑都很简单,花几分钟相信大家就可以看懂,不过其中[注释8]
:
1 2 3 4 5 6 7
| @Override protected void onStart() { super.onStart(); getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); getWindow().setLayout(px2dip(getContext(), 2010), LinearLayout.LayoutParams.WRAP_CONTENT); }
|
这个MyDialogView继承了Dialog类,而Dialog类有自己的 Window,我们自定义的MyDialogView的四个角是圆形角,因此需要把Window设置为透明背景,否则会变成下图这样:
![image]()
继承ViewGroup类的自定义View
自定义ViewGroup
自定义View优化
为了使自定义View运行更加流畅,有以下几点需要注意:
- onDraw()尽量不分配内存:onDraw()被调用频率很高,如果在此进行内存分配可能会导致GC,从而导致卡顿;
- 使用含有参数的invalidate():不带参数的invalidate()会强制重绘整个View,所以如果可能的话,尽量调用含有4个参数的invalidate()方法;
- 减少requestLayout()调用:requestLayout()会使系统遍历整个View树来计算每个View的大小,是费时操作;
参考文档
- Android View体系(一)视图坐标系
- [自定义 View](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/自定义 View.md)
- View绘制流程全解析
- Android Paint API总结和使用方法