Croller - Thư viện nhỏ tạo circular seekbar

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

  1. 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 .

  1. 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 :

  1. 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" />
  1. 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 ý.