Croller - Thư viện nhỏ tạo circular seekbar
Bài đăng này đã không được cập nhật trong 3 năm
Giới thiệu
Trong khi lập trinhg Android App rất nhiều bạn gặp khó khăn trong việc tạo 1 circular seekbar , hôm này mình xin giới thiệu 1 thư viện nhỏ, khá tiện dụng và cũng dễ sử dụng . Dưới đây là hình ảnh demo thư viện :
Áp dụng
Thư viện Croller được mình tìm kiếm trên github (https://github.com/harjot-oberai/Croller) . Trong bài viết này mình chỉ trình bày cơ bản cách sử dụng và custom .
Tạo thư viện
- Cách 1 : Add trực tiếp vào gradle trong android studio như sau
dependencies {
compile 'com.sdsmdg.harjot:croller:1.0.5'
}
Vậy là xong cách này khá đơn giản và thông dụng .
- Cách 2 : Bạn có thể clone thư viện từ gihub về và tìm hiêu cơ chế , ta thấy qua các bước sau :
- Bước 1 : Tạo 1 Class Croller.java extend từ View. Đây là thành phần cơ bản của toàn bộ lib cũng như muốn custom lib ta đều thực hiện ở đây. Class khá dài nên mình tóm lại 1 số function ta nên tham khảo trong class
private void init() : khởi tạo view ban đầu
private void init() {
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(labelColor);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextSize(labelSize);
textPaint.setFakeBoldText(true);
textPaint.setTextAlign(Paint.Align.CENTER);
circlePaint = new Paint();
circlePaint.setAntiAlias(true);
circlePaint.setColor(progressSecondaryColor);
circlePaint.setStrokeWidth(progressSecondaryStrokeWidth);
circlePaint.setStyle(Paint.Style.FILL);
circlePaint2 = new Paint();
circlePaint2.setAntiAlias(true);
circlePaint2.setColor(progressPrimaryColor);
circlePaint2.setStrokeWidth(progressPrimaryStrokeWidth);
circlePaint2.setStyle(Paint.Style.FILL);
linePaint = new Paint();
linePaint.setAntiAlias(true);
linePaint.setColor(indicatorColor);
linePaint.setStrokeWidth(indicatorWidth);
oval = new RectF();
}
private void initXMLAttrs(Context context, AttributeSet attrs) : khởi tạo, đọc các tham số cấu hình
private void initXMLAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Croller);
final int N = a.getIndexCount();
for (int i = 0; i < N; ++i) {
int attr = a.getIndex(i);
if (attr == R.styleable.Croller_progress) {
setProgress(a.getInt(attr, 1));
} else if (attr == R.styleable.Croller_label) {
setLabel(a.getString(attr));
} else if (attr == R.styleable.Croller_back_circle_color) {
setBackCircleColor(a.getColor(attr, Color.parseColor("#222222")));
} else if (attr == R.styleable.Croller_main_circle_color) {
setMainCircleColor(a.getColor(attr, Color.parseColor("#000000")));
} else if (attr == R.styleable.Croller_indicator_color) {
setIndicatorColor(a.getColor(attr, Color.parseColor("#FFA036")));
} else if (attr == R.styleable.Croller_progress_primary_color) {
setProgressPrimaryColor(a.getColor(attr, Color.parseColor("#FFA036")));
} else if (attr == R.styleable.Croller_progress_secondary_color) {
setProgressSecondaryColor(a.getColor(attr, Color.parseColor("#111111")));
} else if (attr == R.styleable.Croller_label_size) {
setLabelSize(a.getInteger(attr, 40));
} else if (attr == R.styleable.Croller_label_color) {
setLabelColor(a.getColor(attr, Color.WHITE));
} else if (attr == R.styleable.Croller_indicator_width) {
setIndicatorWidth(a.getFloat(attr, 7));
} else if (attr == R.styleable.Croller_is_continuous) {
setIsContinuous(a.getBoolean(attr, false));
} else if (attr == R.styleable.Croller_progress_primary_circle_size) {
setProgressPrimaryCircleSize(a.getFloat(attr, -1));
} else if (attr == R.styleable.Croller_progress_secondary_circle_size) {
setProgressSecondaryCircleSize(a.getFloat(attr, -1));
} else if (attr == R.styleable.Croller_progress_primary_stroke_width) {
setProgressPrimaryStrokeWidth(a.getFloat(attr, 25));
} else if (attr == R.styleable.Croller_progress_secondary_stroke_width) {
setProgressSecondaryStrokeWidth(a.getFloat(attr, 10));
} else if (attr == R.styleable.Croller_sweep_angle) {
setSweepAngle(a.getInt(attr, -1));
} else if (attr == R.styleable.Croller_start_offset) {
setStartOffset(a.getInt(attr, 30));
} else if (attr == R.styleable.Croller_max) {
setMax(a.getInt(attr, 25));
} else if (attr == R.styleable.Croller_main_circle_radius) {
setMainCircleRadius(a.getFloat(attr, -1));
} else if (attr == R.styleable.Croller_back_circle_radius) {
setBackCircleRadius(a.getFloat(attr, -1));
} else if (attr == R.styleable.Croller_progress_radius) {
setProgressRadius(a.getFloat(attr, -1));
}
}
a.recycle();
}
Các hàm override xử lí :
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int minWidth = (int) Utils.convertDpToPixel(160, getContext());
int minHeight = (int) Utils.convertDpToPixel(160, getContext());
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(minWidth, widthSize);
} else {
// only in case of ScrollViews, otherwise MeasureSpec.UNSPECIFIED is never triggered
// If width is wrap_content i.e. MeasureSpec.UNSPECIFIED, then make width equal to height
width = heightSize;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(minHeight, heightSize);
} else {
// only in case of ScrollViews, otherwise MeasureSpec.UNSPECIFIED is never triggered
// If height is wrap_content i.e. MeasureSpec.UNSPECIFIED, then make height equal to width
height = widthSize;
}
if (widthMode == MeasureSpec.UNSPECIFIED && heightMode == MeasureSpec.UNSPECIFIED) {
width = minWidth;
height = minHeight;
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mListener != null)
mListener.onProgressChanged((int) (deg - 2));
midx = canvas.getWidth() / 2;
midy = canvas.getHeight() / 2;
if (!isContinuous) {
startOffset2 = startOffset - 15;
circlePaint.setColor(progressSecondaryColor);
circlePaint2.setColor(progressPrimaryColor);
linePaint.setStrokeWidth(indicatorWidth);
linePaint.setColor(indicatorColor);
textPaint.setColor(labelColor);
textPaint.setTextSize(labelSize);
int radius = (int) (Math.min(midx, midy) * ((float) 14.5 / 16));
if (sweepAngle == -1) {
sweepAngle = 360 - (2 * startOffset2);
}
if (mainCircleRadius == -1) {
mainCircleRadius = radius * ((float) 11 / 15);
}
if (backCircleRadius == -1) {
backCircleRadius = radius * ((float) 13 / 15);
}
if (progressRadius == -1) {
progressRadius = radius;
}
float x, y;
float deg2 = Math.max(3, deg);
float deg3 = Math.min(deg, max + 2);
for (int i = (int) (deg2); i < max + 3; i++) {
float tmp = ((float) startOffset2 / 360) + ((float) sweepAngle / 360) * (float) i / (max + 5);
x = midx + (float) (progressRadius * Math.sin(2 * Math.PI * (1.0 - tmp)));
y = midy + (float) (progressRadius * Math.cos(2 * Math.PI * (1.0 - tmp)));
circlePaint.setColor(progressSecondaryColor);
if (progressSecondaryCircleSize == -1)
canvas.drawCircle(x, y, ((float) radius / 30 * ((float) 20 / max) * ((float) sweepAngle / 270)), circlePaint);
else
canvas.drawCircle(x, y, progressSecondaryCircleSize, circlePaint);
}
for (int i = 3; i <= deg3; i++) {
float tmp = ((float) startOffset2 / 360) + ((float) sweepAngle / 360) * (float) i / (max + 5);
x = midx + (float) (progressRadius * Math.sin(2 * Math.PI * (1.0 - tmp)));
y = midy + (float) (progressRadius * Math.cos(2 * Math.PI * (1.0 - tmp)));
if (progressPrimaryCircleSize == -1)
canvas.drawCircle(x, y, (progressRadius / 15 * ((float) 20 / max) * ((float) sweepAngle / 270)), circlePaint2);
else
canvas.drawCircle(x, y, progressPrimaryCircleSize, circlePaint2);
}
float tmp2 = ((float) startOffset2 / 360) + ((float) sweepAngle / 360) * deg / (max + 5);
float x1 = midx + (float) (radius * ((float) 2 / 5) * Math.sin(2 * Math.PI * (1.0 - tmp2)));
float y1 = midy + (float) (radius * ((float) 2 / 5) * Math.cos(2 * Math.PI * (1.0 - tmp2)));
float x2 = midx + (float) (radius * ((float) 3 / 5) * Math.sin(2 * Math.PI * (1.0 - tmp2)));
float y2 = midy + (float) (radius * ((float) 3 / 5) * Math.cos(2 * Math.PI * (1.0 - tmp2)));
circlePaint.setColor(backCircleColor);
canvas.drawCircle(midx, midy, backCircleRadius, circlePaint);
circlePaint.setColor(mainCircleColor);
canvas.drawCircle(midx, midy, mainCircleRadius, circlePaint);
canvas.drawText(label, midx, midy + (float) (radius * 1.1), textPaint);
canvas.drawLine(x1, y1, x2, y2, linePaint);
} else {
int radius = (int) (Math.min(midx, midy) * ((float) 14.5 / 16));
if (sweepAngle == -1) {
sweepAngle = 360 - (2 * startOffset);
}
if (mainCircleRadius == -1) {
mainCircleRadius = radius * ((float) 11 / 15);
}
if (backCircleRadius == -1) {
backCircleRadius = radius * ((float) 13 / 15);
}
if (progressRadius == -1) {
progressRadius = radius;
}
circlePaint.setColor(progressSecondaryColor);
circlePaint.setStrokeWidth(progressSecondaryStrokeWidth);
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint2.setColor(progressPrimaryColor);
circlePaint2.setStrokeWidth(progressPrimaryStrokeWidth);
circlePaint2.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(indicatorWidth);
linePaint.setColor(indicatorColor);
textPaint.setColor(labelColor);
textPaint.setTextSize(labelSize);
float deg3 = Math.min(deg, max + 2);
oval.set(midx - progressRadius, midy - progressRadius, midx + progressRadius, midy + progressRadius);
canvas.drawArc(oval, (float) 90 + startOffset, (float) sweepAngle, false, circlePaint);
canvas.drawArc(oval, (float) 90 + startOffset, ((deg3 - 2) * ((float) sweepAngle / max)), false, circlePaint2);
float tmp2 = ((float) startOffset / 360) + (((float) sweepAngle / 360) * ((deg - 2) / (max)));
float x1 = midx + (float) (radius * ((float) 2 / 5) * Math.sin(2 * Math.PI * (1.0 - tmp2)));
float y1 = midy + (float) (radius * ((float) 2 / 5) * Math.cos(2 * Math.PI * (1.0 - tmp2)));
float x2 = midx + (float) (radius * ((float) 3 / 5) * Math.sin(2 * Math.PI * (1.0 - tmp2)));
float y2 = midy + (float) (radius * ((float) 3 / 5) * Math.cos(2 * Math.PI * (1.0 - tmp2)));
circlePaint.setStyle(Paint.Style.FILL);
circlePaint.setColor(backCircleColor);
canvas.drawCircle(midx, midy, backCircleRadius, circlePaint);
circlePaint.setColor(mainCircleColor);
canvas.drawCircle(midx, midy, mainCircleRadius, circlePaint);
canvas.drawText(label, midx, midy + (float) (radius * 1.1), textPaint);
canvas.drawLine(x1, y1, x2, y2, linePaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent e) {
if (Utils.getDistance(e.getX(), e.getY(), midx, midy) > Math.max(mainCircleRadius, Math.max(backCircleRadius, progressRadius))) {
return super.onTouchEvent(e);
}
if (e.getAction() == MotionEvent.ACTION_DOWN) {
float dx = e.getX() - midx;
float dy = e.getY() - midy;
downdeg = (float) ((Math.atan2(dy, dx) * 180) / Math.PI);
downdeg -= 90;
if (downdeg < 0) {
downdeg += 360;
}
downdeg = (float) Math.floor((downdeg / 360) * (max + 5));
return true;
}
if (e.getAction() == MotionEvent.ACTION_MOVE) {
float dx = e.getX() - midx;
float dy = e.getY() - midy;
currdeg = (float) ((Math.atan2(dy, dx) * 180) / Math.PI);
currdeg -= 90;
if (currdeg < 0) {
currdeg += 360;
}
currdeg = (float) Math.floor((currdeg / 360) * (max + 5));
if ((currdeg / (max + 4)) > 0.75f && ((downdeg - 0) / (max + 4)) < 0.25f) {
deg--;
if (deg < 3) {
deg = 3;
}
downdeg = currdeg;
} else if ((downdeg / (max + 4)) > 0.75f && ((currdeg - 0) / (max + 4)) < 0.25f) {
deg++;
if (deg > max + 2) {
deg = max + 2;
}
downdeg = currdeg;
} else {
deg += (currdeg - downdeg);
if (deg > max + 2) {
deg = max + 2;
}
if (deg < 3) {
deg = 3;
}
downdeg = currdeg;
}
invalidate();
return true;
}
if (e.getAction() == MotionEvent.ACTION_UP) {
return true;
}
return super.onTouchEvent(e);
}
Ngoài ra còn 1 số hàm get, set tham số từ code các bạn tham khảo thêm trong file Croller.java nhé.
- Bước 2 : trong /values : bạn tạo file attrs.xml (nếu chưa có), nếu có rồi bạn thêm các tham số sau để hiệu chỉnh khi create layout :
<declare-styleable name="Croller">
<attr name="progress" format="integer" />
<attr name="label" format="string" />
<attr name="back_circle_color" format="color" />
<attr name="main_circle_color" format="color" />
<attr name="indicator_color" format="color" />
<attr name="progress_primary_color" format="color" />
<attr name="progress_secondary_color" format="color" />
<attr name="label_size" format="integer" />
<attr name="label_color" format="color" />
<attr name="indicator_width" format="float" />
<attr name="is_continuous" format="boolean" />
<attr name="progress_primary_circle_size" format="float" />
<attr name="progress_secondary_circle_size" format="float" />
<attr name="progress_primary_stroke_width" format="float" />
<attr name="progress_secondary_stroke_width" format="float" />
<attr name="sweep_angle" format="integer" />
<attr name="start_offset" format="integer" />
<attr name="max" format="integer" />
<attr name="main_circle_radius" format="float" />
<attr name="back_circle_radius" format="float" />
<attr name="progress_radius" format="float" />
</declare-styleable>
Xong các bước trên các bạn đã setup xong thư viện và có thể sử dụng rồi.
Sử dụng thư viện :
Cách sử dụng thư viện rất đơn giản :
- Trong layout xml bạn sử dụng như sau :
<com.sdsmdg.harjot.crollerTest.Croller (hoặc link đến file .Croller nếu bạn tự edit)
android:id="@+id/croller"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
app:back_circle_color="#EDEDED"
app:indicator_color="#0B3C49"
app:indicator_width="10"
app:is_continuous="false"
app:label="Label"
app:label_color="#000000"
app:main_circle_color="#FFFFFF"
app:max="50"
app:progress_primary_color="#0B3C49"
app:progress_secondary_color="#EDEDED"
app:start_offset="45" />
- Sử dụng custom trong file java :
Croller croller;
croller = (Croller) findViewById(R.id.croller);
croller.setIndicatorWidth(10);
croller.setBackCircleColor(Color.parseColor("#EDEDED"));
croller.setMainCircleColor(Color.WHITE);
croller.setMax(50);
croller.setStartOffset(45);
croller.setIsContinuous(false);
croller.setLabelColor(Color.BLACK);
croller.setProgressPrimaryColor(Color.parseColor("#0B3C49"));
croller.setIndicatorColor(Color.parseColor("#0B3C49"));
croller.setProgressSecondaryColor(Color.parseColor("#EEEEEE"));
croller.setProgressRadius(380);
croller.setBackCircleRadius(300);
Kết luận
Mình vừa giới thiệu với các bạn 1 thư viện nhỏ về tạo circular seekbar trong android. Rất mong các bạn giúp đỡ và góp ý.
All rights reserved