Android

Android Circle Progress Bar ( Segmented )

Machine_웅 2025. 12. 9. 14:21
728x90
반응형

 

 

// java

package com.test.alphav.CustomUI;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;

import androidx.annotation.Nullable;

import com.gitsn.alphav.R;

/**
 * 원형 Segmented Progress - 로딩 인디케이터용
 */
public class SegmentedCircleProgressView extends View {

    private int tickCount = 60;              // 전체 틱 개수
    private float tickWidthPx = 4f;          // 틱 두께
    private float tickLengthPx = 16f;        // 틱 길이
    private float innerRadiusRatio = 0.80f;  // 뷰 반지름 대비 안쪽 시작 비율 (0~1)

    private int activeColor = 0xFFFFFFFF;    // 채워진 틱 색
    private int inactiveColor = 0x55FFFFFF;  // 비활성 틱 색

    private long animDuration = 1200L;       // 인디케이터 회전 속도

    private float progress = 0f;             // 0.0 ~ 1.0
    private boolean isIndeterminate = true;  // true면 로딩 인디케이터처럼 회전

    private final Paint tickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private float radius;                    // 실제 그릴 때 사용할 반지름
    private float rotationOffset = 0f;       // 인디케이터 회전 각도

    private ValueAnimator animator;

    public SegmentedCircleProgressView(Context context) {
        this(context, null);
    }

    public SegmentedCircleProgressView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SegmentedCircleProgressView(Context context,
                                    @Nullable AttributeSet attrs,
                                    int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttrs(context, attrs);
        initPaint();
        initAnimator();
    }

    private void initAttrs(Context context, @Nullable AttributeSet attrs) {
        if (attrs == null) return;

        TypedArray a = context.obtainStyledAttributes(
                attrs,
                R.styleable.CircularTickProgressView
        );
        try {
            tickCount = a.getInt(R.styleable.CircularTickProgressView_ctp_tickCount, tickCount);
            tickWidthPx = a.getDimension(R.styleable.CircularTickProgressView_ctp_tickWidth, dpToPx(2));
            tickLengthPx = a.getDimension(R.styleable.CircularTickProgressView_ctp_tickLength, dpToPx(10));
            
            // 바깥쪽 원이 안쪽 원보다 떨어질 거리 (0~1)
            innerRadiusRatio = a.getFloat(R.styleable.CircularTickProgressView_ctp_innerRadiusRatio, innerRadiusRatio);
            activeColor = a.getColor(R.styleable.CircularTickProgressView_ctp_activeColor, activeColor);
            inactiveColor = a.getColor(R.styleable.CircularTickProgressView_ctp_inactiveColor, inactiveColor);
            animDuration = a.getInt(R.styleable.CircularTickProgressView_ctp_animDuration, (int) animDuration);
        } finally {
            a.recycle();
        }
    }

    private void initPaint() {
        tickPaint.setStyle(Paint.Style.STROKE);
        tickPaint.setStrokeCap(Paint.Cap.ROUND); // 양 끝 둥글게 (이미지 느낌 살리기)
        tickPaint.setStrokeWidth(tickWidthPx);
        tickPaint.setColor(activeColor);
    }

    private void initAnimator() {
        animator = ValueAnimator.ofFloat(0f, 360f);
        animator.setInterpolator(new LinearInterpolator());
        animator.setDuration(animDuration);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(animation -> {
            rotationOffset = (float) animation.getAnimatedValue();
            invalidate();
        });
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (isIndeterminate) {
            startIndeterminate();
        }
    }

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        radius = Math.min(w, h) / 2f;
    }

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

        final float cx = getWidth() / 2f;
        final float cy = getHeight() / 2f;
        final float angleStep = 360f / tickCount;

        // 얼마나 채울지 (ex: progress=0.5, tickCount=60 -> 30개)
        int activeTickCount = (int) (tickCount * clamp(progress, 0f, 1f));

        for (int i = 0; i < tickCount; i++) {
            // 진행/인디케이터에 따라 색 결정
            boolean isActive;
            if (isIndeterminate) {
                // 회전하는 25% 구간만 하얗게 보이게 (원하면 비율 조절)
                int visibleSpan = tickCount / 4;
                int head = (int) ((rotationOffset / 360f) * tickCount);
                int index = (i - head + tickCount) % tickCount;
                isActive = index < visibleSpan;
            } else {
                isActive = i < activeTickCount;
            }

            tickPaint.setColor(isActive ? activeColor : inactiveColor);

            float angle = (i * angleStep) - 90f; // 위쪽(12시 방향)에서 시작
            double rad = Math.toRadians(angle);

            float innerR = radius * innerRadiusRatio;
            float outerR = innerR + tickLengthPx;

            float startX = (float) (cx + innerR * Math.cos(rad));
            float startY = (float) (cy + innerR * Math.sin(rad));
            float endX = (float) (cx + outerR * Math.cos(rad));
            float endY = (float) (cy + outerR * Math.sin(rad));

            canvas.drawLine(startX, startY, endX, endY, tickPaint);
        }
    }

    // ======= 외부에서 쓸 메서드들 =======

    public void setProgress(float progress) {
        this.progress = clamp(progress, 0f, 1f);
        if (!isIndeterminate) {
            invalidate();
        }
    }

    public void setIndeterminate(boolean indeterminate) {
        if (this.isIndeterminate == indeterminate) return;
        this.isIndeterminate = indeterminate;
        if (indeterminate) {
            startIndeterminate();
        } else {
            stopIndeterminate();
        }
        invalidate();
    }

    private void startIndeterminate() {
        if (animator != null && !animator.isStarted()) {
            animator.start();
        }
    }

    private void stopIndeterminate() {
        if (animator != null && animator.isStarted()) {
            animator.cancel();
        }
    }

    private float dpToPx(float dp) {
        return dp * getResources().getDisplayMetrics().density;
    }

    private float clamp(float v, float min, float max) {
        return Math.max(min, Math.min(max, v));
    }
}

 

 

// xml

        <com.test.alphav.CustomUI.SegmentedCircleProgressView
            android:id="@+id/recordRing"
            android:layout_width="230dp"
            android:layout_height="230dp"
            app:ctp_tickCount="100"
            app:ctp_tickWidth="3dp"
            app:ctp_tickLength="12dp"
            app:ctp_innerRadiusRatio="0.84"

            app:ctp_activeColor="#40FFFFFF"
            app:ctp_inactiveColor="#FFFFFFFF"
            app:ctp_animDuration="900"

            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            />

 

// res/value/attrs.xml

<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CircularTickProgressView">
        <attr name="ctp_tickCount" format="integer" />
        <attr name="ctp_tickWidth" format="dimension" />
        <attr name="ctp_tickLength" format="dimension" />
        <attr name="ctp_innerRadiusRatio" format="float" /> <!-- 0~1 -->
        <attr name="ctp_activeColor" format="color" />
        <attr name="ctp_inactiveColor" format="color" />
        <attr name="ctp_animDuration" format="integer" />
    </declare-styleable>
</resources>
728x90
반응형

'Android' 카테고리의 다른 글

androis system UI ( Navigation Bar, Status Bar )  (0) 2025.12.09
SSL 소켓 연결  (0) 2025.11.07
Android View환경에 따른 XML 생성  (0) 2025.04.21
Android Context 비교  (0) 2025.04.04
BLE ) isConnectable  (0) 2025.03.13