一篇搞懂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 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);
	}
 
	@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)
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值