您的位置:首页 > 产品设计 > UI/UE

4.2 自定义控件 之 自定义属性与引入布局

2017-09-22 11:31 281 查看


点此进入:从零快速构建APP系列目录导图

点此进入:UI编程系列目录导图

点此进入:四大组件系列目录导图

点此进入:数据网络和线程系列目录导图

本节例程下载地址:

WillFLowCustomAttribute

WillFlowInlcude





一、自定义属性

(1)为何要引入自定义属性?

当Android提供的原生属性不能满足实际的需求的时候,比如我们需要自定义圆形百分比半径大小、圆形背景、圆形显示的位置、圆形进度的背景等等。这个时候就需要我们自定义属性了。

(2)自定义属性的基本步骤

在res/values文件下添加一个attrs.xml文件

如果项目比较大的话,会导致attrs.xml代码相当庞大,这时可以根据相应的功能模块起名字,方便查找,例如:登录模块相关attrs_login.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundImageView">
<attr name="borderRadius" />
<attr name="type" />
</declare-styleable>
</resources>


声明一组属性

使用来定义一个属性集合,name就是属性集合的名字,这个名字一定要起的见名知意。

<declare-styleable name="PercentView">
<!--添加属性-->
</declare-styleable>


定义属性值

通过 方式定义属性值,属性名字同样也要起的见名知意,format表示这个属性的值的类型,类型有以下几种:

float:浮点型

integer:整型

fraction:百分数

enum:枚举类型

flag:位或运算

reference:引用资源

string:字符串

Color:颜色

boolean:布尔值

dimension:尺寸值

基于上面的要求,我们可以定义一下百分比控件属性:

<declare-styleable name="PercentView">
<attr name="percent_circle_gravity"><!--圆形绘制的位置-->
<flag name="left" value="0" />
<flag name="top" value="1" />
<flag name="center" value="2" />
<flag name="right" value="3" />
<flag name="bottom" value="4" />
</attr>
<attr name="percent_circle_radius" format="dimension" /><!--圆形半径-->
<attr name="percent_circle_progress" format="integer" /><!--当前进度值-->
<attr name="percent_progress_color" format="color" /><!--进度显示颜色-->
<attr name="percent_background_color" format="color" /><!--圆形背景色-->
<attr name="percent_offset" format="integer" /><!--圆形中心偏差-->
</declare-styleable>


在布局中使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:lee="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<com.whoislcj.views.PercentView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_margin="10dp"
android:background="@color/red"
android:padding="10dp"
lee:percent_background_color="@color/gray"
lee:percent_circle_gravity="left"
lee:percent_circle_progress="30"
lee:percent_circle_radius="50dp"
lee:percent_progress_color="@color/blue" />

</LinearLayout>


为属性集设置一个属性集名称,我这里用的lee,我这是因为实在想不起使用什么属性集名称了,建议在真正的项目中使用项目的缩写,比如微信可能就是使用wx。

在自定义控件中获取自定义属性

每一个属性集合编译之后都会对应一个styleable对象,通过styleable对象获取TypedArray typedArray,然后通过键值对获取属性值,这点有点类似SharedPreference的取法。

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PercentView);
if (typedArray != null) {
mBackgroundColor = typedArray.getColor(R.styleable.PercentView_percent_background_color, Color.YELLOW);
mProgressColor = typedArray.getColor(R.styleable.PercentView_percent_progress_color, Color.RED);
mRadius = typedArray.getDimension(R.styleable.PercentView_percent_circle_radius, 0);
mProgress = typedArray.getInt(R.styleable.PercentView_percent_circle_progress, 0);
mGravity = typedArray.getInt(R.styleable.PercentView_percent_circle_gravity, CENTER);
mOffset = typedArray.getInt(R.styleable.PercentView_percent_offset, 0);
typedArray.recycle();
}


(3)完整示例

public PercentView(Context context) {
super(context);
init();
}

public PercentView(Context context, AttributeSet attrs) {
super(context, attrs);
initParams(context, attrs);
}

public PercentView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initParams(context, attrs);
}

private void initParams(Context context, AttributeSet attrs) {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mRectF = new RectF();
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PercentView);
if (typedArray != null) {
mBackgroundColor = typedArray.getColor(R.styleable.PercentView_percent_background_color, Color.YELLOW);
mProgressColor = typedArray.getColor(R.styleable.PercentView_percent_progress_color, Color.RED);
mRadius = typedArray.getDimension(R.styleable.PercentView_percent_circle_radius, 0);
mProgress = typedArray.getInt(R.styleable.PercentView_percent_circle_progress, 0);
mGravity = typedArray.getInt(R.styleable.PercentView_percent_circle_gravity, CENTER);
mOffset = typedArray.getInt(R.styleable.PercentView_percent_offset, 0);
typedArray.recycle();
}
}

private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mRectF = new RectF();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

switch (widthMode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
break;
case MeasureSpec.UNSPECIFIED:
break;
}
Log.i(TAG, "onMeasure() widthMode  : " + widthMode);
Log.i(TAG, "onMeasure() widthSize  : " + widthSize);
Log.i(TAG, "onMeasure() heightMode : " + heightMode);
Log.i(TAG, "onMeasure() heightSize : " + heightSize);

int with = getWidth();
int height = getHeight();
mCenterX = with / 2;
mCenterY = with / 2;
switch (mGravity) {
case LEFT:
mCenterX = mRadius + getPaddingLeft();
break;
case TOP:
mCenterY = mRadius + getPaddingTop();
break;
case CENTER:
break;
case RIGHT:
mCenterX = with - mRadius - getPaddingRight();
break;
case BOTTOM:
mCenterY = height - mRadius - getPaddingBottom();
break;
}
float left = mCenterX - mRadius;
float top = mCenterY - mRadius;
float right = mCenterX + mRadius;
float bottom = mCenterY + mRadius;
mRectF.set(left, top, right, bottom);
Log.i(TAG, "left : " + left + ", top : " + top + ", right : " + right + ", bottom : " + bottom);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.i(TAG, "onLayout()  left : " + left + ", top : " + top + ", right : " + right + ", bottom : " + bottom);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

mPaint.setColor(mBackgroundColor);
// FILL:填充; STROKE:描边; FILL_AND_STROKE:填充和描边。
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawCircle(mCenterX, mCenterY, mRadius, mPaint);
Log.i(TAG, "onDraw() mCenterX : " + mCenterX + ", mCenterY : " + mCenterY + ", mRadius : " + mRadius);

mPaint.setColor(mProgressColor);
double percent = mProgress * 1.0 / 100;
int angle = (int) (percent * 360);
canvas.drawArc(mRectF, 270, angle, true, mPaint);  // 根据进度画圆弧
}


通过自定义属性可以达到自定义的控件也能像原生的控件一样实现可配置。但是在实际的项目开发中,像本文介绍的这种自定义控件使用频率并不是最高的,使用频率较高的是通过自定义一个组合控件的方式,来达到布局文件的复用,以减少项目维护成本以及开发成本,下篇文章将重点介绍如何自定义控件组合。

二、引入布局

为什么要引入布局?

我们在项目开发中经常会遇见很多相似或者相同的布局,比如用过 iPhone 的同学都应该知道,几乎每一个 iPhone 应用的界面顶部都会有一个标题栏,标题栏上会有一到两个按钮可用于返回或其他操作,因为 iPhone 没有实体返回键嘛。现在很多的Android 程序也都喜欢模仿 iPhone 的风格,在界面的顶部放置一个标题栏,虽然 Android 系统已经给每个活动提供了标题栏功能,但这里我们还是决定创建一个自定义的标题栏,这样就使得我们的界面布局更加灵活。

一个标题栏布局不是什么困难的事情,只需要加入两个 Button 和一个 TextView,然后在布局中摆放好就可以了。可是这样做存在的问题是:一般我们的程序中可能有很多个活动都需要这样的标题栏,如果在每个活动的布局中都编写一遍同样的标题栏代码,明显就会导致代码的大量重复,这个时候我们就可以使用引入布局的方式来解决这个问题。



(1)静态引入

新建一个布局 title.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">

<ImageButton
android:id="@+id/imageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:padding="5dp"
android:src="@mipmap/ic_launcher" />

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_alignBottom="@+id/imageButton"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/imageButton"
android:background="@color/colorPrimary"
android:gravity="center_vertical"
android:paddingLeft="50dp"
android:text="自定义标题"
android:textSize="24sp" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/textView"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:text="右键"
android:textSize="18sp" />

</RelativeLayout>


我们在 RelativeLayout 中分别加入了一个 Button 、一个 ImageButton 和一个 TextView,左边的 ImageButton 可用于返回,右边的 Button 可用于编辑,中间的 TextView 则可以显示一段标题文本。上面的代码中大多数的属性我们都已经是见过的,不明白的可以到本系列开始的几篇文章当中学习,里面详细列出了常用的所有属性。

现在标题栏布局已经编写完成了,剩下的就是如何在程序中使用这个标题栏了,在

activity_main.xml 的代码中添加

<include
layout="@layout/title"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</include>


是的,我们只需要通过一行 include 语句将标题栏布局引入进来就可以了,最后在 MainActivity 中将系统自带的标题栏隐藏掉,代码如下所示:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//        requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);

if (getSupportActionBar() != null){
getSupportActionBar().hide();
}
}


这里还要说明一点:如果你的代码的Activity是继承自AppCompatActivity,那么使用该示例代码就可以了,但是如果你的代码的Activity是继承自Activity,那么则需要使用requestWindowFeature(Window.FEATURE_NO_TITLE);来去掉原有的标题栏。但是无论如何,使用这种方式,不管有多少布局需要添加标题栏,只需一行 include 语句就可以了。

(2)动态引入

静态引入布局的技巧确实解决了重复编写布局代码的问题,但是如果布局中有一些控件要求能够响应事件,我们还是需要在每个Activity中为这些控件单独编写一次事件注册的代码。比如说标题栏中的返回按钮,其实不管是在哪一个Activity中,这个按钮的功能都是相同的,即销毁掉当前Activity。而如果在每一个Activity中都需要重新注册一遍返回按钮的点击事件,无疑又是增加了很多重复代码,这种情况最好是使用动态引入的方式来解决。

新建 TitleLayout 继承自 RelativeLayout,让它成为我们自定义的标题栏控件,代码如下所示:

public TitleLayout(Context context) {
super(context);
mContext = context;
init();
}

public TitleLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}

public TitleLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
init();
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TitleLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mContext = context;
}

private void init() {
LayoutInflater.from(mContext).inflate(R.layout.title, this);
mButton = (Button) findViewById(R.id.button);
mImageButton = (ImageButton) findViewById(R.id.imageButton);
}


首先我们重写了 RelativeLayout 中的几个构造函数,在布局中引入 TitleLayout 控件就会调用这个init()方法。然后在init()方法中需要对标题栏布局进行动态加载,这就要借助 LayoutInflater 来实现了。通过 LayoutInflater 的 from() 方法可以构建出一个 LayoutInflater 对象,然后调用 inflate() 方法就可以动态加载一个布局文件,inflate()方法接收两个参数,第一个参数是要加载的布局文件的 id,这里我们传入 R.layout.title,第二个参数是给加载好的布局再添加一个父布局, 这里我们想要指定为 TitleLayout,于是直接传入 this。

现在自定义控件已经创建好了,然后我们需要在布局文件中添加这个自定义控件,在 activity_main.xml 中添加如下代码:

<com.wgh.willflowinlcude.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.wgh.willflowinlcude.TitleLayout>


添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候我们需要指明控件的完整类名,包名在这里是不可以省略的。

重新运行程序,你会发现此时效果和使用引入布局方式的效果是一样的,然后我们来尝试为标题栏中的按钮注册点击事件,修改 TitleLayout 中的代码,如下所示:

private void init() {
LayoutInflater.from(mContext).inflate(R.layout.title, this);
mButton = (Button) findViewById(R.id.button);
mImageButton = (ImageButton) findViewById(R.id.imageButton);
mButton.setOnClickListener(this);
mImageButton.setOnClickListener(this);
}

@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button :
showToast("单击了右键", view);
break;
case R.id.imageButton :
showToast("单击了左键", view);
break;
}
}


首先还是通过 findViewById() 方法得到按钮的实例,然后分别调用 setOnClickListener() 方法给两个按钮注册了点击事件,当点击返回按钮时销毁掉当前的Activity,当点击按钮时用Toast弹出一段文本。这样的话,每当我们在一个布局中引入 TitleLayout,返回按钮和编辑按钮的点击事件就已经自动实现好了,这就省去了很多编写重复代码的工作。

这里你能看到我们的Toast和平时使用的有些区别,这是因为我们使用了自定义的Toast,当然如果你对之前的有所了解,你自然知道除此之外的四种Toast用法,如果没有的话,可以到这里查看:

2.6 通知类控件 Toast、Menu

点此进入:GitHub开源项目“爱阅”。“爱阅”专注于收集优质的文章、站点、教程,与大家共分享。下面是“爱阅”的效果图:





联系方式:

简书:WillFlow

CSDN:WillFlow

微信公众号:WillFlow

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息