自定义view属于android应用开发中很高频使用的技能,其中涉及到的知识点也很多。其中包括Activity的启动流程、view的刷新机制、view的绘制流程、事件的分发、属性动画等,本节的分享重点是具体的自定义view实现,其他的知识点如果大家有兴趣的话后面可以增加课题进行深入分析;
首先,自定义view共分为三种形式,其中包括:修改现有view类型、复合控件、完全自定义的控件;
自定义view使用场景对比
自定义view类型 | 使用场景 |
修改现有view类型 | 内置控件大部分满足需求,只需做一些扩展即可满足UI需求 |
复合控件 | 1、一组控件的封装,用于项目中的多处使用;2、优化视图层级,减少测量工作量,对布局进行优化; |
完全自定义的控件 | 内置组件都无法满足UI需求,无论您以何种方式组合使用这些组件都无法实现UI设计,只能去创造一个新的控件; |
一、修改现有view类型:
比如说需求要显示小米的logo,但是觉得他太方了,我们要显示一个圆形的logo,这种需求可以用一些第三方库进行实现,比如Glide、fresco等,其实我们自己也可以做,只需要在ImageView的基础上进行裁剪就可以了,而且可以自定义去裁剪想要的形状;
默认的ImageView效果
现在修改ImageVIew,首先继承ImageView,
public class CircleImageView extends ImageView {
然后重写onDraw方法,这里补充一个知识点,就是坐标系的概念:画图中所参照的坐标系,是以当前View位置的左上角为原点(0,0),水平方向向右为X轴正方向,竖直向下为Y轴正方向,这里的View位置是个相对值,它取决于开发者把它放在哪里。坐标系大致如下所示,注意和平时我们数学上坐标系略有差别。
//获取drawable
Drawable drawable = getDrawable();
if (drawable != null) {
//计算半径和圆心
float x = getWidth() >> 1;
float y = getHeight() >> 1;
float radius = Math.min(x - getPaddingLeft(), y - getPaddingTop());
//添加圆形的path
circlePath.addCircle(x, y, radius, Path.Direction.CW);
canvas.save();
//进行裁剪
canvas.clipPath(circlePath);
//画布上进行绘制
super.onDraw(canvas);
canvas.restore();
} else {
super.onDraw(canvas);
}
在我们的布局文件中,使用我们自定义的view,注意此处需要完整的包名类名来指定:
<com.example.testapp.CircleImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints"
android:layout_centerInParent="true"
android:src="@drawable/mi"
/>
修改后的CircleImageView展示小米logo效果图如下:
这是一个简单的例子,但重点在于,创建何种复杂程度的自定义组件完全取决于您的需要。
更复杂的组件可以替换更多的 on...
方法,并引入一些自己的辅助方法,从而充分地自定义其属性和行为。
下面汇总了框架针对视图调用的一些其他标准方法:
类别 | 方法 | 说明 |
创建 | 构造函数 | 包含在从代码创建视图时调用的构造函数形式和在从布局文件扩充视图时调用的构造函数形式。第二种形式的构造函数应解析并应用布局文件中定义的任何属性。 |
在视图及其所有子级都已从 XML 扩充之后调用。 | ||
布局 | 调用以确定此视图及其所有子级的大小要求。 | |
在此视图应为其所有子级分配大小和位置时调用。 | ||
在此视图的大小发生变化时调用。 | ||
绘图 | 在视图应渲染其内容时调用。 | |
事件处理 | 在发生新的按键事件时调用。 | |
在发生 key up 事件时调用。 | ||
在发生轨迹球动作事件时调用。 | ||
在发生触屏动作事件时调用。 | ||
焦点 | 在视图获得或失去焦点时调用。 | |
在包含视图的窗口获得或失去焦点时调用。 | ||
附加 | 在视图附加到窗口时调用。 | |
在视图与其窗口分离时调用。 | ||
在包含视图的窗口的可见性发生变化时调用。 |
以上的on...方法根据具体的需求来进行修改,每个点中都有一些扩展的知识点,如果大家有兴趣,后面可以单独进行说明;
二、复合控件
比较典型的例子就是TitleBar的封装,每个页面都需要进行展示,但是内置的控件无法满足我们定制TitleBar的需求,这时候我们就可以用复合控件来进行封装一层,然后在每个页面布局中进行使用。
首先定义复合控件的布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_title_bar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/holo_orange_light">
<ImageView
android:id="@+id/iv_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:paddingLeft="10dp"
android:paddingRight="20dp"
android:src="@drawable/ic_back" />
<com.example.testapp.view.CircleImageView
android:id="@+id/civ_head"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@android:color/primary_text_dark"
android:textSize="20sp" />
<ViewStub
android:id="@+id/vs_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="4dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="4dp"
android:layout_toLeftOf="@id/civ_head"
android:layout_toRightOf="@id/iv_back"
android:layout="@layout/search_bar" />
</RelativeLayout>
然后定义自定义属性,在atts文件内进行添加:
<resources>
<declare-styleable name="combination">
<!--声明一个属性 name format-->
<attr name="showHead" format="boolean" />
<attr name="showTitle" format="boolean" />
<attr name="showBack" format="boolean" />
<attr name="bgColor" format="color" />
</declare-styleable>
</resources>
format支持的类型一共有11种:
(1). reference:参考某一资源ID
<ImageView android:background = "@drawable/图片ID"/>
(2). color:颜色值
<TextView android:textColor = "#00FF00" />
(3). boolean:布尔值
<Button android:focusable = "true"/>
(4). dimension:尺寸值**
<Button android:layout_width = "48dp"/>
(5). float:浮点值
<alpha android:fromAlpha = "1.0"/>
(6). integer:整型值**
`<animated-rotate android:framesCount = "12"/
(7). string:字符串
<TextView android:text = "我是小米人"/>
(8). fraction:百分数**
<rotate android:pivotX = "200%"/>
(9). enum:枚举值
属性定义:
<declare-styleable name="名称">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
属性使用:
<LinearLayout android:orientation = "vertical"></LinearLayout>
注意:枚举类型的属性在使用的过程中只能同时使用其中一个,不能 android:orientation = “horizontal|vertical"
(10). flag:位或运算
属性定义:
<declare-styleable name="名称">
<attr name="gravity">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="center_vertical" value="0x10" />
</attr>
</declare-styleable>
属性使用:
<TextView android:gravity="bottom|left"/>
注意:位运算类型的属性在使用的过程中可以使用多个
(11). 混合类型:属性定义时可以指定多种类型值属性定义:
<declare-styleable name = "名称">
<attr name = "background" format = "reference|color" />
</declare-styleable
属性使用:<ImageViewandroid:background = "@drawable/图片ID" />
或者:<ImageViewandroid:background = "#00FF00" />
应用了这些属性应该去我们的应用程序包中找,所以在布局文件中要引入我们应用包的命名空间xmlns:TitleBar="http://schemas.android.com/apk/res-auto”
,res-auto表示自动查找,还有一种写法xmlns:TitleBar="http://schemas.android.com/apk/com.example.testapp",com.example.testapp
为我们的应用程序包名。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:combination="http://schemas.android.com/apk/res-auto"
android:id="@+id/rl_title_bar"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.testapp.TitleBar
android:id="@+id/title_bar_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
combination:showHead="true"
combination:showTitle="true"
/>
<com.example.testapp.TitleBar
android:id="@+id/title_bar_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:layout_below="@id/title_bar_0"
android:layout_alignParentTop="true"
combination:showHead="false"
combination:showTitle="false"
combination:bgColor = "@android:color/holo_green_light"
/>
<com.example.testapp.TitleBar
android:id="@+id/title_bar_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="200dp"
android:layout_below="@id/title_bar_1"
android:layout_alignParentTop="true"
combination:showHead="true"
combination:showTitle="false"
combination:showBack="false"
/>
</RelativeLayout>
然后在自定义类中对属性值进行获取并加以使用
public TitleBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initView(context);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.combination);
isShowHead = ta.getBoolean(R.styleable.combination_showHead, false);
isShowTitle = ta.getBoolean(R.styleable.combination_showTitle, true);
isShowBack = ta.getBoolean(R.styleable.combination_showBack, true);
color = ta.getColor(R.styleable.combination_bgColor,getResources().getColor(android.R.color.holo_orange_light));
ta.recycle(); //注意回收
}
private void initView(Context context) {
View.inflate(context, R.layout.title_bar, this);
rlTitleBar = this.findViewById(R.id.rl_title_bar);
tvTitle = this.findViewById(R.id.tv_title);
civHead = this.findViewById(R.id.civ_head);
rlSearch = this.findViewById(R.id.rl_search);
ivBack = this.findViewById(R.id.iv_back);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
rlTitleBar.setBackgroundColor(color);
civHead.setVisibility(isShowHead ? View.VISIBLE : View.GONE);
ivBack.setVisibility(isShowBack ? VISIBLE : GONE);
if (isShowTitle) {
tvTitle.setVisibility(VISIBLE);
} else {
tvTitle.setVisibility(GONE);
ViewStub stub = (ViewStub) this.findViewById(R.id.vs_search);
stub.setVisibility(VISIBLE);
}
ivBack.setOnClickListener(this);
civHead.setOnClickListener(this);
}
说明一点:这里是继承的FrameLayout,但是继承LinearLayout,RelativeLayout等系统布局控件都可以。之所以要继承这些系统现成的ViewGroup,是因为这样可以不用再重写onMeasure,onLayout等,这样省事很多。由于这里是一个布局控件,要用LayoutInflater来填充,所以需要继承ViewGroup,如果继承View的直接子类,编译会不通过。所以,TitleBar自己就是一个容器,完全可以当成容器使用,此时CustomTitleView自身的内容会和其作为父布局添加的子控件,效果会叠加,具体的叠加效果是根据继承的容器特性决定的。
从代码中我们可以看到,对部分控件添加了点击事件,通过点击返回键退出当前activity,点击头像进入个人中心等等,这些可以做一些想做的一些操作
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.iv_back:
((Activity)mContext).finish();
break;
case R.id.civ_head:
Intent intent = new Intent(mContext, CustomViewAct.class);
mContext.startActivity(intent);
break;
default:
break;
}
}
我们对外提供了三个接口供外部去调用
/***
* 设置title文字
* @param title
*/
public void setTitle(String title) {
tvTitle.setText(title);
}
/***
* 设置头像
* @param drawable
*/
public void setHead(Drawable drawable) {
civHead.setImageDrawable(drawable);
}
/***
* 设置背景颜色
* @param color
*/
public void setBgColor(int color) {
rlTitleBar.setBackgroundColor(color);
}
我们在Activity当中便可以进行调用
mTitleBar0 = this.findViewById(R.id.title_bar_0);
mTitleBar0.setTitle("自定义view");
mTitleBar0.setHead(getDrawable(R.drawable.mi));
mTitleBar1 = this.findViewById(R.id.title_bar_1);
mTitleBar1.setBgColor(android.R.color.holo_green_light);
mTitleBar2 = this.findViewById(R.id.title_bar_2);
mTitleBar2.setHead(getDrawable(R.drawable.cmb_qrlogo));
这样,我们复合控件的自定义就实现了,
通过这种方式有很多优点:
(1). 您可以像使用 Activity 屏幕一样使用声明式 XML 文件指定布局,也可以通过编程方式从代码中创建视图并将其嵌套在布局中。
(2). onDraw()
和 onMeasure()
方法(加上大多数其他 on...
方法)可能具有适当的行为,因此您无需替换它们。
(3). 最后,您可以非常快速地构建任意复杂化的复合视图,并像使用单个组件一样重复使用它们。
这里只是一个简单的例子,更多的酷炫UI大家有兴趣的话可以找几个练练手,熟悉下流程和步骤。
由于篇幅比较长,完全自定义view涉及到的知识点比较多,就由下一节单独来进行分享啦!!
本节中有什么错误的地方欢迎大家来拍砖,有什么疑问或者好的点子,也希望大家踊跃指出~