这篇文章主要参考
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);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected 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);
}
@Override
protected 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);
}
@Override
protected 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">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/me"/>
<EditText
android: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? = null
private var deleteButton: View? = null
private var itemLayout: ViewGroup? = null
private var selectedItem: Int = 0
private var isDeleteShown: Boolean = false
init {
setOnTouchListener(this)
}
fun setOnDeleteListener(l: OnDeleteListener) {
listener = l
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return if (isDeleteShown) {
itemLayout?.removeView(deleteButton)
isDeleteShown = false
deleteButton = null
false
} 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 = null
isDeleteShown = false
listener?.onDelete(selectedItem)
})
itemLayout = getChildAt(selectedItem - firstVisiblePosition) as ViewGroup
val 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)
}
}