系列专栏:

  • 安卓高频面经解析大全专栏链接:150道安卓高频面试题全解析
  • 安卓高频面经解析大全目录详情 : 安卓面经_anroid面经_150道安卓常见基础面试题全解析

  • 安卓系统Framework面经专栏:Android系统Framework面试题解析大全
  • 安卓系统Framework面经目录详情:Android系统面经_Framework开发面经_150道面试题答案解析

  • Android进阶知识体系解析专栏链接:Android进阶知识体系解析
  • Android进阶知识体系解析目录详情:Android进阶知识体系解析_20大安卓进阶必备知识点

  • 嵌嵌入式面经解析大全专栏链接 :嵌入式面经C++软件开发面经111道面试全解析
  • 嵌入式面经解析大全目录详情 : 嵌入式面经111道面试题全解析C/C++可参考

本人是2020年毕业于广东工业大学研究生:许乔丹,有国内大厂CVTE和世界500强企业安卓开发经验,该专栏整理本人对常见安卓高频开发面试题的理解;

网上安卓资料千千万,笔者将继续维护专栏,一杯奶茶价格不止提供答案解析,承诺提供专栏内容免费技术答疑,直接咨询即可。助您提高安卓面试准备效率,为您面试保驾护航!

正文开始⬇

自定义View在日常的开发中,用到的频率非常高,面试中主要会考察平时自定义View的实战,我们看看面试官可能会问什么吧:

  1. 自定义View的流程 ⭐⭐⭐⭐⭐
  2. 自定义View需要重写哪些函数?说说你在自定义View时常常重写的一些方法? ⭐⭐⭐⭐
  3. 自定义View的种类有哪些?给我说说你之前项目中的案例。⭐⭐⭐⭐
  4. 说说自定义View中如何自定义属性?⭐⭐⭐
  5. 自定义View如何处理padding?⭐⭐
  6. 自定义View效率高于xml布局文件吗?⭐⭐
  7. 自定义View什么时候需要处理wrap_content属性?怎么处理?⭐

看完以下的解析,一定可以让面试官眼前一亮。

目录

  • 1、什么是自定义View

    • 1.1 自定义View和自定义ViewGroup
    • 1.2 自定义View基础知识
      • 1.2.1 坐标系
      • 1.2.2 颜色
      • 1.2.3 触摸事件
      • 1.2.4 margin和padding
    • 1.3 自定义View效率高于xml布局文件吗?
  • 2、自定义View的流程
    • 2.1 onMeasure()
    • 2.2 onLayout()
    • 2.3 onDraw()
      • 2.3.1 Canvas(画布)
      • 2.3.2 Paint(画笔)
      • 2.3.3 Path(路径)
  • 3、常见的自定义View类型
  • 4、继承系统提供的现有控件的自定义View
  • 5、继承View类的自定义View
    • 5.1 注意事项
    • 5.2 处理padding
    • 5.3 wrap_content属性处理
    • 5.4 添加自定义属性
  • 6、将多个单一的View合成复杂的自定义组合View
  • 7、继承ViewGroup类的自定义View(引导)
  • 8、自定义View优化

1、什么是自定义View

阅读本文之前,需要了解View的绘制有测量 -〉布局 -〉绘制,这三大步骤,具体可见本系列另一篇文章:View绘制

1.1 自定义View和自定义ViewGroup

  • 自定义View:如果官方提供现成的View控件无法达到符合自己预期的View的样式,那就需要自己来实现,一般需要重写onDraw()方法来设置绘制的样式,这就是自定义View;
  • 自定义ViewGroup:如果希望将一个或多个现有的View按照特定的布局方式,组装形成一个新的组件,这就是自定义ViewGroup。

1.2 自定义View基础知识

1.2.1 坐标系

在安卓系统中,屏幕左上角为原点,往右边是X轴正向,往下边是Y轴正向。常见API函数如下:

  • 子View到父View的距离
getHeight()  //获取View自身高度
getWidth()  //获取View自身宽度
getTop()    //获取子 View 左上角到父 View 顶部的距离
getLeft()   //获取子 View 左上角到父 View 左边的距离
getBottom()     //获取子 View 右下角到父 View 顶部的距离
getRight()  //获取子 View 右上角到父 View 左边的距离getBottom() - getTop() = 子View 的高
getRight() - getLeft() = 子View 的宽
  • 触摸点到所在View或者屏幕坐标系
event.getX() //触摸点相对于其所在 View 坐标系的坐标
event.getY()event.getRawX() //触摸点相对于屏幕坐标系的坐标
event.getRawY()

详细可参考下图(抄录于参考文档1),其中绿色方块为子View,子View里面的蓝色小圆圈代表触摸点,子View外边依次是父View和屏幕坐标。

1.2.2 颜色

Android系统支持的颜色模式有以下三种:

颜色模式 备注
ARGB8888 四通道高精度(32位)
ARGB4444 四通道低精度(16位)
RGB565 屏幕默认模式(16位)

其中A代表透明度,RGB分别代表红绿蓝三种原色,后面的数值代表该模式用多少位二进制数来表示,比如:

#0xF00   //低精度 - 不带透明通道红色
#0xAF00 //低精度 - 带透明通道红色
#0xFF0000       //高精度 - 不带透明通道红色
#0xAAFF0000 //高精度 - 带透明通道红色

1.2.3 触摸事件

既然是View,那就离不开触摸事件,常见的触摸事件如下:

事件 简介
ACTION_DOWN 手指初次接触屏幕时触发
ACTION_MOVE 手指在屏幕上滑动时触发,会多次触发
ACTION_UP 手指离开屏幕时触发
ACTION_CANCEL 事件被上层拦截时触发

View的触摸事件派发流程,可见本系列另一文章:触摸事件分发流程

1.2.4 margin和padding

在开发中,经常可以看到这两个,在此再次介绍下:

  • margin:子控件与父控件的距离,也就是“外边距”;
  • padding:控件里内容和控件的边界之间的距离,也就是“内边距”。在使用系统自带的控件时,只要在xml布局文件设置好padding即刻生效,但在自定义View则不会生效,需要手动在onDraw()方法里处理。

1.3 自定义View效率高于xml布局文件吗?

自定义View效率高于xml定义,原因如下:

  1. 自定义View少了解析xml;
  2. 自定义View 减少了ViewGroup与View之间的测量,包括父量子,子量自身,子在父中位置摆放,当子view变化时,父的某些属性都会跟着变化。

2、自定义View的流程

自定义View有一个通用的流程,如下图(抄录于参考文档2):

2.1 onMeasure()

在Measure阶段,需根据需要重写onMeasure()方法,即使在xml布局文件里面设置了View的宽高。因为一个子View的宽高不止受自身参数决定,还需要受到父控件的影响。具体见本系列文章:View绘制流程全解析(参考文档3)的2.3小节,在此不复述。

2.2 onLayout()

确定布局可以用onLayout()方法,在自定义View中,一般不需要重写该方法。但在自定义ViewGroup中可能需要重写,一般做法是循环取出子View,并计算每个子View位置等坐标值,然后使用child.layout()方法设置子View的位置,如下所示:

@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int childCount = getChildCount();int left = 0;View child;//循环遍历各个子Viewfor (int i = 0; i < childCount; i++) {child = getChildAt(i);if (child.getVisibility() != View.GONE) {int width = child.getMeasuredWidth();childWidth = width; //设置子View位置child.layout(left, 0, left + width, child.getMeasuredHeight());left += width;}}}

2.3 onDraw()

2.3.1 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 位移、缩放、旋转、错切

2.3.2 Paint(画笔)

Paint(画笔)在自定义View的实现也是非常常见的,所以需要了解常见的API函数,详情可见:Android开发手册-Paint

以下是Paint常用API函数,抄录于参考文档4:Android Paint API总结和使用方法:

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);
//设置该项为true,将有助于文本在LCD屏幕上的显示效果
void setSubpixelText( boolean subpixelText);
//设置下划线
void setUnderlineText( boolean underlineText);
//设置带有删除线的效果
void setStrikeThruText( boolean strikeThruText);
//设置伪粗体文本,设置在小字体上效果会非常差
void setFakeBoldText( boolean fakeBoldText);
//如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作
//加快显示速度,本设置项依赖于dither和xfermode的设置
void setFilterBitmap( boolean filter);
//设置画笔风格,空心或者实心 FILL,FILL_OR_STROKE,或STROKE
//Paint.Style.STROKE 表示当前只绘制图形的轮廓,而Paint.Style.FILL表示填充图形。
void setStyle(Style style);//设置颜色值
void setColor( int color);
//设置透明图0~255,要在setColor后面设置才生效
void setAlpha( int a);
//设置RGB及透明度
void setARGB( int a,  int r,  int g,  int b);
//当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度
void setStrokeWidth( float width);
void setStrokeMiter( float miter);
//当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷末端的图形样式
//如圆形样式Cap.ROUND,或方形样式Cap.SQUARE
void setStrokeCap(Cap cap);
//设置绘制时各图形的结合方式,如平滑效果等
void setStrokeJoin(Join join);
//设置图像效果,使用Shader可以绘制出各种渐变效果
Shader setShader(Shader shader);
//设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果
ColorFilter setColorFilter(ColorFilter filter);
//设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果
Xfermode setXfermode(Xfermode xfermode);
//设置绘制路径的效果,如点画线等
PathEffect setPathEffect(PathEffect effect);
//设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等
MaskFilter setMaskFilter(MaskFilter maskfilter);
//设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
Typeface setTypeface(Typeface typeface);
//设置光栅化
Rasterizer setRasterizer(Rasterizer rasterizer);
//在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色
//注意:在Android4.0以上默认开启硬件加速,有些图形的阴影无法显示。关闭View的硬件加速 view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
void setShadowLayer( float radius,  float dx,  float dy,  int color);
//设置文本对齐
void setTextAlign(Align align);
//设置字体大小
void setTextSize( float textSize);
//设置文本缩放倍数,1.0f为原始
void setTextScaleX( float scaleX);
//设置斜体文字,skewX为倾斜弧度
void setTextSkewX( float skewX);

2.3.3 Path(路径)

Path类封装了由直线段,二次曲线和三次曲线组成的复合(多轮廓)几何路径。 它可以用canvas.drawPath(path,paint)绘制,可以是填充或描边(基于绘制的样式),也可以用于剪裁或在路径上绘制文本。
详情见:Android开发手册-Path

3、常见的自定义View类型

如1.1小节说的,自定义View主要可以分为自定义View和自定义ViewGroup两种,根据本人实际开发经验,现将常见的自定义View分为以下4种类型进行讲解:

  • 继承系统提供的现有控件的自定义View;
  • 继承View类的自定义View;
  • 将多个单一的View合成复杂的自定义组合View;
  • 继承ViewGroup类的自定义View(引导);

接下来根据每种类型依次介绍。

4、继承系统提供的现有控件的自定义View

继承系统控件,一般是为了在系统控件上增加新的特性,可以在onDraw()方法里进行处理即可。系统控件TextView可以正常设置背景颜色和文本内容,但如果需要增加背景线条等特殊操作,正常的TextView的API函数是无法做到的。这时候就可以通过自定义View来实现了:

public class MyTextView extends TextView { //继承TextViewprivate 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); //设置画笔宽度,也就是字体的宽度}@Overrideprotected 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文件里直接引用该控件即可:

<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"/>

5、继承View类的自定义View

5.1 注意事项

上面继承系统控件相对简单,如果是继承View类的自定义View,就不仅要重写onDraw()方法,还要考虑以下几点:

  • padding属性处理;
  • wrap_content属性处理;
  • 提供自定义属性,方便自定义View的属性配置;
  • 如果涉及触摸操作,还需要重写onTouchEvent()方法来处理触摸事件;

既然是继承View类来创造一个新的View控件,我们画一个贝塞尔曲线,就是仿手机边缘滑动时的曲线图:

直接上代码(为方便展示,以下是最终版本的代码): ```java 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);//提取BezierView属性集合的bezier_color属性,如果没设置默认值为Color.BLACKmColor=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) { //1super.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);//AT_MOST对应的是wrap_content的宽高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);// 半弧的宽度是可以根据手指位置而变化的,这里简化写死为200float currentWidth = 200;float height = getHeight();int maxWidth = getWidth();float centerY = height / 2;float progress = currentWidth / maxWidth;if (progress == 0) {return;}//开始画半弧图形/*ps: 形状如下,小点为起始点和结束点,星号为控制点·|**|·|**|·*///设置画笔颜色,现在设置为黑色paint.setColor(mColor);//半弧颜色的深度是可以根据手指位置而变化的,这里简化写死paint.setAlpha((int) (500 * progress));float bezierWidth = currentWidth / 2;float coordinateX = 0;  //2//正式绘制贝塞尔曲线,使用cubicTo()方法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即可(这几行代码下文多次讲到,记得回来这里看):
```java
<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 -->

5.2 处理padding

上面[注释4]指定的padding(内边距),会发现无论修改到任何数值,绘制出来的View都不受影响,这是因为我们需要在onDraw()方法里做特殊处理才能显示出效果。至于系统控件设置padding数值可以生效,正是系统帮我们处理好了。

因此,在BezierView的onDraw()中的[注释2]需要做如下修改:

    //原来的代码:float coordinateX = 0;//修改为:int paddingLeft = getPaddingLeft();float coordinateX = paddingLeft;

其中coordinateX是贝塞尔曲线横坐标的开始点,我们需要手动获取xml布局文件设置的偏差值,手动的修改coordinateX,如此就可以使padding值生效。效果图如下,可以发现图像确实往中间偏移了一些:

5.3 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()

  • android:layout_width=wrap_content并且重写onMeasure()

5.4 添加自定义属性

我们看5.1小节最后的代码,以android:开头的都是系统自带的属性,而[注释5]:app:bezier_color,是自定义的属性。只要在values目录下创建attrs.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="BezierView"><attr name="bezier_color" format="color" /></declare-styleable>
</resources>

在这个文件我们设置了一个名为BezierView的自定义属性组合,里面可以有多个属性,目前只需要一个颜色格式的属性:bezier_color。可以根据需要添加多个属性。创建好后,我们看看如何使用,有两个地方需要修改:

  • Step1:xml布局文件修改

自定义属性需要添加:

xmlns:app="http://schemas.android.com/apk/res-auto" <!-- 6 -->
app:bezier_color="@android:color/black" />   <!-- 7 -->

使用自定义属性,都要添加[注释6]。其中app是自定义的名字,可以改为其他的。最后在[注释7]配置为黑色。

  • Step2:代码修改
    在上面BezierView的代码里有如下构造函数:
public BezierView(Context context, AttributeSet attrs) {super(context, attrs);//获取自定义属性组合TypedArray mTypedArray=context.obtainStyledAttributes(attrs,R.styleable.BezierView);//提取BezierView属性集合的bezier_color属性,如果没设置默认值为Color.BLACKmColor=mTypedArray.getColor(R.styleable.bezier_color,Color.BLACK);//获取资源后要及时回收mTypedArray.recycle();initDraw();}

6、将多个单一的View合成复杂的自定义组合View

Android系统自带的AlertDialog比较丑,因此想要自定义一个MyDialogView,如下图:

可以简单的看出,这个自定义组合View包含至少2个TextView、2个Button、1个ImageView。还是先上最终完整代码,再进行解析:

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);}@Overrideprotected void onStart() {super.onStart();//8:设置窗口背景为透明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);// 9:Step 1: 初始化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));// Step 2:设置图标,如果不设置则隐藏图标ImageView控件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);}// Step 3:设置标题,如果不设置则隐藏标题TextView控件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);}// Step 4:设置确认按钮,如果不设置则隐藏确认控件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);}// Step 5:设置取消按钮,如果不设置则隐藏取消控件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);}// Step 6:设置消息内容,如果不设置则隐藏消息TextView控件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里面可以这么使用:

@Overrideprotected 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布局文件

<?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"><ImageViewandroid: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" /><TextViewandroid: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" /><TextViewandroid: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" /><Viewandroid: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" /><LinearLayoutandroid:id="@+id/test_info_bottom_button"android:layout_width="match_parent"android:layout_height="48dp"android:layout_below="@id/divider1"android:orientation="horizontal"><Buttonandroid: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"/><Viewandroid:id="@+id/divider2"android:layout_width="1dp"android:layout_height="match_parent"android:background="#20000000" /><Buttonandroid: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]

 @Overrideprotected void onStart() {super.onStart();//8:设置窗口背景为透明getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));getWindow().setLayout(px2dip(getContext(), 2010), LinearLayout.LayoutParams.WRAP_CONTENT);}

这个MyDialogView继承了Dialog类,而Dialog类有自己的 Window,我们自定义的MyDialogView的四个角是圆形角,因此需要把Window设置为透明背景,否则会变成下图这样:

7、继承ViewGroup类的自定义View(引导)

受篇幅限制,继承ViewGroup暂不继续展开了,这里做个引导。想了解的同学可以看看刘望舒男神的文章:自定义ViewGroup

8、自定义View优化

为了使自定义View运行更加流畅,有以下几点需要注意:

  • onDraw()尽量不分配内存:onDraw()被调用频率很高,如果在此进行内存分配可能会导致GC,从而导致卡顿;
  • 使用含有参数的invalidate():不带参数的invalidate()会强制重绘整个View,所以如果可能的话,尽量调用含有4个参数的invalidate()方法;
  • 减少requestLayout()调用:requestLayout()会使系统遍历整个View树来计算每个View的大小,是费时操作;

参考文档

  1. Android View体系(一)视图坐标系
  2. 自定义 View
  3. View绘制流程全解析
  4. Android Paint API总结和使用方法

超全的Android面经_安卓面经(20/30)之自定义View全解析相关推荐

  1. 安卓自定义view全解:初始化,onDraw函数,onMeasure函数,用户手势事件

    全栈工程师开发手册 (作者:栾鹏) 安卓教程全解 安卓自定义view全解. view类包含如下函数.可供重写. onFinishInflate() 回调方法,当应用从XML加载该组件并用它构建界面之后 ...

  2. Android 常见界面控件(ListView、RecyclerView、自定义View篇)

    Android 常见界面控件(ListView.RecyclerView.自定义View篇) 目录 3.3 ListView的使用 3.3.1 ListView控件的简单使用 3.3.2 常用数据适配 ...

  3. Android自定义View全解

    目录 目录.png 1. 自定义View基础 1.1 分类 自定义View的实现方式有以下几种 类型 定义 自定义组合控件 多个控件组合成为一个新的控件,方便多处复用 继承系统View控件 继承自Te ...

  4. Android 自定义view完全解析--带你通透了解自定义view

    参考转自郭霖博客带你一步步深入了解View系列 Android LayoutInflater原理分析 相信接触Android久一点的朋友对于LayoutInflater一定不会陌生,都会知道它主要是用 ...

  5. [安卓开发]弹幕滚幕效果自定义View之BarrageView|支持点击事件|隐藏不滞留|颜色随机|大小速度范围随机

    安卓弹幕滚幕效果自定义View之BarrageView|支持点击事件|隐藏不滞留|颜色随机|大小速度范围随机 1.简介 项目地址: https://github.com/tpnet/BarrageVi ...

  6. 安卓仿苹果音量调节_android自定义view仿照MIUI中音量控制效果

    先看效果图: 这就是miui中的音量效果图,实现思路是自定义视图,绘制圆环,然后设置进度显示. 核心代码在onDraw中实现如下: @Override protected void onDraw(Ca ...

  7. android 自定义饼图半径不定,【Android】仿支付宝账单统计饼状图的自定义view

    仿支付宝统计饼状图的自定义view,如下图: 项目地址:https://github.com/bigeyechou/CustomViewCollection 目录:customviewcollecti ...

  8. Android模仿淘宝语音输入条形动画,录音动画自定义View

    Android模仿淘宝语音输入条形动画自定义View,类似柱状音频,折线音频,音乐跳动,音频跳动,录音动画,语音输入效果 地址: https://github.com/xfans/VoiceWaveV ...

  9. 安卓设置原生alert设置圆角_安卓手机设置充电提示音全新最全教程

    安卓版充电提示音教程_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili​m.bilibili.com 哎,自从前端时间IOS14发布后,他的其他所有功能没有活,唯一火了充电提示音.奈何大傻没有苹果手 ...

最新文章

  1. 系统分析与设计 实验一用例模型
  2. HDU - 6486 Flower(思维)
  3. 【图片识别】java 图片文字识别 ocr (转)
  4. Tuple VS ValueTuple
  5. 论文浅尝 | 面向视觉常识推理的层次语义增强方向图网络
  6. RQNOJ36 数石子 并查集 简单应用
  7. 举例说明操作系统在计算机系统中的重要地位,第一二三章作业参考答案
  8. 避免在ASP.NET Core中使用服务定位器模式
  9. 正确使用计算机说课稿,计算机优秀说课稿讲课讲稿.pdf
  10. java-net-php-python-jsp安利达物流公司管理系统计算机毕业设计程序
  11. “碳壁垒”悄然而起,碳足迹如何算清楚、减明白?|双碳科普
  12. App Store榜单优化:App出海必须掌握的ASO技巧
  13. 阿里云数据迁移工具解决方案:华为云迁移到阿里云
  14. ValueError: You are trying to load a weight file containing 0 layers into a model with 16 layers.
  15. 【弱监督显著目标检测论文】Weakly-Supervised Salient Object Detection via Scribble Annotations
  16. 腾讯云-视频直播(android集成)
  17. 力扣146题 LRU 缓存机制
  18. 数据库工程师考点2023
  19. 什么是5g卡,5g有啥好的
  20. 黑马12月开班时间出炉!戳文章免费试学!

热门文章

  1. Python提取PDF文件中的表格文本保存为Excel文件
  2. python爬取携程网游记_网页爬虫 - 用python selenium抓取携程信息
  3. LaTeX的版面设计
  4. 确定kafka对应的zookeeper版本
  5. c语言中除法怎么取模,c语言如何取模运算
  6. CameraBag Photo for Mac v2021.4.0 – 视频照片滤镜
  7. Java小程序——宠物商店
  8. 【教程】基于 Python 的地理空间绘图指南
  9. Web前端-Vue ElementUI el-input组件绑定点击事件
  10. 有人用YOLOv5和CLIP做了一个找图神器!搜图、裁剪一步到位!在线可试玩...