白雪新号 發表於 2025-9-14 14:35:23

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

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>附上效果如下:</li><li>一: 自定义圆形loadingView</li><ul class="second_class_ul"><li>1.1 核心属性定义</li><li>1.2 构造方法</li><li>1.3 初始化方法init()</li><li>1.4 动画实现</li><li>1.5 绘制</li><li>1.6: 尺寸测量</li><li>1.7 : attrs自定义参数</li></ul><li>二: 自定义Dialog.</li><ul class="second_class_ul"><li>2.1 代码</li><li>2.2: layout布局</li><li>2.3: dialog的样式</li></ul><li>三: 弹框的特点.</li><ul class="second_class_ul"></ul><li>总结&nbsp;</li><ul class="second_class_ul"></ul></ul></div><p>本文主要记录创建一个 Android 自定义加载弹窗,实现指定个数且从小到大的实心圆周期性旋转的效果。</p>
<p class="maodian"></p><h2>附上效果如下:</h2>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202509/2025914143234668.gif" /></p>
<p class="maodian"></p><h2>一: 自定义圆形loadingView</h2>
<p class="maodian"></p><h3>1.1 核心属性定义</h3>
<div class="jb51code"><pre class="brush:java;"> //默认参数
    private int circleCount = 8; // 圆圈数量
    private int minCircleRadius = 5; // 最小圆圈半径
    private int maxCircleRadius = 15; // 最大圆圈半径
    private int circleColor = Color.parseColor("#3F51B5"); //默认颜色
</pre></div>
<p>这些属性控制了加载动画的外观:圆的数量、大小范围、颜色以及动画实例。</p>
<p class="maodian"></p><h3>1.2 构造方法</h3>
<div class="jb51code"><pre class="brush:java;"> 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);
    }
</pre></div>
<ul><li>实现了 3 个构造方法,覆盖了代码创建和 XML 布局引用两种场景</li><li>所有构造方法最终都调用 <code>init()</code> 方法完成初始化,符合 Android 自定义 View 的最佳实践</li></ul>
<p class="maodian"></p><h3>1.3 初始化方法init()</h3>
<div class="jb51code"><pre class="brush:java;">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();
    }
</pre></div>
<ul><li><strong>自定义属性处理</strong>:通过 <code>TypedArray</code> 读取 XML 中设置的自定义属性(如圆的数量、颜色等),如果没设置则使用默认值</li><li><strong>画笔初始化</strong>:配置绘制圆形的画笔,设置为实心、抗锯齿</li><li><strong>动画初始化</strong>:调用 <code>initAnimation()</code> 方法创建旋转动画</li></ul>
<p class="maodian"></p><h3>1.4 动画实现</h3>
<div class="jb51code"><pre class="brush:java;"> // 创建旋转动画,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);
    }
</pre></div>
<p class="maodian"></p><h3>1.5 绘制</h3>
<p>这是自定义 View 的核心方法,负责绘制所有圆形:</p>
<div class="jb51code"><pre class="brush:java;"> @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 &lt; circleCount; i++) {
            // 计算每个圆的角度(等分圆周,从顶部开始)
            // 减去90度(Math.PI/2)使第一个点位于顶部
            float angle = (float) (2 * Math.PI * i / circleCount - Math.PI / 2);
            // 计算当前圆的半径(从小到大渐变)
            float radius = minCircleRadius;
            if (circleCount &gt; 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 &lt;= 1) return 255;
      // 计算透明度,
      return 155 + (index * 100 / (circleCount - 1));
    }
</pre></div>
<p class="maodian"></p><h3>1.6: 尺寸测量</h3>
<div class="jb51code"><pre class="brush:java;"> @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);
    }
</pre></div>
<ul><li>处理 View 的尺寸测量,支持 <code>wrap_content</code>、<code>match_parent</code> 和固定尺寸</li><li>默认大小为 80dp,通过 <code>dp2px()</code> 方法将 dp 转为 px,适配不同屏幕密度</li></ul>
<p class="maodian"></p><h3>1.7 : attrs自定义参数</h3>
<div class="jb51code"><pre class="brush:xml;">&lt;!-- 自定义加载视图的属性 --&gt;
&lt;declare-styleable name="CircleLoadingView"&gt;
    &lt;attr name="circleCount" format="integer" /&gt;
    &lt;attr name="minCircleRadius" format="integer" /&gt;
    &lt;attr name="maxCircleRadius" format="integer" /&gt;
    &lt;attr name="circleColor" format="color" /&gt;
&lt;/declare-styleable&gt;
</pre></div>
<p>目前定义了四个属性包括圆点的个数,颜色,最大最小的半径. 有其他需求的我们可以继续补充.</p>
<p>下面是完整的自定义View的代码:</p>
<div class="jb51code"><pre class="brush:java;">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 &lt; circleCount; i++) {
            // 计算每个圆的角度(等分圆周,从顶部开始)
            // 减去90度(Math.PI/2)使第一个点位于顶部
            float angle = (float) (2 * Math.PI * i / circleCount - Math.PI / 2);
            // 计算当前圆的半径(从小到大渐变)
            float radius = minCircleRadius;
            if (circleCount &gt; 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 &lt;= 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 &amp;&amp; rotateAnimation.hasEnded()) {
            startAnimation(rotateAnimation);
      }
    }

    // 停止动画
    public void stopLoading() {
      if (rotateAnimation != null) {
            clearAnimation();
      }
    }

    // 更新圆点的颜色
    public void setCircleColor(int color) {
      this.circleColor = color;
      paint.setColor(color);
      invalidate();
    }

}

</pre></div>
<p class="maodian"></p><h2>二: 自定义Dialog.</h2>
<p class="maodian"></p><h3>2.1 代码</h3>
<p>这里我写的很简单, 直接集成DIalog. 界面也只有CircleLoadingView.</p>
<div class="jb51code"><pre class="brush:java;">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();
      }
    }
}
</pre></div>
<p class="maodian"></p><h3>2.2: layout布局</h3>
<div class="jb51code"><pre class="brush:xml;">&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;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"&gt;

    &lt;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"
      /&gt;
&lt;/LinearLayout&gt;
</pre></div>
<p class="maodian"></p><h3>2.3: dialog的样式</h3>
<div class="jb51code"><pre class="brush:xml;">&lt;!-- 加载弹窗样式 --&gt;
&lt;style name="LoadingDialogStyle" parent="@android:style/Theme.Dialog"&gt;
    &lt;!-- 不显示标题栏 --&gt;
    &lt;item name="android:windowNoTitle"&gt;true&lt;/item&gt;
    &lt;!-- 背景透明 --&gt;
    &lt;item name="android:windowBackground"&gt;@android:color/transparent&lt;/item&gt;
    &lt;!-- 窗口进入退出动画 --&gt;
    &lt;item name="android:windowAnimationStyle"&gt;@android:style/Animation.Dialog&lt;/item&gt;
&lt;/style&gt;
</pre></div>
<p class="maodian"></p><h2>三: 弹框的特点.</h2>
<div class="jb51code"><pre class="brush:plain;">    1.        核心是CircleLoadingView自定义视图,它负责绘制 8个从小到大的实心圆,并通过旋转动画实现周期性旋转效果。
    2.        通过自定义属性,您可以轻松调整:
                       圆的数量(默认 8个)
                       最小圆半径
                       最大圆半径
                       圆的颜色
    3.        LoadingDialog封装了弹窗的显示逻辑,包括设置弹窗样式、背景透明度等。
    4.        使用方法简单:
       创建LoadingDialog实例
       调用show()方法显示
       调用dismiss()方法隐藏
</pre></div>
<p class="maodian"></p><h2>总结&nbsp;</h2>
<p>到此这篇关于Android自定义View实现圆形渐变多点的加载框效果的文章就介绍到这了,更多相关Android自定义加载弹窗内容请搜索琼殿技术社区以前的文章或继续浏览下面的相关文章希望大家以后多多支持琼殿技术社区!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>Android自定义带增长动画和点击弹窗提示效果的柱状图DEMO</li><li>Android编程实现的自定义弹窗(PopupWindow)功能示例</li><li>Android自定义弹窗提醒控件使用详解</li><li>Android 自定义加载动画Dialog弹窗效果的示例代码</li><li>Android自定义仿ios加载弹窗</li><li>Android自定义弹窗提示效果</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: Android自定义View实现圆形渐变多点的加载框效果