Android自定义View实现圆形渐变多点的加载框效果

 更新时间:2025年09月14日 14:35:23   作者:夏沫琅琊  
自定义View是Android开发中的一项重要技术,允许开发者创建满足特定需求的图形用户界面,这篇文章主要介绍了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 的核心方法,负责绘制所有圆形:

 @Override
    protected 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: 尺寸测量

 @Override
    protected 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转px
    private int dp2px(float dpValue) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
  • 处理 View 的尺寸测量,支持 wrap_contentmatch_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);
    }

    @Override
    protected 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));
    }

    @Override
    protected 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转px
    private 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);
    }

    @Override
    protected 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);
        }
    }

    @Override
    public void show() {
        super.show();
        if (loadingView != null) {
            loadingView.startLoading();
        }
    }

    @Override
    public 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.CircleLoadingView
        android: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()方法隐藏

总结 

到此这篇关于Android自定义View实现圆形渐变多点的加载框效果的文章就介绍到这了,更多相关Android自定义加载弹窗内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Kotlin协程概念原理与使用万字梳理

    Kotlin协程概念原理与使用万字梳理

    协程的作用是什么?协程是一种轻量级的线程,解决异步编程的复杂性,异步的代码使用协程可以用顺序进行表达,文中通过示例代码介绍详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-08-08
  • Android毕业设计备忘录APP

    Android毕业设计备忘录APP

    这篇文章主要介绍了一个Android毕业设计备忘录APP,它很小,但是功能很全,可实现添加、删除、修改、查看的功能,使用Java语言开发,风格简练
    2021-08-08
  • Android ToolBar 修改边距的实现方法

    Android ToolBar 修改边距的实现方法

    这篇文章主要介绍了Android ToolBar 修改边距的实现方法的相关资料,通过此文希望能帮助到大家,需要的朋友可以参考下
    2017-08-08
  • Android 中View.onDraw(Canvas canvas)的使用方法

    Android 中View.onDraw(Canvas canvas)的使用方法

    这篇文章主要介绍了Android 中View.onDraw(Canvas canvas)的使用方法的相关资料,希望通过本文能帮助到大家,需要的朋友可以参考下
    2017-09-09
  • android屏蔽按钮连续点击的示例代码

    android屏蔽按钮连续点击的示例代码

    这篇文章主要介绍了android屏蔽按钮连续点击的示例代码,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • Android实用编程技巧代码总结

    Android实用编程技巧代码总结

    这篇文章主要介绍了Android实用编程技巧代码,总结分析了Android编程中常见的实用代码段,包括图片、文本、控件等常用操作技巧,需要的朋友可以参考下
    2016-10-10
  • Android生存指南之:开发中的注意事项

    Android生存指南之:开发中的注意事项

    本篇文章是对在Android开发中的一些注意事项,需要的朋友可以参考下
    2013-05-05
  • DataBinding onClick的七种点击方式

    DataBinding onClick的七种点击方式

    这篇文章主要给大家介绍了关于DataBinding onClick的七种点击方式,文中通过示例代码介绍的非常详细,对各位Android开发者们具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-07-07
  • Android开源库自定义相机模块

    Android开源库自定义相机模块

    这篇文章主要为大家详细介绍了Android开源库自定义相机模块,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-05-05
  • Android截取指定View为图片的实现方法

    Android截取指定View为图片的实现方法

    这篇文章主要为大家详细介绍了Android截取指定View为图片的实现方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-06-06

最新评论