Android⾃定义View详解
View 的绘制系列⽂章:
对于 Android 开发者来说,原⽣控件往往⽆法满⾜要求,需要开发者⾃定义⼀些控件,因此,需要去了解⾃定义 view 的实现原理。这样即使碰到需要⾃定义控件的时候,也可以游刃有余。
基础知识
⾃定义 View 分类
⾃定义 View 的实现⽅式有以下⼏种:
类型定义
⾃定义组合控件多个控件组合成为⼀个新的控件,⽅便多处复⽤
继承系统 View 控件继承⾃TextView等系统控件,在系统控件的基础功能上进⾏扩展
继承 View不复⽤系统控件逻辑,继承View进⾏功能定义
继承系统 ViewGroup继承⾃LinearLayout等系统控件,在系统控件的基础功能上进⾏扩展
继承 View ViewGroup不复⽤系统控件逻辑,继承ViewGroup进⾏功能定义
从上到下越来越难,需要的了解的知识也是越来越多的。
构造函数
当我们在⾃定义 View 的时候,构造函数都是不可缺少,需要对构造函数进⾏重写,构造函数有多个,⾄少要重写其中⼀个才⾏。例如我们新建 MyTextView:
public class MyTextView extends View {
  /**
* 在java代码⾥new的时候会⽤到
* @param context
*/
public MyTextView(Context context) {
super(context);
}
/**
* 在xml布局⽂件中使⽤时⾃动调⽤
* @param context
*/
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 不会⾃动调⽤,如果有默认style时,在第⼆个构造函数中调⽤
* @param context
* @param attrs
* @param defStyleAttr
*/
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 只有在API版本>21时才会⽤到
* 不会⾃动调⽤,如果有默认style时,在第⼆个构造函数中调⽤
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
对于每⼀种构造函数的作⽤,都已经再代码⾥⾯写出来了。
⾃定义属性
写过布局的同学都知道,系统控件的属性在 xml 中都是以 android 开头的。对于⾃定义 View,也可以⾃定义属性,在 xml 中使⽤。Android ⾃定义属性可分为以下⼏步:
1. ⾃定义⼀个 View
2. 编写 l,在其中编写 styleable 和 item 等标签元素
3. 在布局⽂件中 View 使⽤⾃定义的属性(注意 namespace)
4. 在 View 的构造⽅法中通过 TypedArray 获取
e.g  还是以上⾯的 MyTextView 做演⽰:
⾸先我在 l 中引⼊了 MyTextView:
<?xml version="1.0" encoding="utf-8"?>
<straint.ConstraintLayout xmlns:android="schemas.android/apk/res/android"
xmlns:app="schemas.android/apk/res-auto"
xmlns:tools="schemas.android/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
&application.MyTextView
android:layout_width="100dp"
android:layout_height="200dp"
app:testAttr="520"
app:text="helloWorld"/>
</straint.ConstraintLayout>
然后我在 l 中添加⾃定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="test">
<attr name="text" format="string"/>
<attr name="testAttr" format="integer"/>
</declare-styleable>
</resources>
记得在构造函数⾥⾯说过,xml 布局会调⽤第⼆个构造函数,因此在这个构造函数⾥⾯获取属性和解析:
/**
* 在xml布局⽂件中使⽤时⾃动调⽤
* @param context
*/
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, st);
int textAttr = ta.getInteger(st_testAttr, -1);
String text = ta.getString(st_text);
Log.d(TAG, " text = " + text + ", textAttr = " + textAttr);
     // toast 显⽰获取的属性值
Toast.makeText(context, text + " " + textAttr, Toast.LENGTH_LONG).show();
}
注意当你在引⽤⾃定义属性的时候,记得加上 name 前缀,否则会引⽤不到。
这⾥本想截图 log 的,奈何就是不显⽰,就搞成 toast 了。
当然,你还可以⾃定义很多其他属性,包括 color, string, integer, boolean, flag,甚⾄是混合等。
⾃定义组合控件
⾃定义组合控件就是将多个控件组合成为⼀个新的控件,主要解决多次重复使⽤同⼀类型的布局。如我们顶部的 HeaderView 以及 dailog 等,我们都可以把他们组合成⼀个新的控件。
我们通过⼀个⾃定义 MyView1 实例来了解⾃定义组合控件的⽤法。
xml 布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="schemas.android/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/feed_item_com_cont_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="2"
android:text="title"/>
<TextView
android:id="@+id/feed_item_com_cont_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/feed_item_com_cont_title"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="2"
android:text="desc"/>
</merge>
⾃定义 View 代码:
application;
t.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class MyView1 extends RelativeLayout {
/** 标题 */
private TextView mTitle;
/** 描述 */
private TextView mDesc;
public MyView1(Context context) {
this(context, null);
}
public MyView1(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
android layout布局/**
* 初使化界⾯视图
*
* @param context 上下⽂环境
*/
protected void initView(Context context) {
View rootView = LayoutInflater.from(getContext()).inflate(_view1, this);
mDesc = rootView.findViewById(R.id.feed_item_com_cont_desc);
mTitle = rootView.findViewById(R.id.feed_item_com_cont_title);
}
}
在布局当中引⽤该控件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="schemas.android/apk/res/android"
xmlns:app="schemas.android/apk/res-auto"
xmlns:tools="schemas.android/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/text"
android:layout_width="100dp"
android:layout_height="100dp"
android:clickable="true"
android:enabled="false"
android:focusable="true"
android:text="trsfnjsfksjfnjsdfjksdhfjksdjkfhdsfsdddddddddddddddddddddddddd"/>
&application.MyTextView
android:id="@+id/myview"
android:layout_width="100dp"
android:layout_height="200dp"
android:clickable="true"
android:enabled="false"
android:focusable="true"
app:testAttr="520"
app:text="helloWorld"/>
&application.MyView1
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
最终效果如下图所⽰:
继承系统控件
继承系统的控件可以分为继承 View⼦类(如 TextView 等)和继承 ViewGroup ⼦类(如 LinearLayout 等),根据业务需求的不同,实现的⽅式也会有⽐较⼤的差异。这⾥介绍⼀个⽐较简单的,继承⾃View的实现⽅式。
业务需求:为⽂字设置背景,并在布局中间添加⼀条横线。
因为这种实现⽅式会复⽤系统的逻辑,⼤多数情况下我们希望复⽤系统的onMeaseur 和onLayout 流程,所以我们只需要重写onDraw ⽅法。实现⾮常简单,话不多说,直接上代码。
application;
t.Context;
aphics.Canvas;
aphics.LinearGradient;
aphics.Shader;
TextPaint;
import android.util.AttributeSet;
import android.widget.TextView;
import static android.Color;
/**
* 包含分割线的textView
* ⽂字左右两边有⼀条渐变的分割线
* 样式如下:
* ———————— ⽂字 ————————
*/
public class DividingLineTextView extends TextView {
/** 线性渐变 */
private LinearGradient mLinearGradient;
/** textPaint */
private TextPaint mPaint;
/** ⽂字 */
private String mText = "";
/** 屏幕宽度 */
private int mScreenWidth;
/** 开始颜⾊ */
private int mStartColor;
/** 结束颜⾊ */
private int mEndColor;
/** 字体⼤⼩ */
private int mTextSize;
/**
* 构造函数
*/
public DividingLineTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle);
mTextSize = getResources().getDimensionPixelSize(_size);
mScreenWidth = getCalculateWidth(getContext());
mStartColor = getColor(getContext(), lorAccent);
mEndColor = getColor(getContext(), lorPrimary);
mLinearGradient = new LinearGradient(0, 0, mScreenWidth, 0,
new int[]{mStartColor, mEndColor, mStartColor},
new float[]{0, 0.5f, 1f},
Shader.TileMode.CLAMP);
mPaint = new TextPaint();
}
public DividingLineTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DividingLineTextView(Context context) {
this(context, null);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setAntiAlias(true);
mPaint.setTextSize(mTextSize);
int len = getTextLength(mText, mPaint);
/
/ ⽂字绘制起始坐标
int sx = mScreenWidth / 2 - len / 2;
// ⽂字绘制结束坐标
int ex = mScreenWidth / 2 + len / 2;
int height = getMeasuredHeight();
mPaint.setShader(mLinearGradient);
// 绘制左边分界线,从左边开始:左边距15dp,右边距距离⽂字15dp
canvas.drawLine(mTextSize, height / 2, sx - mTextSize, height / 2, mPaint);        mPaint.setShader(mLinearGradient);
// 绘制右边分界线,从⽂字右边开始:左边距距离⽂字15dp,右边距15dp
canvas.drawLine(ex + mTextSize, height / 2,
mScreenWidth - mTextSize, height / 2, mPaint);
}
/**
* 返回指定⽂字的宽度,单位px
*
* @param str  要测量的⽂字
* @param paint 绘制此⽂字的画笔
* @return返回⽂字的宽度,单位px
*/
private int getTextLength(String str, TextPaint paint) {
return (int) asureText(str);
}
/
**
* 更新⽂字
*
* @param text ⽂字
*/
public void update(String text) {
mText = text;
setText(mText);
// 刷新重绘
requestLayout();
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。