前提

为什么要自定义View?

怎么自定义View?

当 Android SDK 中提供的系统 UI 控件无法满足业务需求时,我们就需要考虑自己实现 UI 控件。而且自定义View在面试时是很经典的一道Android面试题,对其的理解程度决定面试官看你的高度。

PS:关于我

本人是一个拥有6年开发经验的帅气Android工程师,记得看完点赞,养成习惯,微信搜一搜「 程序猿养成中心 」关注这个喜欢写干货的程序员。

另外耗时两年整理收集的Android一线大厂面试完整考点PDF出炉,资料【完整版】已更新在我的【Github】,有面试需要、或者想梳理自己的Android知识体系的朋友们可以去参考参考,如果对你有帮助,可以点个Star哦!

自定义View的方式

继承系统提供的成熟控件(比如 LinearLayout、RelativeLayout、ImageView 等);

直接继承自系统 View 或者 ViewGroup,并自绘显示内容。

建议:尽量直接使用系统提供的UI控件、或者方式1实现需求效果,因为google提供的UI控件等都已经做了非常完整的边界、特殊case处理。自定义的View可能由于考虑不周全在适配,某些特殊case下出现问题等。

方式1: 继承系统UI控件

举例:继承RelativeLayout实现一个titleBar

1 添加布局

注意:这里使用merge标签,因为后面我们extends RelativeLayout,不需要再套一层

xmlns:tool="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="wrap_content">

android:id="@+id/left_button"

android:layout_width="40dp"

android:layout_height="56dp"

android:layout_marginStart="6dp"

android:paddingStart="5dp"

android:paddingTop="13dp"

android:paddingEnd="5dp"

android:paddingBottom="13dp"/>

android:id="@+id/title_view"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_centerVertical="true"

android:layout_marginStart="20dp"

android:layout_marginEnd="20dp"

android:layout_toStartOf="@+id/right_button"

android:layout_toEndOf="@id/left_button"

android:ellipsize="end"

android:gravity="center"

android:maxLines="1"

android:textColor="@color/color_ffffff"

android:textSize="18sp"

tool:text="This is tools text"/>

android:id="@+id/right_button"

android:layout_width="40dp"

android:layout_height="40dp"

android:layout_alignParentEnd="true"

android:layout_marginTop="7dp"

android:layout_marginEnd="6dp"

android:padding="5dp"/>

2 添加自定义属性

在valules的attrs.xml中定义如下自定义属性

name : 是属性名称

format : 代表属性的格式

使用自定义属性的时候需要添加命名空间如:xmlns:app(可以自己定义)

3 TitleBar代码实现

public class TitleBarLayout extends RelativeLayout {

public TitleBarLayout(Context context) {

this(context, null);

}

public TitleBarLayout(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

public TitleBarLayout(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

// 引入步骤1中添加的布局

ViewUtils.inflate(this, R.layout.layout_title_bar, true);

// 获取自定义属性

TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.TitleBar);

Drawable leftDrawable = typedArray.getDrawable(R.styleable.TitleBar_title_left_icon);

Drawable rightDrawable = typedArray.getDrawable(R.styleable.TitleBar_title_right_icon);

CharSequence titleText = typedArray.getString(R.styleable.TitleBar_title_bar_title_text);

int titleTextColor = typedArray.getColor(R.styleable.TitleBar_title_bar_text_color, Color.BLACK);

// 如果不调用recycle,As会有提示

typedArray.recycle();

ImageView leftButton = findViewById(R.id.left_button);

ImageView rightButton = findViewById(R.id.right_button);

TextView titleTextView = findViewById(R.id.title_view);

if (leftDrawable != null) {

leftButton.setImageDrawable(leftDrawable);

}

if (rightDrawable != null) {

rightButton.setImageDrawable(rightDrawable);

}

titleTextView.setText(titleText);

titleTextView.setTextColor(titleTextColor);

}

}

方式2: 继承View / ViewGroup

自定义View的时候,一般是3步走:

1 重写onMeasure

测量子View及View本身的大小

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

为什么我们需要重写onMeasure?

如果我们直接在 XML 布局文件中定义好 View 的宽高,然后让自定义 View 在此宽高的区域内显示即可,那么就不需要重写onMeasure了。

但是Android 系统提供了 wrap_content 和 match_parent 属性来规范控件的显示规则。它们分别代表自适应大小和填充父视图的大小,但是这两个属性并没有指定具体的大小,因此我们需要在 onMeasure 方法中过滤出这两种情况,真正的测量出自定义 View 应该显示的宽高大小.

MesureSpec

MesureSpec 定义 :

测量规格,View根据该规格从而决定自己的大小。

MeasureSpec是由一个32位 int 值来表示的。其中该 int 值对应的二进制的高2位代表SpecMode,低30位代表SpecSize

测量模式

EXACTLY:表示在 XML 布局文件中宽高使用 match_parent 或者固定大小的宽高;

AT_MOST:表示在 XML 布局文件中宽高使用 wrap_content;

UNSPECIFIED:父容器没有对当前 View 有任何限制,当前 View 可以取任意尺寸,比如 ListView 中的 item。

自定义FlowLayout onMeasure方法说明(看注释 !看助手!看注释!)

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 宽度测量模式

int widthMode = MeasureSpec.getMode(widthMeasureSpec);

// 高度测量模式

int heightMode = MeasureSpec.getMode(heightMeasureSpec);

// 测量宽度

int width = MeasureSpec.getSize(widthMeasureSpec);

// 测量高度

int height = MeasureSpec.getSize(heightMeasureSpec);

// 每一行的宽度(FlowLayout当标签长度超过一行后会换行)

int lineWidth = 0;

// 最终测量的width

int resultWidth = 0;

// 最终测量的height

int resultHeight = 0;

// 换行次数

int lineCount = 1;

// 遍历所有子View,拿到每一行的最大宽度 和 换行次数

for (int i = 0; i < getChildCount(); i++) {

View childAt = getChildAt(i);

measureChild(childAt, widthMeasureSpec, heightMeasureSpec);

MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();

lineWidth += childAt.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;

// 换行

if (lineWidth > width) {

resultWidth = Math.max(lineWidth - childAt.getMeasuredWidth(), resultWidth);

lineWidth = 0;

lineCount++;

}

}

View lastChild = getChildAt(getChildCount() - 1);

MarginLayoutParams marginParams = (MarginLayoutParams) lastChild.getLayoutParams();

// 根据换行次数 + 2(第一行和最后一行) * 高度,算出最终的高度

resultHeight += (lastChild.getMeasuredHeight() + marginParams.topMargin + marginParams.bottomMargin) * (lineCount + 2);

resultWidth += getPaddingLeft() + getPaddingRight();

resultHeight += getPaddingTop() + getPaddingBottom();

// 重写onMeasure必须调用这个方法来保存测量的宽、高

setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? resultWidth : width,

heightMode == MeasureSpec.AT_MOST ? resultHeight : height);

}

2 重写onDraw

绘制自身内容

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

}

onDraw 方法接收一个 Canvas 类型的参数。Canvas 可以理解为一个画布,在这块画布上可以绘制各种类型的 UI 元素。

Canvas 中每一个绘制操作都需要传入一个 Paint 对象。Paint 就相当于一个画笔,我们可以通过设置画笔的各种属性。

如果不想看Canvas、Paint枯燥的文档,可以搜索Hencoder,看下hencoder大佬的自定义教程,有趣生动。

3、 重写onLayout

摆放子View位置(继承ViewGroup必须要重写)

自定义FlowLayout onLayout方法说明(看注释 !看注释!看注释!)

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

// 获取FlowLayout 宽度

int parentWidth = getMeasuredWidth();

// 子View摆放的位置

int left, top, right, bottom;

// 一行的宽度

int lineWidth = 0;

// 一行的高度

int lineHeight = 0;

// 遍历子View 计算它们应该摆放的位置

for (int i = 0; i < getChildCount(); i++) {

View childAt = getChildAt(i);

int childWidth = childAt.getMeasuredWidth();

int childHeight = childAt.getMeasuredHeight();

MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();

left = lineWidth + layoutParams.leftMargin;

right = left + childWidth + layoutParams.rightMargin;

top = lineHeight + layoutParams.topMargin;

bottom = top + childHeight + layoutParams.bottomMargin;

if (right > parentWidth) {

lineWidth = 0;

lineHeight += childHeight + layoutParams.topMargin + layoutParams.bottomMargin;

left = lineWidth + layoutParams.leftMargin;

right = left + childWidth + layoutParams.rightMargin;

top = lineHeight + layoutParams.topMargin;

bottom = top + childHeight + layoutParams.bottomMargin;

}

childAt.layout(left, top, right, bottom);

lineWidth += childWidth + layoutParams.rightMargin + layoutParams.leftMargin;

}

}

效果图如下

在这里插入图片描述

FlowLayout完成代码

public class FlowLayout extends ViewGroup {

public FlowLayout(Context context) {

super(context);

}

public FlowLayout(Context context, AttributeSet attrs) {

super(context, attrs);

}

public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int widthMode = MeasureSpec.getMode(widthMeasureSpec);

int heightMode = MeasureSpec.getMode(heightMeasureSpec);

int width = MeasureSpec.getSize(widthMeasureSpec);

int height = MeasureSpec.getSize(heightMeasureSpec);

int lineWidth = 0;

int resultWidth = 0;

int resultHeight = 0;

int lineCount = 1;

for (int i = 0; i < getChildCount(); i++) {

View childAt = getChildAt(i);

measureChild(childAt, widthMeasureSpec, heightMeasureSpec);

MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();

lineWidth += childAt.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;

if (lineWidth > width) {

resultWidth = Math.max(lineWidth - childAt.getMeasuredWidth(), resultWidth);

// 换行

lineWidth = 0;

lineCount++;

}

}

View lastChild = getChildAt(getChildCount() - 1);

MarginLayoutParams marginParams = (MarginLayoutParams) lastChild.getLayoutParams();

resultHeight += (lastChild.getMeasuredHeight() + marginParams.topMargin + marginParams.bottomMargin) * (lineCount + 2);

resultWidth += getPaddingLeft() + getPaddingRight();

resultHeight += getPaddingTop() + getPaddingBottom();

setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? resultWidth : width,

heightMode == MeasureSpec.AT_MOST ? resultHeight : height);

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

}

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

int parentWidth = getMeasuredWidth();

int left, top, right, bottom;

int lineWidth = 0;

int lineHeight = 0;

for (int i = 0; i < getChildCount(); i++) {

View childAt = getChildAt(i);

int childWidth = childAt.getMeasuredWidth();

int childHeight = childAt.getMeasuredHeight();

MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();

left = lineWidth + layoutParams.leftMargin;

right = left + childWidth + layoutParams.rightMargin;

top = lineHeight + layoutParams.topMargin;

bottom = top + childHeight + layoutParams.bottomMargin;

if (right > parentWidth) {

lineWidth = 0;

lineHeight += childHeight + layoutParams.topMargin + layoutParams.bottomMargin;

left = lineWidth + layoutParams.leftMargin;

right = left + childWidth + layoutParams.rightMargin;

top = lineHeight + layoutParams.topMargin;

bottom = top + childHeight + layoutParams.bottomMargin;

}

childAt.layout(left, top, right, bottom);

lineWidth += childWidth + layoutParams.rightMargin + layoutParams.leftMargin;

}

}

@Override

public LayoutParams generateLayoutParams(AttributeSet attrs) {

return new MarginLayoutParams(getContext(), attrs);

}

}

android面试自定义view,资深面试官:自定义View的实现方式,你知道几种?相关推荐

  1. 【DB笔试面试593】在Oracle中,表的访问方式有哪几种?

    ♣题目 部分 在Oracle中,表的访问方式有哪几种? ♣答案部分 访问表的方式也叫优化器访问路径,主要有3种访问路径:全表扫描(FULL TABLE SCAN,FTS).索引扫描(INDEX SCA ...

  2. 【程序员面试干货】资深面试官告诉你:测试工程师面试要注意什么?

    找工作,找更好的工作,永远是职场人士特别是IT/互联网这个人才流动性巨大行业的永恒话题.而提到找工作,又离不开对于面试的探讨. 网上虽然有诸多面试相关的文章攻略,不过站在面试官角度谈面试的却很少. 本 ...

  3. Android自定义View之(一)View绘制流程详解——向源码要答案

    前言 View作为整个app的颜值担当,在Android体系中占有重要的地位.深入理解Android View的绘制流程,对正确使用View来构建赏心悦目的外观,以及用自定义View来设计理想中的酷炫 ...

  4. android 自定义布局 根据布局获取类,阿里高级Android面试题解析:Android自定义View—布局过程的自定义...

    自定义分三部分绘制.布局和触摸反馈,本篇主要讲的布局过程的自定义 布局过程的含义 布局过程,就是程序在运行时利用布局文件的代码来计算出实际尺寸的过程. 布局过程的工作内容 两个阶段:测量阶段和布局阶段 ...

  5. 【Android自定义View实战】之自定义评价打分控件RatingBar,可以自定义星星大小和间距...

    [Android自定义View实战]之自定义评价打分控件RatingBar,可以自定义星星大小和间距

  6. 10年资深面试官直言:80%人面试Java都会止步于此!

    从业十多年,我从面试者变成面试官,在 Java 面试上积累了比较丰富的经验. 其实,很多面试者在搜集面试资料的时候都踩过一些"坑",你是不是也遇到过: 免费搜索的面试题,内容不全面 ...

  7. 资深面试官解答:大厂月薪过20K的测试工程师,都需要满足哪些要求?

    正值金三银四跳槽季,不少小伙伴都做起了准备,希望能够寻觅到更合适的工作,其中大厂,作为IT届的领头企业,更是许多测试员趋之若鹜的工作宝地. 那究竟大厂需要怎么样的软件测试工程师,怎样的测试员才算是优秀 ...

  8. android刷新时的圆形动画_Android自定义view渐变圆形动画

    本文实例为大家分享了Android自定义view渐变圆形动画的具体代码,供大家参考,具体内容如下 直接上效果图 自定义属性 attrs.xml文件 创建一个类 ProgressRing继承自 view ...

  9. android根据滑动字体颜色被填充,自定义View:02-滑动变色的字体

    效果图如下: 滑动文字.gif 一.自定义属性: 1.1.字体要变的颜色 1.2.字体不变的颜色 二.继承TextView 2.1.初始化画笔 两个字体画笔 :变色与不变色 2.2.onDraw() ...

最新文章

  1. 从AI应用的五大要素看,AI产业存在哪些机会?(算力算法)
  2. 【iCore3 双核心板】例程二十一:LAN_TCPS实验——以太网数据传输
  3. 解析第一高中教育首份成绩单:营利双增,轻资产模式成“杀手锏”
  4. 计算机专业论文范文精选,计算机毕业论文提纲范文精选
  5. pdo oracle返回参数游标,PDOStatement::closeCursor
  6. 气体管道管径及流量对照表_建筑用管道产品常见技术质量问答汇总
  7. 【Linux】VirtualBox安装ubuntu排错LowGraphic
  8. java 启动顺序_java语句执行顺序
  9. linux 查看pgsql端口,如何查看postgres数据库端口
  10. 海思Hi3559A Sample_comm_vdec模块解码 视频解码解析
  11. 图像的旋转,imrotate函数的应用
  12. Head First设计模式-适配器模式
  13. 【AP/AR】借项通知单和贷项通知单的区别
  14. mac下如何在多种输入法中达到最好的切换体验
  15. RSA结合DES解决上下行接口的安全性问题
  16. 努比亚手机安装linux,努比亚红魔5G电竞手机将发布;Linux版荣耀MagicBook降价促销...
  17. 英语 Yang liang 120 --180
  18. 阿里云栖大会的现场,到底是个什么样?
  19. 游戏人工智能编程案例精粹(修订版) (Mat Buckland 著)
  20. 判断手机机型及用户访问方式

热门文章

  1. 基于某网站的信息爬取与保存_指定跳转页
  2. java继承类型转换_#java 一个简单的例子理解java继承、成员函数重写、类型转换...
  3. idea怎么打包有依赖关系的项目_项目需求不明确,项目各模块逻辑关系不清晰,怎么排计划?...
  4. 深度学习: 从 RoIPooling 到 RoIAlign
  5. C++之变量的作用域,生存期,可见性
  6. Robot Framework - Variable file
  7. 研究人员发现利用Excel宏可发起跳板攻击
  8. 操作 Wave 文件(14): waveOutSetPlaybackRate、waveOutSetPitch
  9. bacula 的安装、配置和运行
  10. Windows 编程