+1

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


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí