在 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 可直接作为项目基础组件,适用于提示框、确认框、加载框以及全屏自定义布局,极大提高开发效率。