前言
在Android开发时,我们经常会碰见在很多地方会重复使用相同的布局,或者是需要显示一些非基础组件,这个时候我们第一反应就是去自定义布局。将很多常用的UI业务需求,封装成一个View来操作,可以有效加快我们编码和开发效率。
自定义View的实现方式
实现方式常用的有两种:
1.通过继承Layout布局(相对来说简单易实现)
2.通过继承View(需要对onMeasure、onLayout和onDraw有一定的了解)
另外很重要的一点是自定义View和自定义属性的联合使用。
下面具体介绍两种方式的实现过程:
1. 继承Layout布局
- 首先你要写一个布局文件,里面包括了一个ImageView展示图片,一个TextView展示文字
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/top_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
android:layout_centerInParent="true"
android:layout_alignParentTop="true"/>
<TextView
android:id="@+id/bottom_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_below="@+id/top_image"
android:layout_marginTop="5dp"
android:layout_centerInParent="true" />
</RelativeLayout>
- 写一个继承于对应Layout的自定义类
public class ImageTextLayoutView extends RelativeLayout {
private ImageView topImage;
private TextView bottomText;
/**
* 这个构造方法是在代码中new的时候调用的
* @param context
*/
public ImageTextLayoutView(Context context) {
super(context);
}
/**
* 这个构造方法是在xml文件中初始化调用的
* @param context
* @param attrs View的xml属性
*/
public ImageTextLayoutView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context,attrs);
}
/**
* 这个方法不常用,有前两个足够了
* @param context
* @param attrs
* @param defStyleAttr 应用到View的主题风格(定义在主题中)
*/
public ImageTextLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void initView(Context context,AttributeSet attrs){
//获取子控件
LayoutInflater.from(context).inflate(R.layout.image_text_view_layout,this);
topImage = findViewById(R.id.top_image);
bottomText = findViewById(R.id.bottom_text);
//获取xml中定义的所有属性
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.ImageTextLayoutView);
//获取ImageView相关自定义属性并设置
int image_width = typedArray.getDimensionPixelSize(R.styleable.ImageTextLayoutView_image_width,30);
int image_height = typedArray.getDimensionPixelSize(R.styleable.ImageTextLayoutView_image_height,30);
Drawable drawable = typedArray.getDrawable(R.styleable.ImageTextLayoutView_image_src);
//topImage.setLayoutParams(new LayoutParams(image_width,image_height));
drawable.setBounds(0,0,image_width,image_height);
topImage.setImageDrawable(drawable);
//获取TextView相关的自定义属性并设置
String content = typedArray.getString(R.styleable.ImageTextLayoutView_text_content);
float text_size = typedArray.getDimension(R.styleable.ImageTextLayoutView_text_size,12);
int text_color = typedArray.getColor(R.styleable.ImageTextLayoutView_text_color, Color.BLACK);
bottomText.setText(content);
bottomText.setTextSize(text_size);
bottomText.setTextColor(text_color);
}
}
- 最后在xml文件引用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<com.ctsaing.flyandroid.ImageTextLayoutView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image_height="40dp"
app:image_src="@mipmap/ic_launcher"
app:image_width="40dp"
app:text_color="#aaff00"
app:text_size="8sp"
app:text_content="自定义View"
android:layout_marginTop="20dp"
android:layout_marginLeft="20dp"/>
2. 通过继承View的方式
- 创建自定义 View 类。首先,你需要创建一个 Java 类来作为自定义 View 的基类。可以按照以下代码来创建一个基础的自定义 View 类:
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
- 重写 onMeasure() 方法。onMeasure() 方法用于测量 View 的大小,你可以在这个方法中设置 View 的宽度和高度等属性。以下是一个示例代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取 MeasureSpec 的模式和尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 根据模式设置自定义 View 的宽高
int width, height;
if (widthMode == MeasureSpec.EXACTLY) {
// 如果宽度的测量模式是 EXACTLY,直接使用测量得到的尺寸
width = widthSize;
} else {
// 否则根据自定义 View 的内容来计算宽度
// 这里可以根据实际需要来设置宽度
width = calculateWidth();
}
if (heightMode == MeasureSpec.EXACTLY) {
// 如果高度的测量模式是 EXACTLY,直接使用测量得到的尺寸
height = heightSize;
} else {
// 否则根据自定义 View 的内容来计算高度
// 这里可以根据实际需要来设置高度
height = calculateHeight();
}
// 使用 setMeasuredDimension() 方法设置自定义 View 的宽高
setMeasuredDimension(width, height);
}
- 重写 onLayout() 方法。onLayout() 方法用于确定子 View 在自定义 View 中的摆放位置。以下是一个示例代码:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 遍历子 View,设置子 View 的位置
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 这里可以根据实际需要来设置子 View 的位置
child.layout(left, top, right, bottom);
}
}
- 重写 onDraw() 方法。onDraw() 方法用于绘制自定义 View 的内容。你可以在这个方法中使用 Canvas 对象来进行绘制操作。以下是一个示例代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 这里可以使用 Canvas 对象来进行绘制操作
// 可以绘制文字、图形、图片等
// 例如,绘制一个红色的矩形
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
}
以上就是实现 Android 自定义 View 三个方法的基本步骤和代码示例。你可以根据实际需求来进行具体的实现和调整。
3. 自定义属性
自定义布局和自定义属性是很难分开的,自定义属性的好处是我们可以再xml文件中直接使用。下面是实现过程:
- 继承View
针对上面的ImageTextLayoutView 布局,我们也可以直接通过自定义View来实现。不需要布局,直接继承View,然后重写onMeasure()和onDraw()方法。
package com.ctsaing.flyandroid.customViews;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.text.Layout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
import com.ctsaing.flyandroid.R;
/**
* 通过继承View实现上面图片下边文字的自定义View
* created by cyh on 2019/12/25
*/
public class ImageTextCustomView extends View {
private Drawable drawable;//画上方图片
private String content;// 画下方文字
//默认的View宽高
private int viewWidth;
private int viewHeight;
//默认的属性值
private int defalutImageWidth = 30;
private int defalutImageHeight = 30;
private int defalutTextColor = Color.BLACK;
private float defalutTextSize = 8;
//xml中获取到的属性值
private int xmlImageWidth;
private int xmlImageHeight;
private int xmlTextColor;
private float xmlTextSize;
//计算text的长度
private int textWidth;
private int textHeight;
//支持image和text的距离
private int imageAndTextMargin;
//画笔
private Paint imagePaint = new Paint();
private TextPaint textPaint = new TextPaint();
public ImageTextCustomView(Context context) {
super(context);
}
public ImageTextCustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context, attrs, 0);
}
public ImageTextCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void initView(Context context, AttributeSet attrs, int defStyleAttr) {
//先获取xml中的属性值
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ImageTextCustomView);
xmlImageWidth = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageWidth, defalutImageWidth);
xmlImageHeight = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageHeight, defalutImageHeight);
drawable = typedArray.getDrawable(R.styleable.ImageTextCustomView_imageSrc);
xmlTextColor = typedArray.getColor(R.styleable.ImageTextCustomView_textColor, defalutTextColor);
xmlTextSize = typedArray.getDimension(R.styleable.ImageTextCustomView_textSize, defalutTextSize);
content = typedArray.getString(R.styleable.ImageTextCustomView_textContent);
imageAndTextMargin = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageAndTextMargin, 0);
textHeight = (int) xmlTextSize;
textPaint.setColor(xmlTextColor);
textPaint.setTextSize(xmlTextSize);
textWidth = (int) Layout.getDesiredWidth(content,textPaint);
typedArray.recycle();
}
/**
* 重新测量View的宽高,并支持wrap_content设置
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//获取到View的整个高度
viewWidth = xmlImageWidth > textWidth ? xmlImageWidth : textWidth;
viewHeight = xmlImageHeight + imageAndTextMargin + textHeight;
viewWidth = Math.max(viewWidth, getSuggestedMinimumWidth());
viewHeight = Math.max(viewHeight, getSuggestedMinimumHeight());
/**
* 设置View的宽高
* tips:当xml文件中设置的是wrap_content时,Mode==AT_MOST
* 当设置位match_parent或具体的宽高时,Mode == EXACTLY
*/
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(viewWidth, viewHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(viewWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, viewHeight);
} else {
setMeasuredDimension(widthSize, heightSize);
}
}
/**
* 在画布中间画出图片和文字,图片在上,文字在下
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//xml文件中image设置的宽高时,而实际要显示的图片宽高可能要大于设置值;因此要要对bitmap按比例缩放
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
//获得bitmap的宽高
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
//计算缩放比列
float scaleWidth = ((float)xmlImageWidth)/bitmapWidth;
float scaleHeight = ((float)xmlImageHeight)/bitmapHeight;
//设置缩放的Matrix参数
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth,scaleHeight);
Bitmap newBitmap = Bitmap.createBitmap(bitmap,0,0,bitmapWidth,bitmapHeight,matrix,true);
//开始画bitmap的位置
float imageLeft = (viewWidth - xmlImageWidth) / 2;
canvas.drawBitmap(newBitmap, imageLeft, 0, imagePaint);
//画text的位置
float y = xmlImageHeight + imageAndTextMargin;
canvas.drawText(content,0,y,textPaint);
bitmap.recycle();
newBitmap.recycle();
canvas.restore();
}
}
- 在value文件夹中新建attrs.xml文件
- 在attrs文件中声明我们想要的自定义属性
<declare-styleable name="ImageTextCustomView">
<attr name="imageWidth" format="dimension"/>
<attr name="imageHeight" format="dimension"/>
<attr name="imageSrc" format="reference"/>
<attr name="textSize" format="float|dimension"/>
<attr name="textColor" format="color|reference"/>
<attr name="textWidth" format="dimension"/>
<attr name="textHeight" format="dimension"/>
<attr name="textContent" format="string"/>
<attr name="imageAndTextMargin" format="dimension"/>
</declare-styleable>
- 在我们的自定义文件中,通过typedArray取到我们在xml关于自定义View的属性值。
private void initView(Context context, AttributeSet attrs, int defStyleAttr) {
//先获取xml中的属性值
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ImageTextCustomView);
xmlImageWidth = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageWidth, defalutImageWidth);
xmlImageHeight = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageHeight, defalutImageHeight);
drawable = typedArray.getDrawable(R.styleable.ImageTextCustomView_imageSrc);
xmlTextColor = typedArray.getColor(R.styleable.ImageTextCustomView_textColor, defalutTextColor);
xmlTextSize = typedArray.getDimension(R.styleable.ImageTextCustomView_textSize, defalutTextSize);
content = typedArray.getString(R.styleable.ImageTextCustomView_textContent);
imageAndTextMargin = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageAndTextMargin, 0);
textHeight = (int) xmlTextSize;
textPaint.setColor(xmlTextColor);
textPaint.setTextSize(xmlTextSize);
textWidth = (int) Layout.getDesiredWidth(content,textPaint);
typedArray.recycle();
}
- 使用自定义属性的方式:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="vertical">
<com.ctsaing.flyandroid.customViews.ImageTextCustomView
android:layout_marginTop="50dp"
android:layout_marginLeft="50dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageSrc="@mipmap/ic_launcher"
app:imageWidth="40dp"
app:imageHeight="40dp"
app:imageAndTextMargin="10dp"
app:textSize="10sp"
app:textColor="@android:color/holo_red_dark"
app:textContent="另一个自定义view"/>
</LinearLayout>
上面app: 后面的都是我们在attrs文件中声明的属性值,可以直接这样使用。
结语
本文讲述了关于自定义View的简单实现,主要是展示方面的,自定义view的内容还很多,之后再学习再交流。