05_自定义View

133 阅读5分钟

自定义 View 分类

  1. 继承 View 并重写 onDraw 方法 适用于实现无法通过布局组合来完成的不规则效果。需要通过绘制来实现,即重写 onDraw 方法。该方法需要自行支持 wrap_content,并处理 padding

  2. 继承 ViewGroup 并派生特殊的 Layout 用于自定义布局,创建系统布局之外的新布局。当需要将多种 View 组合在一起实现某种效果时,可以采用此方法。需要处理 ViewGroup 的测量和布局过程,以及子元素的测量和布局。

  3. 继承特定的 View(如 TextView) 常用于扩展已有 View 的功能,例如 TextView。实现相对简单,不需要自行支持 wrap_contentpadding

  4. 继承特定的 ViewGroup(如 LinearLayout) 当某种效果需要将几种 View 组合在一起时,可以采用此方法。与方法 2 的区别在于,方法 2 更接近 View 的底层,方法 4 不需要处理 ViewGroup 的测量和布局过程。

自定义 View 须知

  1. 让 View 支持 wrap_content 继承 View 或者 ViewGroup 的控件需要在 onMeasure 中对 wrap_content 做特殊处理,否则无法达到预期效果。

  2. 让 View 支持 padding 继承 View 的控件需要在 draw 方法中处理 padding。继承 ViewGroup 的控件需在 onMeasureonLayout 中考虑 padding 和子元素的 margin

  3. 尽量不要在 View 中使用 Handler View 内部提供了 post 系列的方法,可以替代 Handler 的作用,除非明确需要使用 Handler 发送消息。

  4. 及时停止线程或动画onDetachedFromWindow 中停止线程或动画,避免内存泄漏。onAttachedToWindow 对应 Activity 启动时调用,需要处理 View 不可见时的停止操作。

  5. 处理好滑动冲突 处理 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. 创建自定义组合控件的布局文件

首先,为组合控件创建一个布局文件。例如,假设我们要创建一个包含 ImageViewTextView 的自定义组合控件,布局文件可以这样写:

<!-- 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 中使用自定义组合控件

ActivityFragment 中,通过 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!");
    }
}

View系列文章

01_View基础知识

02_View的滑动

03_View的事件分发机制

04_View的工作流程

05_自定义View

05_自定义ViewGroup

06_View滑动冲突处理