Android 通用 BaseDialog 实现:支持 ViewBinding + 全屏布局 + 加载弹窗

在 Android 开发中,Dialog 是最常用的提示、加载和确认控件之一。但原生 Dialog 有一些问题:

  • 布局受限,无法全屏或灵活留白
  • 重复创建布局和初始化逻辑
  • 点击事件、显示时长和加载状态管理不统一
  • 为此,我整理了一套 通用 BaseDialog 方案,具有以下特点:
  • 支持 ViewBinding 初始化
  • 支持 全屏、宽度自适应、左右留白
  • 支持 链式调用设置宽高、位置、动画、遮罩透明度
  • 内置 加载弹窗管理
  • 支持 延迟关闭、强制关闭

本文将详细讲解实现原理和使用方法。

一、BaseDialog 类设计

1. 泛型支持 ViewBinding

bash 复制代码
public abstract class BaseDialog<VB extends ViewBinding> extends Dialog {
    protected VB mViewBinding;
}
  • VB 泛型支持 ViewBinding
  • 子类可直接操作绑定视图,无需 findViewById
  • 子类需要实现:
    • initViewBinding() → 返回 ViewBinding 实例
    • initialize() → 初始化视图逻辑
    • initListener() → 设置点击/交互事件
    • getLayoutId() → 可选布局资源(0 表示使用 ViewBinding)

2. 构造与初始化

bash 复制代码
public BaseDialog(@NonNull Context context)
public BaseDialog(@NonNull Context context, int themeResId)
  • 在 onCreate() 中判断使用 ViewBinding 或传统布局
  • 调用 initialize() 初始化界面
  • 调用 initListener() 设置监听
  • 初始化加载弹窗 AdvancedLoadingDialog

二、布局控制与全屏适配

1. 宽高设置(链式调用)

bash 复制代码
dialog.setWidthDp(300)
      .setHeightDp(200)
      .setGravity(Gravity.CENTER)
      .setWindowAnimation(R.style.DialogAnim)
      .setDimAmount(0.5f);
  • 支持 px、dp、屏幕比例 多种方式
  • 支持 链式调用,灵活设置宽高、位置、动画和遮罩透明度

2. 全屏布局

bash 复制代码
dialog.applyFullWidthLayout(); // 宽度 MATCH_PARENT,高度 WRAP_CONTENT
dialog.applyFullWidthHeightLayout(); // 宽高都 MATCH_PARENT
dialog.applyFullWidthLayoutWithMargin(40); // 留白效果
  • 内部通过 FLAG_LAYOUT_NO_LIMITS 扩展到整个屏幕,包括状态栏、导航栏
  • 视觉效果更加沉浸、灵活

3. 样式文件(BaseDialogStyle)

bash 复制代码
    <!-- Base-Dialog-Styles.xml -->
    <style name="BaseDialogStyle" parent="Theme.AppCompat.Dialog">
        <!-- 边框 -->
        <item name="android:windowFrame">@null</item>
        <!-- 是否浮现在activity之上 -->
        <item name="android:windowIsFloating">true</item>
        <!-- 半透明 -->
        <item name="android:windowIsTranslucent">true</item>
        <!-- 无标题 -->
        <item name="android:windowNoTitle">true</item>
        <item name="android:background">@color/transparent</item>
        <!-- 背景透明 -->
        <item name="android:windowBackground">@color/transparent</item>
        <!-- 模糊 -->
        <item name="android:backgroundDimEnabled">true</item>
        <!-- 遮罩层 -->
        <item name="android:backgroundDimAmount">0.5</item>
        <item name="android:fitsSystemWindows">true</item>
        <item name="android:windowContentOverlay">@null</item>
    </style>
  • 透明背景 + 半透明遮罩
  • 支持模糊或淡化效果
  • 去除系统默认边框、标题和阴影

三、显示控制与延迟关闭

1. 延迟关闭

bash 复制代码
@Override
public void show() {
    showTime = System.currentTimeMillis();
    super.show();
}

public void delayedDismiss() {
    long elapsedTime = System.currentTimeMillis() - showTime;
    if (elapsedTime < 2000) {
        handler.postDelayed(dismissRunnable, 2000 - elapsedTime);
    } else {
        dismiss();
    }
}

public void forceDismiss() {
    dismiss();
}
  • 延迟关闭保证用户至少看到提示 2 秒
  • 强制关闭可立即退出 Dialog

四、加载弹窗封装

bash 复制代码
private AdvancedLoadingDialog mLoadingDialog;

public void showLoading(String message)
public void showLoading()
public void delayDismissLoading()
public void forceDismissLoading()
  • 内置 AdvancedLoadingDialog
  • 支持 带文字 / 不带文字
  • 支持 延迟关闭 / 强制关闭
  • 避免每次创建加载框,提高性能和一致性

五、工具方法

bash 复制代码
protected <T extends View> T findView(int resId) { ... }
public static int dp2px(Context context, float dp) { ... }
  • findView() 封装视图查找
  • dp2px() 保证多屏幕适配

六、示例使用

1. 定义自定义 Dialog

bash 复制代码
public class CustomDialog extends BaseDialog<DialogCustomBinding> {

    public CustomDialog(@NonNull Context context) {
        super(context);
    }

    @Override
    protected int getLayoutId() { return 0; } // 使用 ViewBinding

    @Override
    protected DialogCustomBinding initViewBinding() {
        return DialogCustomBinding.inflate(getLayoutInflater());
    }

    @Override
    protected void initialize() {
        mViewBinding.tvTitle.setText("提示信息");
    }

    @Override
    protected void initListener() {
        mViewBinding.btnOk.setOnClickListener(v -> dismiss());
    }
}

2. 使用 Dialog

bash 复制代码
CustomDialog dialog = new CustomDialog(this);
dialog.applyFullWidthLayoutWithMargin(40)
      .setWindowAnimation(R.style.DialogAnim)
      .setDimAmount(0.5f)
      .show();

七、BaseDialog代码示例

bash 复制代码
package com.xxxx.xxxx.base;

import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.viewbinding.ViewBinding;
import com.xxxx.xxxx.dialog.AdvancedLoadingDialog;

public abstract class BaseDialog<VB extends ViewBinding> extends Dialog {

    private static final int DEFAULT_GRAVITY = Gravity.CENTER;
    private long showTime;
    private final Handler handler = new Handler(Looper.getMainLooper());
    private final Runnable dismissRunnable = this::dismiss; // 用于延迟 dismiss 的 Runnable
    protected VB mViewBinding;
    // 加载弹窗
    private AdvancedLoadingDialog mLoadingDialog;
    private Context mContext = null;

    public BaseDialog(@NonNull Context context) {
        super(context);
        mContext = context;
    }

    public BaseDialog(@NonNull Context context, int themeResId) {
        super(context, themeResId);
        mContext = context;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 判断是否使用 ViewBinding 初始化视图
        if (getLayoutId() == 0) {
            if(initViewBinding() != null) {
                mViewBinding = initViewBinding();
                setContentView(mViewBinding.getRoot());
            }
        } else {
            setContentView(getLayoutId());
        }

        initialize();
        initListener();
        initLoadingDialog();
    }

    /**
     * 获取布局资源ID
     */
    protected abstract int getLayoutId();


    /**
     * 当使用 ViewBinding 时,子类必须实现该方法来返回对应的 ViewBinding 实例。
     */
    protected abstract VB initViewBinding();

    /**
     * 初始化视图和逻辑
     */
    protected abstract void initialize();

    /**
     * 设置监听事件,子类必须实现
     */
    protected abstract void initListener();

    public void initWindowParams(boolean widthMatch, boolean heightMatch) {
        Window window = getWindow();
        if (window != null) {
            WindowManager.LayoutParams params = window.getAttributes();

            // 默认参数设置
            params.gravity = DEFAULT_GRAVITY;
            params.width = widthMatch ? WindowManager.LayoutParams.MATCH_PARENT : WindowManager.LayoutParams.WRAP_CONTENT;
            params.height = heightMatch ? WindowManager.LayoutParams.MATCH_PARENT : WindowManager.LayoutParams.WRAP_CONTENT;

            window.setAttributes(params);
        }
    }

    /**
     * 应用全屏宽度布局设置:
     * 1. 去除系统默认 Dialog 背景边距;
     * 2. 宽度设置为 MATCH_PARENT,撑满屏幕;
     * 3. 高度设置为 WRAP_CONTENT;
     * 4. 设置 FLAG_LAYOUT_NO_LIMITS 允许覆盖底部虚拟按键区域(如导航栏)。
     */
    public void applyFullWidthLayout() {
        Window window = getWindow();
        if (window != null) {
            // 去除系统默认边距(背景带圆角或阴影)
            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

            // 允许布局扩展到整个屏幕(包括状态栏、导航栏)
            window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

            // 设置宽高属性
            window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);

        }
    }

    /**
     * 设置 Dialog 的宽度为屏幕宽度减去左右边距(单位为 dp),实现左右留白的全屏效果。
     *
     * 功能说明:
     * 1. 去除系统默认 Dialog 背景边距(背景阴影、圆角等);
     * 2. 设置宽度为:屏幕宽度 - 2 * horizontalMarginDp;
     * 3. 设置高度为 WRAP_CONTENT;
     * 4. 支持覆盖状态栏、导航栏区域(通过 FLAG_LAYOUT_NO_LIMITS,视情况可保留或删除);
     *
     * @param totalHorizontalMarginDp 左右边距,单位为 dp(density-independent pixels)
     */
    public void applyFullWidthLayoutWithMargin(int totalHorizontalMarginDp) {
        Window window = getWindow();
        if (window != null) {
            // 去除系统默认白色背景和边距(圆角、阴影等)
            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

            // 设置 Dialog 宽度为屏幕宽度,高度为 WRAP_CONTENT(高度自适应)
            window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);

            // 清除窗口背景遮罩(默认半透明),可选:看是否需要灰色背景
            window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
        }

        // 获取当前 Dialog 的最外层 DecorView(包括背景区域)
        View decorView = getWindow().getDecorView();

        // 将 dp 单位的 margin 转为像素值(px)
        float density = decorView.getResources().getDisplayMetrics().density;
        int halfPaddingPx = (int) ((totalHorizontalMarginDp / 2f) * density + 0.5f);

        // 设置 DecorView 左右 padding,实现视觉左右留白
        // 上下 padding 保持为 0(如需可扩展)
        decorView.setPadding(halfPaddingPx, 0, halfPaddingPx, 0);
    }



    /**
     * 应用全屏宽度布局设置:
     * 1. 去除系统默认 Dialog 背景边距;
     * 2. 宽度设置为 MATCH_PARENT,撑满屏幕;
     * 3. 高度设置为 MATCH_PARENT,撑满屏幕;
     * 4. 设置 FLAG_LAYOUT_NO_LIMITS 允许覆盖底部虚拟按键区域(如导航栏)。
     */
    public void applyFullWidthHeightLayout() {
        Window window = getWindow();
        if (window != null) {
            // 去除系统默认边距(背景带圆角或阴影)
            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

            // 允许布局扩展到整个屏幕(包括状态栏、导航栏)
            window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

            // 设置宽高属性
            window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);

        }
    }

    //------------------------ 链式调用方法 ------------------------//

    /**
     * 设置弹窗宽度(单位:px)
     */
    public BaseDialog setWidth(int width) {
        Window window = getWindow();
        if (window != null) {
            WindowManager.LayoutParams params = window.getAttributes();
            params.width = width;
            window.setAttributes(params);
        }
        return this;
    }

    /**
     * 设置弹窗高度(单位:px)
     */
    public BaseDialog setHeight(int height) {
        Window window = getWindow();
        if (window != null) {
            WindowManager.LayoutParams params = window.getAttributes();
            params.height = height;
            window.setAttributes(params);
        }
        return this;
    }

    /**
     * 设置弹窗宽度(单位:dp)
     */
    public BaseDialog setWidthDp(int dp) {
        return setWidth(dp2px(getContext(), dp));
    }

    /**
     * 设置弹窗高度(单位:dp)
     */
    public BaseDialog setHeightDp(int dp) {
        return setHeight(dp2px(getContext(), dp));
    }

    /**
     * 按屏幕宽度比例设置弹窗尺寸
     * @param ratio 比例值(0.0-1.0)
     */
    public BaseDialog setWidthRatio(float ratio) {
        Window window = getWindow();
        if (window != null) {
            DisplayMetrics metrics = new DisplayMetrics();
            window.getWindowManager().getDefaultDisplay().getMetrics(metrics);
            setWidth((int) (metrics.widthPixels * ratio));
        }
        return this;
    }

    /**
     * 设置弹窗位置
     */
    public BaseDialog setGravity(int gravity) {
        Window window = getWindow();
        if (window != null) {
            window.setGravity(gravity);
        }
        return this;
    }

    /**
     * 设置窗口动画
     */
    public BaseDialog setWindowAnimation(int styleResId) {
        Window window = getWindow();
        if (window != null) {
            window.setWindowAnimations(styleResId);
        }
        return this;
    }

    /**
     * 设置背景遮罩透明度
     * @param dimAmount 0.0(完全透明)~1.0(完全不透明)
     */
    public BaseDialog setDimAmount(float dimAmount) {
        Window window = getWindow();
        if (window != null) {
            WindowManager.LayoutParams params = window.getAttributes();
            params.dimAmount = dimAmount;
            window.setAttributes(params);
        }
        return this;
    }

    /**
     * 设置点击外部是否可关闭
     */
    public void setDialogCancelOutside(boolean cancel) {
        setCanceledOnTouchOutside(cancel);
    }
    /**
     * 设置点击返回按键是否可关闭
     */
    public void setDialogCancelable(boolean cancel) {
        setCancelable(cancel);
    }


    //------------------------ 工具方法 ------------------------//

    protected <T extends View> T findView(int resId) {
        return findViewById(resId);
    }

    public static int dp2px(Context context, float dp) {
        float density = context.getResources().getDisplayMetrics().density;
        return (int) (dp * density + 0.5f);
    }

    //------------------------ 显示控制 ------------------------//

    @Override
    public void show() {
        try {
            showTime = System.currentTimeMillis(); // 记录 show 的时间
            super.show();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 延迟关闭(确保显示至少 2 秒)
     */
    public void delayedDismiss() {
        long elapsedTime = System.currentTimeMillis() - showTime;
        if (elapsedTime < 2000) { // 如果显示时间小于 2 秒
            handler.postDelayed(dismissRunnable, 2000 - elapsedTime); // 延迟关闭
        } else {
            dismiss();
        }
    }

    /**
     * 立即关闭(无论是否到 2 秒)
     */
    public void forceDismiss() {
        dismiss();
    }


    /**
     * 初始化加载弹窗
     */
    private void initLoadingDialog() {
        mLoadingDialog = new AdvancedLoadingDialog(mContext);
    }

    /**
     * 显示加载对话框
     *
     * @param message 要显示的提示信息
     */
    public void showLoading(String message) {
        if (mLoadingDialog != null && !mLoadingDialog.isShowing()) {
            mLoadingDialog.setMessage(message);
            mLoadingDialog.show();
        }
    }
    public void showLoading() {
        if (mLoadingDialog != null && !mLoadingDialog.isShowing()) {
            mLoadingDialog.show();
        }
    }

    /**
     * 延迟关闭加载对话框
     */
    public void delayDismissLoading() {
        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
            mLoadingDialog.delayedDismiss();
        }
    }

    /**
     * 强制立即关闭加载对话框
     */
    public void forceDismissLoading() {
        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
            mLoadingDialog.forceDismiss();
        }
    }

}

八、AdvancedLoadingDialog使用示例

bash 复制代码
package com.xxxx.xxxx.dialog;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import com.xxxx.xxxx.R;
import com.xxxx.xxxx.base.BaseDialog;
import com.xxxx.xxxx.base.MyApplication;
import com.xxxx.xxxx.databinding.DialogAdvancedLoadingBinding;
import com.xxxx.xxxx.utils.StringUtils;

/**
 * Author: Su
 * Date: 2025/4/25
 * Description: Description
 */

public class AdvancedLoadingDialog extends BaseDialog<DialogAdvancedLoadingBinding> {

    private long showTime; // 记录对话框显示的时间
    private static final long MIN_SHOW_DURATION = 1000L; // 最短显示时长,单位毫秒
    private final Handler handler = new Handler(Looper.getMainLooper()); // 主线程的 Handler
    private final Runnable dismissRunnable = this::dismiss; // 延迟关闭对话框的 Runnable
    private String pendingMessage; // 待显示的消息文本

    public AdvancedLoadingDialog(@NonNull Context context) {
        super(context, R.style.BaseDialogStyle);
    }

    @Override
    protected int getLayoutId() {
        return 0;
    }

    @Override
    protected DialogAdvancedLoadingBinding initViewBinding() {
        return DialogAdvancedLoadingBinding.inflate(getLayoutInflater());
    }

    @Override
    protected void initialize() {
        setDialogCancelable(true); // 设置对话框可取消
        setDialogCancelOutside(false); // 设置点击外部区域时取消对话框

        setWidth((int) MyApplication.getFloatDimension(R.dimen.dimens_160dp));
        setHeight((int) MyApplication.getFloatDimension(R.dimen.dimens_160dp));
    }

    @Override
    public void show() {
        super.show();

        mViewBinding.ivLoading.startAnimation();
        showTime = System.currentTimeMillis(); // 记录当前时间

        // 如果有待显示的消息,则显示该消息
        if (pendingMessage != null) {
            mViewBinding.tvContent.setText(pendingMessage);
            pendingMessage = null; // 清空待显示的消息
        } else {
            // 默认显示"加载中..."消息
            mViewBinding.tvContent.setText(StringUtils.stringEmpty(pendingMessage, R.string.dialog_loading_content1));
        }
    }

    @Override
    public void dismiss() {
        super.dismiss();
        mViewBinding.ivLoading.stopAnimation();
    }

    // 延迟关闭对话框,确保对话框至少显示指定的最短时间
    public void delayedDismiss() {
        long elapsedTime = System.currentTimeMillis() - showTime;
        if (elapsedTime < MIN_SHOW_DURATION) {
            // 如果显示时间小于最短显示时长,则延迟关闭
            handler.postDelayed(dismissRunnable, MIN_SHOW_DURATION - elapsedTime);
        } else {
            dismiss(); // 立即关闭对话框
        }
    }

    // 立即关闭对话框
    public void forceDismiss() {
        dismiss();
    }

    // 设置对话框中显示的消息文本
    public void setContentMessage(String message) {
        mViewBinding.tvContent.setText(message);
    }

    // 设置待显示的消息文本
    public void setMessage(String message) {
        pendingMessage = message;
    }


    @Override
    protected void initListener() {

    }

}

DialogAdvancedLoadingBinding

bash 复制代码
<?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="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_radius6_color1">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center"
        android:layout_centerInParent="true">
        <com.xxxx.xxxx.widget.GearLoadingView
            android:id="@+id/iv_loading"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:gearGradientStartColor="@color/advanced_loading_dialog_color2"
            app:gearGradientEndColor="@color/advanced_loading_dialog_color3"
            app:gearCount="12"
            app:innerGearCount="0"
            app:outerSizeRatio="0.4"
            app:gearAspectRatio="4"
            app:gearLengthRatio="2"
            app:gearThickness="0.4"
            app:animDuration="2000"/>
        <TextView
            android:id="@+id/tv_content"
            android:layout_below="@id/iv_loading"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/dialog_loading_content1"
            android:includeFontPadding="false"
            android:textColor="@color/advanced_loading_dialog_color2"
            android:textSize="@dimen/dimens_16dp"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="@dimen/dimens_9dp"></TextView>
    </LinearLayout>

</RelativeLayout>

GearLoadingView

bash 复制代码
package com.xxxx.xxxx.widget;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.LinearInterpolator;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;

import com.xxxx.xxxx.R;

/**
 * Author: Su
 * Date: 2025/5/8
 * Description:
 * 仿苹果风格齿轮加载动画控件
 * 功能特性:
 * 1. 支持自定义齿轮数量、颜色、尺寸、厚度等属性
 * 2. 支持内外双层齿轮反向旋转动画
 * 3. 支持透明度渐变效果
 * 4. 完善的XML属性支持
 * 5. 生命周期感知自动启停动画
 */
public class GearLoadingView extends View {

    // 默认参数
    private static final int DEFAULT_COLOR = Color.parseColor("#888888");
    private static final int DEFAULT_SIZE_DP = 48;
    private static final int DEFAULT_GEAR_COUNT = 12;
    private static final int DEFAULT_ANIM_DURATION = 1500;
    private static final float DEFAULT_GEAR_THICKNESS_RATIO = 0.15f;

    // 绘制工具
    private Paint mGearPaint;
    private Path mGearPath; // 复用Path对象避免重复创建

    // 动画相关
    private ValueAnimator mOuterAnimator;
    private ValueAnimator mInnerAnimator;
    private float mOuterRotateDegree = 0;
    private float mInnerRotateDegree = 0;

    // 可配置参数
    private int mStartColor = Color.WHITE;       // 渐变颜色start
    private int mEndColor = Color.BLACK;         // 渐变颜色end
    private int mViewSize;                       // 视图尺寸
    private int mGearCount = DEFAULT_GEAR_COUNT; // 外层齿轮数量
    private int mInnerGearCount = 6;             // 内层齿轮数量
    private float mGearThicknessRatio = DEFAULT_GEAR_THICKNESS_RATIO; // 齿轮厚度比例
    private int mAnimationDuration = DEFAULT_ANIM_DURATION; // 动画周期

    // 新增配置参数
    private float mOuterSizeRatio = 0.7f;  // 外层齿轮环大小比例
    private float mInnerSizeRatio = 0.4f;  // 内层齿轮环大小比例
    private float mGearLengthRatio = 1.2f; // 齿轮长度比例系数
    // 齿轮单体宽高比例
    private float mGearAspectRatio = 4.0f; // 默认细长(宽:高 = 4:1)


    public GearLoadingView(Context context) {
        super(context);
        init(null);
    }

    public GearLoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public GearLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    /**
     * 初始化方法和配置参数
     */
    private void init(AttributeSet attrs) {
        // 将默认尺寸转换为像素
        mViewSize = dpToPx(DEFAULT_SIZE_DP);

        // 解析XML属性
        if (attrs != null) {
            TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.GearLoadingView);

            mViewSize = a.getDimensionPixelSize(R.styleable.GearLoadingView_android_layout_width, mViewSize);
            mGearCount = a.getInt(R.styleable.GearLoadingView_gearCount, DEFAULT_GEAR_COUNT);
            mInnerGearCount = a.getInt(R.styleable.GearLoadingView_innerGearCount, mInnerGearCount);
            mGearThicknessRatio = a.getFloat(R.styleable.GearLoadingView_gearThickness, DEFAULT_GEAR_THICKNESS_RATIO);
            mAnimationDuration = a.getInt(R.styleable.GearLoadingView_animDuration, DEFAULT_ANIM_DURATION);
            mOuterSizeRatio = a.getFloat(R.styleable.GearLoadingView_outerSizeRatio, 0.7f);
            mInnerSizeRatio = a.getFloat(R.styleable.GearLoadingView_innerSizeRatio, 0.4f);
            mGearLengthRatio = a.getFloat(R.styleable.GearLoadingView_gearLengthRatio, 1.2f);
            mGearAspectRatio = a.getFloat(R.styleable.GearLoadingView_gearAspectRatio, 4.0f);
            mStartColor = a.getColor(R.styleable.GearLoadingView_gearGradientStartColor, Color.WHITE);
            mEndColor = a.getColor(R.styleable.GearLoadingView_gearGradientEndColor, Color.BLACK);
            a.recycle();
        }

        // 初始化画笔
        mGearPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mGearPaint.setStyle(Paint.Style.FILL);

        // 创建复用Path对象
        mGearPath = new Path();

        // 配置动画
        setupAnimators();
    }

    /**
     * 设置动画系统
     */
    private void setupAnimators() {
        // 外层齿轮顺时针旋转动画
        mOuterAnimator = ValueAnimator.ofFloat(0, 360);
        mOuterAnimator.setDuration(mAnimationDuration);
        mOuterAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mOuterAnimator.setInterpolator(new LinearInterpolator());
        mOuterAnimator.addUpdateListener(anim -> {
            mOuterRotateDegree = (float) anim.getAnimatedValue();
            invalidate();
        });

        // 内层齿轮动画(根据数量决定是否创建)
        if (mInnerGearCount > 0) {
            mInnerAnimator = ValueAnimator.ofFloat(0, -360);
            mInnerAnimator.setDuration(mAnimationDuration * 2);
            mInnerAnimator.setRepeatCount(ValueAnimator.INFINITE);
            mInnerAnimator.setInterpolator(new LinearInterpolator());
            mInnerAnimator.addUpdateListener(anim -> {
                mInnerRotateDegree = (float) anim.getAnimatedValue();
                invalidate();
            });
        }
    }

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

        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;

        // 绘制外层齿轮(调整大小比例)
        drawGearRing(canvas, centerX, centerY, mOuterRotateDegree, mGearCount, mOuterSizeRatio);

        // 条件绘制内层齿轮
        if (mInnerGearCount > 0) {
            drawGearRing(canvas, centerX, centerY, mInnerRotateDegree, mInnerGearCount, mInnerSizeRatio);
        }
    }

    /**
     * 绘制单个齿轮环
     * @param canvas 画布对象
     * @param centerX 中心X坐标
     * @param centerY 中心Y坐标
     * @param rotate 旋转角度
     * @param gearCount 齿轮数量
     * @param sizeRatio 相对于视图大小的比例
     */
    private void drawGearRing(Canvas canvas, int centerX, int centerY, float rotate, int gearCount, float sizeRatio) {
        canvas.save();
        canvas.translate(centerX, centerY);
        canvas.rotate(rotate);

        float radius = mViewSize * sizeRatio / 2;
        // 修改齿轮长度计算(加入长度比例系数)
        float gearLength = radius * mGearThicknessRatio * mGearLengthRatio;
        float gearWidth = gearLength * 0.6f;

        // 构建单个齿轮形状路径
        buildGearPath(radius, gearLength, gearWidth);

        for (int i = 0; i < gearCount; i++) {
            canvas.save();
            float angle = i * (360f / gearCount);
            canvas.rotate(angle);

            // 设置透明度渐变效果
//            float alphaRatio = 0.3f + 0.7f * (i % 2); // 奇偶齿轮交替透明度
//            mGearPaint.setAlpha((int)(255 * alphaRatio));
            float fraction = (float) i / (gearCount - 1);
            mGearPaint.setColor(blendColors(mStartColor, mEndColor, fraction));

            canvas.drawPath(mGearPath, mGearPaint);
            canvas.restore();
        }

        canvas.restore();
    }

    /**
     * 构建单个齿轮形状路径
     */
    private void buildGearPath(float radius, float length, float width) {
        mGearPath.reset();

        // 修改齿轮形状为更细长的梯形(原0.8改为0.9)
//        mGearPath.moveTo(radius, -width/2);
//        mGearPath.lineTo(radius + length, -width/2);
//        mGearPath.lineTo(radius + length*0.9f, width/2); // 调整此处系数使齿更长
//        mGearPath.lineTo(radius, width/2);
//        mGearPath.close();

        // 圆角矩形作为椭圆形齿轮
//        RectF rect = new RectF(radius, -width / 2, radius + length, width / 2);
//
//        // 计算圆角半径,确保圆润但不变形
//        float cornerRadius = width / 2; // 完全圆角效果
//        mGearPath.addRoundRect(rect, cornerRadius, cornerRadius, Path.Direction.CW);

        // 使用比例计算宽度(越大越细)
        float adjustedWidth = length / mGearAspectRatio;

        RectF rect = new RectF(radius, -adjustedWidth / 2, radius + length, adjustedWidth / 2);
        float cornerRadius = adjustedWidth / 2;
        mGearPath.addRoundRect(rect, cornerRadius, cornerRadius, Path.Direction.CW);
    }

    private int blendColors(int startColor, int endColor, float ratio) {
        int a = (int) (Color.alpha(startColor) * (1 - ratio) + Color.alpha(endColor) * ratio);
        int r = (int) (Color.red(startColor) * (1 - ratio) + Color.red(endColor) * ratio);
        int g = (int) (Color.green(startColor) * (1 - ratio) + Color.green(endColor) * ratio);
        int b = (int) (Color.blue(startColor) * (1 - ratio) + Color.blue(endColor) * ratio);
        return Color.argb(a, r, g, b);
    }

    // 公共方法:设置齿轮颜色
    public void setGearGradient(@ColorInt int startColor, @ColorInt int endColor) {
        mStartColor = startColor;
        mEndColor = endColor;
        invalidate();
    }

    // 新增设置方法
    public void setOuterSizeRatio(float ratio) {
        mOuterSizeRatio = Math.max(0.3f, Math.min(1.0f, ratio));
        invalidate();
    }

    public void setGearLengthRatio(float ratio) {
        mGearLengthRatio = Math.max(0.5f, Math.min(2.0f, ratio));
        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 处理测量逻辑
        int width = resolveSize(mViewSize, widthMeasureSpec);
        int height = resolveSize(mViewSize, heightMeasureSpec);
        int size = Math.min(width, height);
        setMeasuredDimension(size, size);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAnimation();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopAnimation();
    }

    @Override
    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        if (visibility == VISIBLE) {
            startAnimation();
        } else {
            stopAnimation();
        }
    }

    // 工具方法:dp转px
    private int dpToPx(float dp) {
        return (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }

    // 公共方法:设置齿轮数量
    public void setGearCount(int count) {
        mGearCount = Math.max(3, count); // 至少3个齿轮
        invalidate();
    }

    // 启动动画
    public void startAnimation() {
        if (!mOuterAnimator.isRunning()) {
            mOuterAnimator.start();
            if (mInnerGearCount > 0 && mInnerAnimator != null) {
                mInnerAnimator.start();
            }
        }
    }

    // 停止动画
    public void stopAnimation() {
        if (mOuterAnimator.isRunning()) {
            mOuterAnimator.cancel();
            if (mInnerAnimator != null) {
                mInnerAnimator.cancel();
            }
        }
    }

    // 设置动画时长
    public void setAnimationDuration(int durationMs) {
        mAnimationDuration = Math.max(500, durationMs);
        mOuterAnimator.setDuration(mAnimationDuration);
        mInnerAnimator.setDuration(mAnimationDuration * 2);
    }
}

九、优势总结

1. 通用性强 :支持 ViewBinding 或传统布局
2. 全屏灵活 :宽高自适应、留白、沉浸式
3. 链式配置 :宽高、位置、动画、遮罩都可链式设置
4. 加载弹窗内置 :统一管理,避免重复创建
5. 显示控制完善:延迟关闭、强制关闭、保证至少显示 2 秒

这套 BaseDialog 可直接作为项目基础组件,适用于提示框、确认框、加载框以及全屏自定义布局,极大提高开发效率。

相关推荐
生产队队长2 小时前
Linux:awk进行行列转换操作
android·linux·运维
叶羽西2 小时前
Android15 EVS HAL中使用Camera HAL Provider接口
android
2501_915918412 小时前
除了 Perfdog,如何在 Windows 环境中完成 iOS App 的性能测试工作
android·ios·小程序·https·uni-app·iphone·webview
泓博2 小时前
Android状态栏文字图标设置失效
android·composer
叶羽西3 小时前
Android15系统中(娱乐框架和车机框架)中对摄像头的朝向是怎么定义的
android
Java小白中的菜鸟3 小时前
安卓studio链接夜神模拟器的一些问题
android
莫比乌斯环3 小时前
【Android技能点】深入解析 Android 中 Handler、Looper 和 Message 的关系及全局监听方案
android·消息队列
编程之路从0到14 小时前
React Native新架构之Android端初始化源码分析
android·react native·源码阅读
行稳方能走远4 小时前
Android java 学习笔记2
android·java