一篇搞懂Android View
这篇文章主要参考
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 widthMeasureSpec
和 int 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相关推荐
- 一篇文章彻底搞懂Android事件分发机制
本文讲的是一篇文章彻底搞懂Android事件分发机制,在android开发中会经常遇到滑动冲突(比如ScrollView或是SliddingMenu与ListView的嵌套)的问题,需要我们深入的了解 ...
- 一篇搞懂微信小程序以及和其他对比
一篇搞懂微信小程序以及和其他对比** 前两年的文章了,现在小程序肯定是有变化的,作为自己的随记 一.产品定位及功能分析** 微信小程序是一种全新的连接用户与服务的方式,他可以在微信内被便捷的获取和传播 ...
- python 类-Python入门--一篇搞懂什么是类
原标题:Python入门--一篇搞懂什么是类 写一篇Python类的入门文章,在高级编程语言中,明白类的概念和懂得如何运用是必不可少的.文章有点长,3000多字. Python是面向对象的高级编程语言 ...
- 一篇搞懂OOA/OOD/OOP的区别
文章目录 OOA OOD OOP 总结 相关文章: 一篇搞懂OOA/OOD/OOP的区别 面向对象的基本原则-抽象,封装,继承,分解 GRASP模式概述 面向对象的六大原则 OOA什么鬼,OOD又是什 ...
- C++ 一篇搞懂多态的实现原理
C++ 一篇搞懂多态的实现原理 虚函数和多态 01 虚函数 在类的定义中,前面有 virtual 关键字的成员函数称为虚函数: virtual 关键字只用在类定义里的函数声明中,写函数体时不用. cl ...
- 一篇搞懂关于计算机的减法运算
一篇搞懂关于计算机的减法运算 减法 相减结果为正的减法 相减结果为负数的减法 减法 相减结果为正的减法 如下一篇拙言,是自己平时的总结,如有错误欢迎各位大佬指正. 相信你一定听说过,补码,取反加一等等 ...
- 一文读懂Android View事件分发机制
Android View 虽然不是四大组件,但其并不比四大组件的地位低.而View的核心知识点事件分发机制则是不少刚入门同学的拦路虎.ScrollView嵌套RecyclerView(或者ListVi ...
- 转:彻底搞懂Android文件存储---内部存储,外部存储以及各种存储路径解惑
转自:https://blog.csdn.net/u010937230/article/details/73303034 前言: 对于任何一个应用来说,无论是PC端应用还是Android应用,存储肯定 ...
- 全方位带你彻底搞懂Android内存泄露
1 Java内存回收方式 Java判断对象是否可以回收使用的而是可达性分析算法. 在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的.这个算法的基本思路就是通过一系列名 ...
最新文章
- 解读Python的命名空间
- npm install 报错 npm ERR! code Z_BUF_ERROR 问题解决
- @Test的作用以及Testcase和TestSuite的用法
- redhat rpmforge epel 安装源配置
- 论文浅尝 - AAAI2021 | 从历史中学习:利用时间感知拷贝生成网络建模时态知识图谱...
- 2月份Github上很热门的Python项目
- 10个给程序员的建议
- tcp三次握手和在局域网中使用 awl伪装MAC地址进行多线程SYN攻击
- 爬虫的一些知识点 目录 1. 网络爬虫	1 2. 产生背景 垂直领域搜索引擎	2 3. 1 聚焦爬虫工作原理以及关键技术概述	3 4. 涉及技术	3 4.1. 下载网页 一般是通过net api
- Oracle数据库学习笔记(十五)--自连接
- windows10华硕安装杜比音效
- Java将彩色PDF转为灰度
- 计算机桌面设置上时间表,桌面时钟怎么设置-电脑显示时间不对 怎么校准电脑右下角显示的时间?...
- 2017年江苏高考数学14题
- 为什么我的微信小程序开发工具调试窗口一片空白?
- Java高级程序员必备:高性能计数器及Striped64和LongAdder
- 29python腾讯位置大数据北京2019五一期间迁出数据
- R3.6.3下载 Rstudio下载及安装,网盘链接永久有效
- java学生通讯录_简单实现Java通讯录系统
- 网站项目通过钉钉机器人向钉钉群发送信息
热门文章
- 布林通道参数用20还是26_布林通道(BOLL)策略的投资效果如何?
- vue ----vue-cli
- procreate 笔刷_插画学习必备:2000款Procreate大师级笔刷,超级强大,免费领取
- NameError: name ‘time‘ is not defined
- RabbitMQ-Java实现Publish/Subscribe订阅模式
- 分布式文件系统FastDFS安装教程
- Node Sass does not yet support your current environment解决
- Android开发笔记(七)初识Drawable
- ios中amplify配置configure_Asp.netCore3.0 简单的webapi接口 (中)
- C语言去除字符串空格