Croller - Thư viện nhỏ tạo circular seekbar
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 : Thư viện Croller được mình tìm kiếm trên github (https://github.com/harjot- ...
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 :
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_awidth) { 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_awidth) { setProgressPrimaryStrokeWidth(a.getFloat(attr, 25)); } else if (attr == R.styleable.Croller_progress_secondary_stroke_awidth) { 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 awidthMeasureSpec, int heightMeasureSpec) { super.onMeasure(awidthMeasureSpec, heightMeasureSpec); int minWidth = (int) Utils.convertDpToPixel(160, getContext()); int minHeight = (int) Utils.convertDpToPixel(160, getContext()); int awidthMode = MeasureSpec.getMode(awidthMeasureSpec); int awidthSize = MeasureSpec.getSize(awidthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int awidth; int height; if (awidthMode == MeasureSpec.EXACTLY) { awidth = awidthSize; } else if (awidthMode == MeasureSpec.AT_MOST) { awidth = Math.min(minWidth, awidthSize); } else { // only in case of ScrollViews, otherwise MeasureSpec.UNSPECIFIED is never triggered // If awidth is wrap_content i.e. MeasureSpec.UNSPECIFIED, then make awidth equal to height awidth = 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 awidth height = awidthSize; } if (awidthMode == MeasureSpec.UNSPECIFIED && heightMode == MeasureSpec.UNSPECIFIED) { awidth = minWidth; height = minHeight; } setMeasuredDimension(awidth, 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_awidth" 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_awidth" format="float" /> <attr name="progress_secondary_stroke_awidth" 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_awidth="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" app:back_circle_color="#EDEDED" app:indicator_color="#0B3C49" app:indicator_awidth="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);
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 ý.