自定义 View 分类
-
继承 View 并重写 onDraw 方法 适用于实现无法通过布局组合来完成的不规则效果。需要通过绘制来实现,即重写
onDraw方法。该方法需要自行支持wrap_content,并处理padding。 -
继承 ViewGroup 并派生特殊的 Layout 用于自定义布局,创建系统布局之外的新布局。当需要将多种 View 组合在一起实现某种效果时,可以采用此方法。需要处理 ViewGroup 的测量和布局过程,以及子元素的测量和布局。
-
继承特定的 View(如 TextView) 常用于扩展已有 View 的功能,例如 TextView。实现相对简单,不需要自行支持
wrap_content和padding。 -
继承特定的 ViewGroup(如 LinearLayout) 当某种效果需要将几种 View 组合在一起时,可以采用此方法。与方法 2 的区别在于,方法 2 更接近 View 的底层,方法 4 不需要处理 ViewGroup 的测量和布局过程。
自定义 View 须知
-
让 View 支持 wrap_content 继承 View 或者 ViewGroup 的控件需要在
onMeasure中对wrap_content做特殊处理,否则无法达到预期效果。 -
让 View 支持 padding 继承 View 的控件需要在
draw方法中处理padding。继承 ViewGroup 的控件需在onMeasure和onLayout中考虑padding和子元素的margin。 -
尽量不要在 View 中使用 Handler View 内部提供了
post系列的方法,可以替代 Handler 的作用,除非明确需要使用 Handler 发送消息。 -
及时停止线程或动画 在
onDetachedFromWindow中停止线程或动画,避免内存泄漏。onAttachedToWindow对应 Activity 启动时调用,需要处理 View 不可见时的停止操作。 -
处理好滑动冲突 处理 View 嵌套时的滑动冲突,避免影响 View 的效果。
自定义 View 示例 - 继承 View 并重写 onDraw 方法
1. 绘制圆的简单实现
实现一个简单的自定义控件,绘制一个圆:
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int cx = width / 2;
int cy = height / 2;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(cx, cy, radius, mPaint);
}
}
使用示例:
<com.example.viewserise.CircleView
android:id="@+id/circle_view1"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000"
android:layout_margin="20dp"/>
2. 让 padding 生效
调整 onDraw 方法处理 padding:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth() - getPaddingLeft() - getPaddingRight();
int height = getHeight() - getPaddingTop() - getPaddingBottom();
int cx = width / 2 + getPaddingLeft();
int cy = height / 2 + getPaddingTop();
int radius = Math.min(width, height) / 2;
canvas.drawCircle(cx, cy, radius, mPaint);
}
3. 让 wrap_content 生效
在 onMeasure 中处理 wrap_content:
private int mDefaultWidth = 200;
private int mDefaultHeight = 200;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mDefaultWidth, mDefaultHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mDefaultWidth, heightMeasureSpec);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthMeasureSpec, mDefaultHeight);
}
}
4. 添加自定义属性
在 attrs.xml 中定义自定义属性:
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
在构造方法中解析自定义属性:
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED);
typedArray.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
在布局中使用自定义属性:
<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">
<com.example.viewserise.CircleView
android:id="@+id/circle_view1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:background="#000000"
android:layout_margin="20dp"
android:padding="20dp"
app:circle_color="#03DAC5"/>
</LinearLayout>
自定义组合控件
自定义组合控件是一种将多个现有控件组合在一起,形成一个新控件的方法。这种方法可以提高代码复用性和可维护性。以下是创建自定义组合控件的基本步骤:
1. 创建自定义组合控件的布局文件
首先,为组合控件创建一个布局文件。例如,假设我们要创建一个包含 ImageView 和 TextView 的自定义组合控件,布局文件可以这样写:
<!-- res/layout/custom_view.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:id="@+id/custom_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_foreground" />
<TextView
android:id="@+id/custom_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Custom View" />
</LinearLayout>
2. 创建自定义组合控件类
接下来,在代码中创建一个类来扩展 LinearLayout 或其他适合的布局类,并在构造函数中加载布局文件:
// CustomView.java
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
public class CustomView extends LinearLayout {
private ImageView imageView;
private TextView textView;
public CustomView(Context context) {
super(context);
init(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
// Inflate the layout
LayoutInflater.from(context).inflate(R.layout.custom_view, this, true);
// Get references to the child views
imageView = findViewById(R.id.custom_image);
textView = findViewById(R.id.custom_text);
}
// Custom methods to set image and text
public void setImageResource(int resId) {
imageView.setImageResource(resId);
}
public void setText(String text) {
textView.setText(text);
}
}
3. 在布局文件中使用自定义组合控件
现在,可以在其他布局文件中使用自定义组合控件,就像使用普通控件一样:
<!-- res/layout/activity_main.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.myapp.CustomView
android:id="@+id/my_custom_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
4. 在 Activity 或 Fragment 中使用自定义组合控件
在 Activity 或 Fragment 中,通过 ID 获取自定义组合控件,并使用其自定义方法:
// MainActivity.java
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CustomView customView = findViewById(R.id.my_custom_view);
customView.setImageResource(R.drawable.ic_launcher_foreground);
customView.setText("Hello, Custom View!");
}
}