Android Custom ProgressBar 만들기
[Android ProgressBar?]
[SurfaceView 기반의 ProgressBar]
우선 개발을 하다가 기존 안드로이드에서 제공하는 ProgressBar 클래스가 있는데 이거에 크나큰 문제점을 발견 하였다. 그 문제점인게 무엇이냐면 ProgressBar 가 문제 인게 아니라 Android View 자체의 문제점이라고 있고, Developer 사이트에 들어가면 ProgressBar 는 결국 View 로 상속되어 있는 녀석입니다.
그 말인 즉슨 해당 뷰가 빠르게 갱신해야 하는 요건이 들어오면 사용자가 원하는 만큼의 퍼포먼스를 못본다는 문제입니다.
그래서 Android 니까 이 문제는 해결을 못한다? 기본으로 제공하는 클래스의 한계점이 있어서 이 문제를 해결못한다는거는 말이 안되는 것이라 판단하여 SurfaceView 기반으로 ProgressBar를 만들어 보기로 했습니다.
우선 소스부터 공유 해드리겠습니다.
attrs.xml
<!-- CustomProgressView Attr [Start] -->
<declare-styleable name="CustomProgressView">
<attr name="type" format="enum"> <!-- ProgressView type horizontal or vertical -->
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<attr name="gradientRadius" format="dimension" /> <!-- defines gradient radius -->
<attr name="gradientStartColor" format="color" /> <!-- defines gradient start color -->
<attr name="gradientCenterColor" format="color" /> <!-- defines gradient center color -->
<attr name="gradientEndColor" format="color" /> <!-- defines gradient end color -->
<attr name="gradientLocation" format="float" /> <!-- defines gradient color location -->
<attr name="bgColor" format="color" /> <!-- Background Color -->
<attr name="max" format="integer" /> <!-- defines the maximum value -->
<attr name="min" format="integer" /> <!-- defines the minimum value -->
</declare-styleable>
<!-- CustomProgressView Attr [End] -->
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import jsieun.com.androidwork.R;
import jsieun.com.androidwork.utils.Logger;
/**
* SurfaceView 기반의 Progress View Class
* Created by hmju on 2019-01-27.
*/
public class CustomProgressView extends SurfaceView implements Callback {
private final Context mContext;
private final SurfaceHolder mHolder;
private final int DEFAULT_MIN = 0;
private final int DEFAULT_MAX = 100;
final int DEFAULT_COLOR = Color.BLACK;
final int ID_TYPE = R.styleable.CustomProgressView_type;
final int ID_MAX = R.styleable.CustomProgressView_max;
final int ID_MIN = R.styleable.CustomProgressView_min;
final int ID_BG_COLOR = R.styleable.CustomProgressView_bgColor;
final int ID_GRADIENT_RADIUS = R.styleable.CustomProgressView_gradientRadius;
final int ID_GRADIENT_START_COLOR = R.styleable.CustomProgressView_gradientStartColor;
final int ID_GRADIENT_CENTER_COLOR = R.styleable.CustomProgressView_gradientCenterColor;
final int ID_GRADIENT_END_COLOR = R.styleable.CustomProgressView_gradientEndColor;
final int ID_GRADIENT_LOCATION = R.styleable.CustomProgressView_gradientLocation;
// attribute value define [START]
private enum TYPE {
HORIZONTAL, VERTICAL
}
// 프로그래스 색상 및 라운딩 처리에 대한 정보 구조체.
protected class GradientInfo {
int radius;
int startColor;
int centerColor;
int endColor;
float location;
}
private GradientInfo mGradientInfo = new GradientInfo();
private TYPE mType = TYPE.HORIZONTAL;
private int mMin = DEFAULT_MIN;
private int mMax = DEFAULT_MAX;
private int mBgColor;
// attribute value define [END]
private ProgressThread mThread;
private int mCurrentProgress = 0;
// surface life cycle
// Type 은 2가지로 표현 한다. 더 필요한 경우 생명주기 타입 추가.
private enum LIFE {
CAN_DRAW, NOT_DRAW
}
private LIFE mCurrentLife = LIFE.NOT_DRAW;
public CustomProgressView(Context context) {
this(context, null);
}
public CustomProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
// default Setting
mContext = context;
mHolder = getHolder();
mHolder.addCallback(this);
initView(attrs);
}
/**
* init View
*
* @author hmju
*/
private void initView(AttributeSet attrs) {
// 기본 SurfaceView 투명화
setZOrderOnTop(true);
mHolder.setFormat(PixelFormat.TRANSPARENT);
// 속성값 셋팅.
if (attrs != null) {
TypedArray attrsArray = mContext.obtainStyledAttributes(attrs, R.styleable.CustomProgressView);
mBgColor = attrsArray.getColor(ID_BG_COLOR, DEFAULT_COLOR);
mType = TYPE.values()[attrsArray.getInt(ID_TYPE, 0)];
mMax = attrsArray.getInt(ID_MAX, DEFAULT_MAX);
mMin = attrsArray.getInt(ID_MIN, DEFAULT_MIN);
mGradientInfo.radius = attrsArray.getDimensionPixelOffset(ID_GRADIENT_RADIUS, 0);
mGradientInfo.startColor = attrsArray.getColor(ID_GRADIENT_START_COLOR, DEFAULT_COLOR);
mGradientInfo.centerColor = attrsArray.getColor(ID_GRADIENT_CENTER_COLOR, DEFAULT_COLOR);
mGradientInfo.endColor = attrsArray.getColor(ID_GRADIENT_END_COLOR, DEFAULT_COLOR);
mGradientInfo.location = attrsArray.getFloat(ID_GRADIENT_LOCATION, 0f);
// setProgress
mCurrentProgress = mMin;
attrsArray.recycle();
}
}
public CustomProgressView setType(TYPE type) {
mType = type;
return this;
}
public CustomProgressView setMin(int min) {
mMin = min;
mCurrentProgress = mMin;
return this;
}
public CustomProgressView setMax(int max) {
mMax = max;
return this;
}
public CustomProgressView setBgColor(int id) {
mBgColor = id;
return this;
}
public CustomProgressView setRadius(int radius) {
mGradientInfo.radius = radius;
return this;
}
public CustomProgressView setStartColor(int color) {
mGradientInfo.startColor = color;
return this;
}
public CustomProgressView setCenterColor(int color) {
mGradientInfo.centerColor = color;
return this;
}
public CustomProgressView setEndColor(int color) {
mGradientInfo.endColor = color;
return this;
}
public CustomProgressView setGradientLocation(float location) {
mGradientInfo.location = location;
return this;
}
/**
* 현재 프로그래스 진행 률 표시 하는 함수.
*
* @param progress
*/
public void setProgress(int progress) {
mCurrentProgress = progress;
if (mCurrentLife == LIFE.CAN_DRAW) {
mThread.draw();
}
}
/**
* 현재 프로그래스 진행률 get
*
* @return mCurrentProgress
* @author hmju
*/
public int getProgress() {
return mCurrentProgress;
}
/**
* 현재 프로그래스가 증가율을 표시하는 함수.
*
* @param diff
*/
public void incrementProgressBy(int diff) {
mCurrentProgress += diff;
if (mCurrentLife == LIFE.CAN_DRAW) {
mThread.draw();
}
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
if (mThread != null) {
mThread.closeThread();
mThread = null;
}
mCurrentLife = LIFE.CAN_DRAW;
mThread = new ProgressThread(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mCurrentLife = LIFE.NOT_DRAW;
mThread.closeThread();
}
/**
* ProgressThread Class
*
* @author hmju
*/
class ProgressThread {
ExecutorService thread = Executors.newFixedThreadPool(1);
Paint fgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 부드럽게 처리 하는 flag
Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 부드럽게 처리 하는 flag
// Paint txtPaint = new Paint();
Rect fgRect;
RectF bgRectF;
LinearGradient gradient;
int corner;
int width;
int height;
/**
* initialize Thread
*
* @author hmju
*/
ProgressThread(int tmpWidth, int tmpHeight) {
width = tmpWidth;
height = tmpHeight;
// txtPaint.setColor(Color.BLACK);
// txtPaint.setTextSize(30);
//initBackground & init Gradient & init Thread
initBackground();
initGradient();
initThread();
}
/**
* init BackGround
*
* @author hmju
*/
void initBackground() {
corner = mGradientInfo.radius;
bgRectF = new RectF(0, 0, width, height);
bgPaint.setColor(mBgColor);
}
/**
* init Gradient
* └ type 별로 분기 처리
*
* @author hmju
*/
void initGradient() {
int[] colors = new int[]{mGradientInfo.startColor, mGradientInfo.centerColor, mGradientInfo.endColor};
float[] locations = new float[]{0, mGradientInfo.location, 1};
// 라운딩 처리된 배경에 그라데이션되어 있는 foreground 를 입힌다.
fgPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
// horizontal
if (mType == TYPE.HORIZONTAL) {
fgRect = new Rect(0, 0, 0, height);
gradient = new LinearGradient(0, 0, 0, height, colors, locations, Shader.TileMode.CLAMP);
}
// vertical
else {
fgRect = new Rect(0, height, width, height);
gradient = new LinearGradient(0, 0, 0, height, colors, locations, Shader.TileMode.CLAMP);
}
// setGradient
fgPaint.setShader(gradient);
}
/**
* init Thread
*
* @author hmju
*/
void initThread() {
//set Thread interface
thread.execute(runDraw);
// setBgColor
draw();
}
/**
* Canvas Draw
*
* @author hmju
*/
Runnable runDraw = new Runnable() {
@Override
public void run() {
Canvas canvas = null;
canvas = mHolder.lockCanvas();
try {
synchronized (mHolder) {
// draw background
canvas.drawRoundRect(bgRectF, corner, corner, bgPaint);
// 초기값인 경우 아래 로직 패스
if (mCurrentProgress == 0) {
return;
}
// type horizontal
if (mType == TYPE.HORIZONTAL) {
float updateValue = (float) width / mMax * mCurrentProgress;
fgRect.right = (int) updateValue;
}
// type vertical
else {
float updateValue = (float) ((mMax - mCurrentProgress) * height) / mMax;
fgRect.top = (int) updateValue;
}
// draw foreground
canvas.drawRect(fgRect, fgPaint);
// canvas.drawText( mCurrentProgress +"%",width-100,100,txtPaint);
}
} catch (Exception ex) {
Logger.d("Thread Error " + ex.getMessage());
} finally {
if (canvas != null) {
mHolder.unlockCanvasAndPost(canvas);
}
}
}
};
/**
* runDraw
*
* @author hmju
*/
void draw() {
try {
runDraw.run();
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* call ShutDown Thread
*
* @author hmju
*/
void closeThread() {
try {
thread.shutdownNow();
} catch (Exception ex) {
Logger.d("TEST closeThread Error " + ex.getMessage());
}
}
}
}
CustomProgressView
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/size_30"
android:layout_centerInParent="true"
android:layout_margin="@dimen/size_20"
app:bgColor="@color/color_584E4E"
app:gradientCenterColor="@color/color_E27D00"
app:gradientEndColor="@color/color_00ff0f"
app:gradientLocation="0.3"
app:gradientRadius="@dimen/progress_view_radius"
app:gradientStartColor="@color/color_ee2d7a"
app:max="1000"
app:type="horizontal" />
중간 중간 이해하기 쉽도록 주석 처리를 하여 더이상 설명은 안하도록...(귀찮기도 하고..흠..) 만약에 궁금한게 있으시면 댓글 남겨주시길 바랍니다 :D
아무튼 최대한 사용하기 쉽게 코드를 짜보았습니다. 그리고 기존 ProgressBar 에서 사용하는 함수 이름이나, 속성 이름들을 같은 거로 셋팅할려고 했습니다.
만약 여기서 현재 몇 퍼센트까지 왔는지에 대해서 노출 하고 싶다면, runDraw 라는 Runnable 클래스안에
canvas.drawText 라고 있다. 이부분이 주석 처리 되어있는데 풀고, 이와 관련된 것에 주석을 풀면 됩니다.
한가지 더 설명하자면. 기본으로 제공하는 ProgressBar 에서 setProgress 라는 함수를 사용하여 현재 진행 상태를 표시하는데, 이 함수는 되도록 사용하지 않는 것을 추천 합니다.
incrementProgressBy 라는 함수를 사용하는 것을 적극 추천합니다. 이유는 setProgress 라는 함수는 말그대록 현재 진행률을 다시 셋팅해서 View 단에서 그리는 것이고, incrementProgressBy 는 현재 진행 상태에서 증가율을 누적해서 더한후 그리는 것입니다.
아무튼 제가 만든 CustomProgressView 에서도 incrementProgressBy 이 함수를 사용하는 것을 추천 드립니다.
그리고, 쓰레드나 핸들러 이런거 사용안하고 그냥 반복문에 'incrementProgressBy(int 증가율)' 이 함수를 때려 박으시면 됩니다. 내부적으로 안에서 다 처리 하기 때문에 불 필요한 핸들러 사용은 추천 드리지 않습니다. (오히려 더 느리게 작동될수 있습니다.)
감사합니다.