这篇文章主要参考
Android LayoutInflater原理分析,带你一步步深入了解View(一)
Android视图绘制流程完全解析,带你一步步深入了解View(二)
Android视图状态及重绘流程分析,带你一步步深入了解View(三)
Android自定义View的实现方法,带你一步步深入了解View(四)

LayoutInflater

LayoutInflater 主要是用来加载布局的,我们经常使用它来加载自定义布局,在 RecyclerView 中也使用它。但是我们不知道的是 Activity 的 setContentView() 也是调用它来加载 Activity 的布局。

基本使用

  • Step 1:获取LayoutInflater
    LayoutInflater layoutInflater = LayoutInflater.from(context);
    或则
    LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  • Step 2:加载布局
    layoutInflater.inflate(resourceId, root, attachToRoot)
    第一个参数:加载的布局的 id
    第二个参数:布局的父布局,如果不需要传入 null
    第三个参数:是否需要连接到父布局,如果为 false 则只是设置最外层布局的属性,等到添加的时候这些属性会自动生效

实际代码

我们在一个线性布局中添加一个按钮,这次我们不用常规的方法而是采用 LayoutInflater 来完成。
MainActivity 的布局 layout_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/mainLayout"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity">
</LinearLayout>

创建一个 Button,同样放在 layout 文件夹下面,命名为 button_layout.xml :

<Button xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Button"></Button>

加载布局

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val view = LayoutInflater.from(this).inflate(R.layout.button_layout, mainLayout, false)}
}

这里要注意的是:要想让 Button 的属性生效必须为其添加一个父布局,当然也可以通过mainLayout.addView()的方式去添加(LinearLayout mainLayout = (LinearLayout) findViewById(R.id.main_layout)

工作原理

无论使用哪个 inflate() 的重载,最后都会调用 public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot),而这个方法通过 pull 解析方式解析布局 xml 文件,接着通过 createViewFromTag() 创建 View 对象。这里只是创建了根布局,接着会调用 rInflate() 去生成根布局下的子元素。(详细分析可以看最开始的连接)

View 的绘制

视图的绘制要经过三个流程:onMeasure()onLayout()onDraw()

onMeasure

onMeasure 方法是用来测量视图的大小的,View 的绘制流程会从 ViewRoot 的 performTraversals() 开始,在其内部调用 measure 方法,而 measure 方法会调用 onMeasure。onMeasure 有两个参数 int widthMeasureSpecint heightMeasureSpec。(这两个参数怎么获取的可以看参考链接,主要是用父视图计算子视图的大小,然后传递给子视图)

onMeasure 方法是 protected 的所以我们是可以对它进行修改的,它默认是通过 getDefaultSize(int size, int measureSpec) 来设置长和宽的

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}public static int getDefaultSize(int size, int measureSpec) {int result = size;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED:result = size;break;case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:result = specSize;break;}return result;
}

onLayout

onLayout 用于给视图进行布局,确定视图位置。ViewRoot 的 performTraversals() 方法会在 measure 结束后继续执行,并调用 View 的 layout() 方法来执行此过程。而 layout() 调用 onLayout() 进行布局。

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

onLayout() 是一个抽象方法,所以任何 ViewGroup 的子类都需要自己实现这个方法。

onDraw

onDraw 对视图进行绘制。
ViewRoot 中的代码会继续执行并创建出一个 Canvas 对象,然后调用 View 的 draw(Canvas canvas) 方法来执行具体的绘制工作。第一步对背景进行绘制,然后接着调用 onDraw() 对视图内容进行绘制,然后绘制子视图,最后进行滚动条的绘制。

实践

来源于链接中的博客
一个 SimpleLayout

public class SimpleLayout extends ViewGroup {public SimpleLayout(Context context, AttributeSet attrs) {super(context, attrs);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);if (getChildCount() > 0) {View childView = getChildAt(0);measureChild(childView, widthMeasureSpec, heightMeasureSpec);}}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {if (getChildCount() > 0) {View childView = getChildAt(0);childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());}}}

一个简单的视图

public class MyView extends View {private Paint mPaint;public MyView(Context context, AttributeSet attrs) {super(context, attrs);mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);}@Overrideprotected void onDraw(Canvas canvas) {mPaint.setColor(Color.YELLOW);canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);mPaint.setColor(Color.BLUE);mPaint.setTextSize(20);String text = "Hello View";canvas.drawText(text, 0, getHeight() / 2, mPaint);}
}

View 的重绘

视图会在 Activity 加载完成之后自动绘制到屏幕上,当我们动态更新视图的时候,会进行重绘。调用视图的 setVisibility()setEnabled()setSelected() 等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用 invalidate() 方法来实现。invalidate() 会判断视图是否需要重绘,如果需要的话就会最终调用 View 的 performTraversals() 这就是上面说的视图的绘制,只不过重绘不需要进行 onMeasure 和 onLayout (详细的源码解释可以看最开始的链接)

自己实现一个控件

宽和高相等的ImageView

public class WEqualsHImageView extends AppCompatImageView {public WEqualsHImageView(Context context) {super(context);}public WEqualsHImageView(Context context, AttributeSet attrs) {super(context, attrs);}public WEqualsHImageView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, widthMeasureSpec);}
}

自定义输入框

input_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="horizontal" android:layout_width="match_parent"android:layout_height="@dimen/inputViewHeight"android:layout_gravity="center_vertical"android:paddingLeft="@dimen/marginSize"android:paddingRight="@dimen/marginSize"><ImageViewandroid:id="@+id/iv_icon"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@mipmap/me"/><EditTextandroid:id="@+id/et_input"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@null"android:hint="username"android:paddingLeft="@dimen/marginSize"android:paddingRight="@dimen/marginSize"android:textSize="@dimen/titleSize"/>
</LinearLayout>

InputView.java

public class InputView extends FrameLayout {private int inputIcon;private String inputHint;private boolean isPassword;private ImageView ivIcon;private View view;private EditText etInput;public InputView(@NonNull Context context) {super(context);init(context, null);}public InputView(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(context, attrs);}public InputView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context, attrs);}@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)public InputView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);init(context, attrs);}/*** 初始化布局* @param context* @param attrs*/public void init(Context context, AttributeSet attrs) {if (attrs == null) return;TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.inputView);inputHint = typedArray.getString(R.styleable.inputView_input_hint);inputIcon = typedArray.getResourceId(R.styleable.inputView_input_icon, R.mipmap.user);isPassword = typedArray.getBoolean(R.styleable.inputView_is_password, false);typedArray.recycle();//绑定Layout布局view = LayoutInflater.from(context).inflate(R.layout.input_view, this, false);ivIcon = view.findViewById(R.id.iv_icon);etInput = view.findViewById(R.id.et_input);//布局关联属性ivIcon.setImageResource(inputIcon);etInput.setHint(inputHint);etInput.setInputType(isPassword ? InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD : InputType.TYPE_CLASS_PHONE);addView(view);}/*** 返回输入的内容* @return*/public String getInputStr() {return etInput.getText().toString().trim();}/*** 添加输入内容* @param str*/public void setInputStr(String str) {etInput.setText(str);}
}

这里讲一个前面没有说过的:如何给自定义控件添加自定义属性?
首先,我们现在 values 文件夹下面的 attrs.xml(如果没有自己创建一个)添加想要的属性,具体代码如下:
attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="inputView"><attr name="input_icon" format="reference"></attr><attr name="input_hint" format="string"></attr><attr name="is_password" format="boolean"></attr></declare-styleable>
</resources>

第二步,在代码中通过 context.obtainStyledAttributes(attrs, R.styleable.inputView) 获取自定义的属性
第三步,获取属性值,比如 inputHint = typedArray.getString(R.styleable.inputView_input_hint);
第四步,回收typedArray,typedArray.recycle();,TypedArray 是使用池+单例模式,每次获取一个 TypedArray 是从池中获取,所以用完一定要对其回收。

扩展 ListView

这个例子来源于链接,加入在ListView上滑动就可以显示出一个删除按钮,点击按钮就会删除相应数据的功能。
删除按钮的布局:

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/delete_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/delete_button" >
</Button>

MyListView.kt

class MyListView(context: Context, attrs: AttributeSet): ListView(context, attrs), View.OnTouchListener, GestureDetector.OnGestureListener {private val gestureDetector = GestureDetector(getContext(), this)private var listener: OnDeleteListener? = nullprivate var deleteButton: View? = nullprivate var itemLayout: ViewGroup? = nullprivate var selectedItem: Int = 0private var isDeleteShown: Boolean = falseinit {setOnTouchListener(this)}fun setOnDeleteListener(l: OnDeleteListener) {listener = l}override fun onTouch(v: View?, event: MotionEvent?): Boolean {return if (isDeleteShown) {itemLayout?.removeView(deleteButton)isDeleteShown = falsedeleteButton = nullfalse} else {gestureDetector.onTouchEvent(event)}}override fun onShowPress(e: MotionEvent?) {TODO("not implemented") //To change body of created functions use File | Settings | File Templates.}override fun onSingleTapUp(e: MotionEvent?): Boolean {TODO("not implemented") //To change body of created functions use File | Settings | File Templates.}override fun onDown(e: MotionEvent?): Boolean {if (!isDeleteShown && e != null) {selectedItem = pointToPosition(e.x.toInt(), e.y.toInt())}return false}override fun onFling(e1: MotionEvent?,e2: MotionEvent?,velocityX: Float,velocityY: Float): Boolean {if (!isDeleteShown && abs(velocityX) > abs(velocityY)) {deleteButton = LayoutInflater.from(context).inflate(R.layout.delete_button, null)deleteButton?.setOnClickListener(OnClickListener {itemLayout?.removeView(deleteButton)deleteButton = nullisDeleteShown = falselistener?.onDelete(selectedItem)})itemLayout = getChildAt(selectedItem - firstVisiblePosition) as ViewGroupval params = RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)params.addRule(RelativeLayout.CENTER_VERTICAL)itemLayout?.addView(deleteButton, params)isDeleteShown = true}return false}override fun onScroll(e1: MotionEvent?,e2: MotionEvent?,distanceX: Float,distanceY: Float): Boolean {TODO("not implemented") //To change body of created functions use File | Settings | File Templates.}override fun onLongPress(e: MotionEvent?) {TODO("not implemented") //To change body of created functions use File | Settings | File Templates.}interface OnDeleteListener {fun onDelete(index: Int)}
}

一篇搞懂Android View相关推荐

  1. 一篇文章彻底搞懂Android事件分发机制

    本文讲的是一篇文章彻底搞懂Android事件分发机制,在android开发中会经常遇到滑动冲突(比如ScrollView或是SliddingMenu与ListView的嵌套)的问题,需要我们深入的了解 ...

  2. 一篇搞懂微信小程序以及和其他对比

    一篇搞懂微信小程序以及和其他对比** 前两年的文章了,现在小程序肯定是有变化的,作为自己的随记 一.产品定位及功能分析** 微信小程序是一种全新的连接用户与服务的方式,他可以在微信内被便捷的获取和传播 ...

  3. python 类-Python入门--一篇搞懂什么是类

    原标题:Python入门--一篇搞懂什么是类 写一篇Python类的入门文章,在高级编程语言中,明白类的概念和懂得如何运用是必不可少的.文章有点长,3000多字. Python是面向对象的高级编程语言 ...

  4. 一篇搞懂OOA/OOD/OOP的区别

    文章目录 OOA OOD OOP 总结 相关文章: 一篇搞懂OOA/OOD/OOP的区别 面向对象的基本原则-抽象,封装,继承,分解 GRASP模式概述 面向对象的六大原则 OOA什么鬼,OOD又是什 ...

  5. C++ 一篇搞懂多态的实现原理

    C++ 一篇搞懂多态的实现原理 虚函数和多态 01 虚函数 在类的定义中,前面有 virtual 关键字的成员函数称为虚函数: virtual 关键字只用在类定义里的函数声明中,写函数体时不用. cl ...

  6. 一篇搞懂关于计算机的减法运算

    一篇搞懂关于计算机的减法运算 减法 相减结果为正的减法 相减结果为负数的减法 减法 相减结果为正的减法 如下一篇拙言,是自己平时的总结,如有错误欢迎各位大佬指正. 相信你一定听说过,补码,取反加一等等 ...

  7. 一文读懂Android View事件分发机制

    Android View 虽然不是四大组件,但其并不比四大组件的地位低.而View的核心知识点事件分发机制则是不少刚入门同学的拦路虎.ScrollView嵌套RecyclerView(或者ListVi ...

  8. 转:彻底搞懂Android文件存储---内部存储,外部存储以及各种存储路径解惑

    转自:https://blog.csdn.net/u010937230/article/details/73303034 前言: 对于任何一个应用来说,无论是PC端应用还是Android应用,存储肯定 ...

  9. 全方位带你彻底搞懂Android内存泄露

    1 Java内存回收方式 Java判断对象是否可以回收使用的而是可达性分析算法. 在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的.这个算法的基本思路就是通过一系列名 ...

最新文章

  1. 解读Python的命名空间
  2. npm install 报错 npm ERR! code Z_BUF_ERROR 问题解决
  3. @Test的作用以及Testcase和TestSuite的用法
  4. redhat rpmforge epel 安装源配置
  5. 论文浅尝 - AAAI2021 | 从历史中学习:利用时间感知拷贝生成网络建模时态知识图谱...
  6. 2月份Github上很热门的Python项目
  7. 10个给程序员的建议
  8. tcp三次握手和在局域网中使用 awl伪装MAC地址进行多线程SYN攻击
  9. 爬虫的一些知识点 目录 1. 网络爬虫 1 2. 产生背景 垂直领域搜索引擎 2 3. 1 聚焦爬虫工作原理以及关键技术概述 3 4. 涉及技术 3 4.1. 下载网页 一般是通过net api
  10. Oracle数据库学习笔记(十五)--自连接
  11. windows10华硕安装杜比音效
  12. Java将彩色PDF转为灰度
  13. 计算机桌面设置上时间表,桌面时钟怎么设置-电脑显示时间不对 怎么校准电脑右下角显示的时间?...
  14. 2017年江苏高考数学14题
  15. 为什么我的微信小程序开发工具调试窗口一片空白?
  16. Java高级程序员必备:高性能计数器及Striped64和LongAdder
  17. 29python腾讯位置大数据北京2019五一期间迁出数据
  18. R3.6.3下载 Rstudio下载及安装,网盘链接永久有效
  19. java学生通讯录_简单实现Java通讯录系统
  20. 网站项目通过钉钉机器人向钉钉群发送信息

热门文章

  1. 布林通道参数用20还是26_布林通道(BOLL)策略的投资效果如何?
  2. vue ----vue-cli
  3. procreate 笔刷_插画学习必备:2000款Procreate大师级笔刷,超级强大,免费领取
  4. NameError: name ‘time‘ is not defined
  5. RabbitMQ-Java实现Publish/Subscribe订阅模式
  6. 分布式文件系统FastDFS安装教程
  7. Node Sass does not yet support your current environment解决
  8. Android开发笔记(七)初识Drawable
  9. ios中amplify配置configure_Asp.netCore3.0 简单的webapi接口 (中)
  10. C语言去除字符串空格