Android自定义View-圆形渐变多点的加载框
本文主要记录创建一个 Android 自定义加载弹窗,实现指定个数且从小到大的实心圆周期性旋转的效果。
附上效果如下:
一: 自定义圆形loadingView
1.1 核心属性定义
//默认参数private int circleCount = 8; // 圆圈数量private int minCircleRadius = 5; // 最小圆圈半径private int maxCircleRadius = 15; // 最大圆圈半径private int circleColor = Color.parseColor("#3F51B5"); //默认颜色
这些属性控制了加载动画的外观:圆的数量、大小范围、颜色以及动画实例。
1.2 构造方法
public CircleLoadingView(Context context) {super(context);init(null);}public CircleLoadingView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(attrs);}public CircleLoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(attrs);}
- 实现了 3 个构造方法,覆盖了代码创建和 XML 布局引用两种场景
- 所有构造方法最终都调用
init()
方法完成初始化,符合 Android 自定义 View 的最佳实践
1.3 初始化方法 init()
private void init(AttributeSet attrs) {if (attrs != null){TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CircleLoadingView);circleCount = ta.getInt(R.styleable.CircleLoadingView_circleCount, circleCount);minCircleRadius = ta.getInt(R.styleable.CircleLoadingView_minCircleRadius, minCircleRadius);maxCircleRadius = ta.getInt(R.styleable.CircleLoadingView_maxCircleRadius, maxCircleRadius);circleColor = ta.getColor(R.styleable.CircleLoadingView_circleColor, circleColor);ta.recycle();}paint = new Paint();paint.setColor(circleColor);//画笔颜色paint.setStyle(Paint.Style.FILL);//填充 绘制实心圆paint.setAntiAlias(true);//抗锯齿initAnimation();}
- 自定义属性处理:通过
TypedArray
读取 XML 中设置的自定义属性(如圆的数量、颜色等),如果没设置则使用默认值 - 画笔初始化:配置绘制圆形的画笔,设置为实心、抗锯齿
- 动画初始化:调用
initAnimation()
方法创建旋转动画
1.4 动画实现
// 创建旋转动画,3秒完成一圈,无限循环private void initAnimation() {rotateAnimation = new RotateAnimation(0, 360,Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 0.5f);rotateAnimation.setDuration(3000);rotateAnimation.setRepeatCount(Animation.INFINITE);rotateAnimation.setInterpolator(new LinearInterpolator());startAnimation(rotateAnimation);}
1.5 绘制
这是自定义 View 的核心方法,负责绘制所有圆形:
@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// 获取视图中心坐标int centerX = getWidth() / 2;int centerY = getHeight() / 2;// 计算可用的半径(视图最小边的一半减去最大圆半径)int availableRadius = Math.min(centerX, centerY) - maxCircleRadius;// 绘制每个圆for (int i = 0; i < circleCount; i++) {// 计算每个圆的角度(等分圆周,从顶部开始)// 减去90度(Math.PI/2)使第一个点位于顶部float angle = (float) (2 * Math.PI * i / circleCount - Math.PI / 2);// 计算当前圆的半径(从小到大渐变)float radius = minCircleRadius;if (circleCount > 1) {radius = minCircleRadius + (maxCircleRadius - minCircleRadius) * (float) i / (circleCount - 1);}// 计算圆的中心点坐标(均匀分布在圆形轨迹上)float x = centerX + (float) (availableRadius * Math.cos(angle));float y = centerY + (float) (availableRadius * Math.sin(angle));// 设置画笔颜色(可选:可以根据位置设置不同的透明度)paint.setAlpha(calculateAlpha(i));// 绘制圆canvas.drawCircle(x, y, radius, paint);}}/*** 根据圆点位置计算透明度,创建渐变效果* @param index 圆点索引* @return 透明度值(0-255)*/private int calculateAlpha(int index) {// 可以根据需要调整透明度计算逻辑// 这里创建一个渐变效果,第一个点最暗,最后一个点较亮if (circleCount <= 1) return 255;// 计算透明度,return 155 + (index * 100 / (circleCount - 1));}
1.6: 尺寸测量
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 设置默认大小int defaultSize = dp2px(80);int width = measureSize(defaultSize, widthMeasureSpec);int height = measureSize(defaultSize, heightMeasureSpec);setMeasuredDimension(width, height);}private int measureSize(int defaultSize, int measureSpec) {int result = defaultSize;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);if (specMode == MeasureSpec.EXACTLY) {result = specSize;} else if (specMode == MeasureSpec.AT_MOST) {result = Math.min(defaultSize, specSize);}return result;}// dp转pxprivate int dp2px(float dpValue) {final float scale = getContext().getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}
- 处理 View 的尺寸测量,支持
wrap_content
、match_parent
和固定尺寸 - 默认大小为 80dp,通过
dp2px()
方法将 dp 转为 px,适配不同屏幕密度
1.7 : attrs自定义参数
<!-- 自定义加载视图的属性 -->
<declare-styleable name="CircleLoadingView"><attr name="circleCount" format="integer" /><attr name="minCircleRadius" format="integer" /><attr name="maxCircleRadius" format="integer" /><attr name="circleColor" format="color" />
</declare-styleable>
目前定义了四个属性包括圆点的个数,颜色,最大最小的半径. 有其他需求的我们可以继续补充.
下面是完整的自定义View的代码:
public class CircleLoadingView extends View {private static final String TAG = "CircleLoadingView";//默认参数private int circleCount = 8; // 圆圈数量private int minCircleRadius = 5; // 最小圆圈半径private int maxCircleRadius = 15; // 最大圆圈半径private int circleColor = Color.parseColor("#3F51B5"); //默认颜色// 旋转动画private RotateAnimation rotateAnimation;// 画笔private Paint paint;public CircleLoadingView(Context context) {super(context);init(null);}public CircleLoadingView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(attrs);}public CircleLoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(attrs);}private void init(AttributeSet attrs) {if (attrs != null){TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CircleLoadingView);circleCount = ta.getInt(R.styleable.CircleLoadingView_circleCount, circleCount);minCircleRadius = ta.getInt(R.styleable.CircleLoadingView_minCircleRadius, minCircleRadius);maxCircleRadius = ta.getInt(R.styleable.CircleLoadingView_maxCircleRadius, maxCircleRadius);circleColor = ta.getColor(R.styleable.CircleLoadingView_circleColor, circleColor);ta.recycle();}paint = new Paint();paint.setColor(circleColor);//画笔颜色paint.setStyle(Paint.Style.FILL);//填充 绘制实心圆paint.setAntiAlias(true);//抗锯齿initAnimation();}// 创建旋转动画,3秒完成一圈,无限循环private void initAnimation() {rotateAnimation = new RotateAnimation(0, 360,Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 0.5f);rotateAnimation.setDuration(3000);rotateAnimation.setRepeatCount(Animation.INFINITE);rotateAnimation.setInterpolator(new LinearInterpolator());startAnimation(rotateAnimation);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// 获取视图中心坐标int centerX = getWidth() / 2;int centerY = getHeight() / 2;// 计算可用的半径(视图最小边的一半减去最大圆半径)int availableRadius = Math.min(centerX, centerY) - maxCircleRadius;// 绘制每个圆for (int i = 0; i < circleCount; i++) {// 计算每个圆的角度(等分圆周,从顶部开始)// 减去90度(Math.PI/2)使第一个点位于顶部float angle = (float) (2 * Math.PI * i / circleCount - Math.PI / 2);// 计算当前圆的半径(从小到大渐变)float radius = minCircleRadius;if (circleCount > 1) {radius = minCircleRadius + (maxCircleRadius - minCircleRadius) * (float) i / (circleCount - 1);}// 计算圆的中心点坐标(均匀分布在圆形轨迹上)float x = centerX + (float) (availableRadius * Math.cos(angle));float y = centerY + (float) (availableRadius * Math.sin(angle));// 设置画笔颜色(可选:可以根据位置设置不同的透明度)paint.setAlpha(calculateAlpha(i));// 绘制圆canvas.drawCircle(x, y, radius, paint);}}/*** 根据圆点位置计算透明度,创建渐变效果* @param index 圆点索引* @return 透明度值(0-255)*/private int calculateAlpha(int index) {// 可以根据需要调整透明度计算逻辑// 这里创建一个渐变效果,第一个点最暗,最后一个点较亮if (circleCount <= 1) return 255;// 计算透明度,return 155 + (index * 100 / (circleCount - 1));}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 设置默认大小int defaultSize = dp2px(80);int width = measureSize(defaultSize, widthMeasureSpec);int height = measureSize(defaultSize, heightMeasureSpec);setMeasuredDimension(width, height);}private int measureSize(int defaultSize, int measureSpec) {int result = defaultSize;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);if (specMode == MeasureSpec.EXACTLY) {result = specSize;} else if (specMode == MeasureSpec.AT_MOST) {result = Math.min(defaultSize, specSize);}return result;}// dp转pxprivate int dp2px(float dpValue) {final float scale = getContext().getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}// 开始动画public void startLoading() {if (rotateAnimation != null && rotateAnimation.hasEnded()) {startAnimation(rotateAnimation);}}// 停止动画public void stopLoading() {if (rotateAnimation != null) {clearAnimation();}}// 更新圆点的颜色public void setCircleColor(int color) {this.circleColor = color;paint.setColor(color);invalidate();}}
二: 自定义Dialog.
2.1 代码
这里我写的很简单, 直接集成DIalog. 界面也只有CircleLoadingView.
public class CircleDialog extends Dialog {private static final String TAG = "CircleDialog";private CircleLoadingView loadingView;public CircleDialog(@NonNull Context context) {super(context, R.style.LoadingDialogStyle);}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_circle_dialog);loadingView = findViewById(R.id.loading_view);}private void initWindow() {Window window = getWindow();if (window != null) {// 设置窗口属性WindowManager.LayoutParams params = window.getAttributes();// 设置窗口居中params.gravity = Gravity.CENTER;// 设置窗口宽高为包裹内容params.width = WindowManager.LayoutParams.WRAP_CONTENT;params.height = WindowManager.LayoutParams.WRAP_CONTENT;// 设置窗口背景透明度params.dimAmount = 0.3f;window.setAttributes(params);// 设置窗口背景为透明window.setBackgroundDrawableResource(android.R.color.transparent);// 设置窗口是否有遮罩层window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);}// 设置点击外部是否可取消setCanceledOnTouchOutside(false);}// 设置加载圆圈的颜色public void setCircleColor(int color) {if (loadingView != null) {loadingView.setCircleColor(color);}}@Overridepublic void show() {super.show();if (loadingView != null) {loadingView.startLoading();}}@Overridepublic void dismiss() {super.dismiss();if (loadingView != null) {loadingView.stopLoading();}}
}
2.2: layout布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"xmlns:app="http://schemas.android.com/apk/res-auto"android:orientation="vertical"><com.test.accessbilitytest.view.CircleLoadingViewandroid:layout_width="80dp"android:layout_height="80dp"android:id="@+id/loading_view"android:layout_gravity="center_horizontal"app:circleCount="8"app:minCircleRadius="10"app:maxCircleRadius="16"app:circleColor="#fffdbe32"/>
</LinearLayout>
2.3: dialog的样式
<!-- 加载弹窗样式 -->
<style name="LoadingDialogStyle" parent="@android:style/Theme.Dialog"><!-- 不显示标题栏 --><item name="android:windowNoTitle">true</item><!-- 背景透明 --><item name="android:windowBackground">@android:color/transparent</item><!-- 窗口进入退出动画 --><item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
</style>
三: 弹框的特点.
1. 核心是CircleLoadingView自定义视图,它负责绘制 8个从小到大的实心圆,并通过旋转动画实现周期性旋转效果。2. 通过自定义属性,您可以轻松调整:圆的数量(默认 8个)最小圆半径最大圆半径圆的颜色3. LoadingDialog封装了弹窗的显示逻辑,包括设置弹窗样式、背景透明度等。4. 使用方法简单:创建LoadingDialog实例调用show()方法显示调用dismiss()方法隐藏