Android弧形SeekBar的设计与实现

1.概述

现在很多相机都会有zoom的功能,就是可以通过一个选择器设置焦距,来实现将远处的事物放大以达到能看清楚的目的,实现zoom除了可以通过双指滑动屏幕放大缩小来调整焦距外,还可以通过一个选择器。本文中要讲的就是实现一个弧形的选择器,这个弧形的选择器会随着设置的不同而改变UI的显示。因为随着google camera2架构的普及,相机可以通过设置更多的拍照参数来获取需要的图像,比如:iso、曝光值、白平衡、快门时间等。这些参数都可以通过一个选择器arcseekbar设置下去,这时设计一个可以同时适应这不同情景下的选择器就显得很重要,因为没有必要每个模块设计一个选择器。那样太不优雅。本文就是开发一个弧形的seekbar,附带黏附效果,粘附效果就是当滑块滑的距离不足一半时,应该回到上一个刻度,如果超过一半的值,则需要自动滑到下一个刻度。可以根据设置的显示数组的大小改变UI样式。

2.效果展示

当设置的刻度比较多时,两个刻度间就不会有小点,而是每个刻度的点都是一样大的,防止seekbar看起来比较不协调。若是刻度比较小,则需要用小点填充,让界面看起来更和谐
Android弧形SeekBar的设计与实现_第1张图片

3.设计思路

设计的思路如下图所示:
其实这个弧形的seekBar就是取的圆上面的一段弧,将这段弧画出来然后在根据弧的圆心角算出每个刻度的坐标值,再绘制到屏幕上就可以啦。
Android弧形SeekBar的设计与实现_第2张图片

4.代码实现

因为弧形的seekBar比较特殊,无法用现有的控件组合实现,所以需要咱们自己去将这个控件绘制出来。我们继承自View,重写完几个构造方法后,就准备开始咱们的弧形seekBar的绘制:
(1)初始化参数,这里主要是初始化一些自定义属性,画笔,以及图标

   private void initParam(Context context, AttributeSet attrs) {

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ArcSeekBar);
        int leftPadding = typedArray.getDimensionPixelSize(R.styleable.ArcSeekBar_left_padding, -1);
        int rightPadding = typedArray.getDimensionPixelSize(R.styleable.ArcSeekBar_right_padding, -1);
        float bgAlpha = typedArray.getFloat(R.styleable.ArcSeekBar_arc_seek_bar_alpha, 0f);
        int indicatorColor = typedArray.getColor(R.styleable.ArcSeekBar_indicator_color, -1);
        int arcSeekBarBg = typedArray.getColor(R.styleable.ArcSeekBar_arc_seek_bar_background, -1);
        typedArray.recycle();

        mValueArray = getResources().getStringArray(R.array.shutter_list);//刻度的值列表
        init(mValueArray);//根据值数组初始化样式

        mIndicator = getResources().getDrawable(R.drawable.ic_manual_pointer);
        mResetIcon = getResources().getDrawable(R.drawable.ic_manual_auto_inactive);
        mAutoIcon = getResources().getDrawable(R.drawable.ic_manual_auto_location);


        mPadding = leftPadding + rightPadding;
        sweepAngle = START_ANGLE;
        mArcIndicatorPaint = new Paint();//画刻度的画笔
        mArcIndicatorPaint.setStyle(Paint.Style.FILL);
        mArcIndicatorPaint.setAntiAlias(true);
        mArcIndicatorPaint.setColor(indicatorColor);

        mPaintBg = new Paint();//画背景的画笔
        mPaintBg.setAntiAlias(true);
        mPaintBg.setStyle(Paint.Style.STROKE);
        mPaintBg.setColor(arcSeekBarBg);
        mPaintBg.setAlpha((int) (bgAlpha * ALPHA_255));
        mPaintBg.setStrokeWidth(100);

        mValuePaint = new Paint();//画值的画笔
        mValuePaint.setStyle(Paint.Style.STROKE);
        mValuePaint.setAntiAlias(true);
        mValuePaint.setColor(Color.GREEN);
        mValuePaint.setTextSize(120);
    }

这里咱们需要说一下根据值数组初始化样式,咱们的样式主要有两种,如
Android弧形SeekBar的设计与实现_第3张图片Android弧形SeekBar的设计与实现_第4张图片这样的效果就是通过在初始化的时候对传入的刻度值数组长度做判断,然后设置一个临界值,超过这个值就不绘制装饰,否则绘制装饰。
值数组:


<resources>
    <array name="shutter_list">
        <item>1/8000item>
        <item>1/6000item>
        <item>1/4000item>
        <item>1/3000item>
        <item>1/2000item>
        <item>1/1500item>
        <item>1/1000item>
        <item>1/500item>
        <item>1/250item>
        <item>1/125item>
    array>
resources>

根据值数组计算出绘制刻度之间的步长

 public void init(String[] valueArray){
        int length = valueArray.length;
        int dotCount = Math.round(SWEEP_ANGLE/length);
        mStep = SWEEP_ANGLE/((dotCount + 1)*length);
        mStepN = mStep * (dotCount+1);


        if (length > 30) {//刻度大于30,直接用seekbar的弧度除以长度的到步长 
            mStep = SWEEP_ANGLE/length;
            mStepN = mStep;//stepN是隔几个点的步长
        }


        Log.d(TAG,"zhongxj: mstep: " + mStep + ",radius: " + mRadius + ",mStepN: " + mStepN + ", dotCount: " + dotCount
        +" ,length: " + length);
    }

初始化完成后就可以开始在view的onDraw函数中开始画控件的样式了

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float radius = (float) (((getWidth() - mPadding) / INT_2) / Math.sin(Math.toRadians(ANGLE_90_DIV_START_ANGLE)));
        mRadius = radius;
        float cx1 = getWidth() >> 1;
        float cy1 = (float) (mRadius * Math.cos(Math.toRadians(ANGLE_90_DIV_START_ANGLE)) +
                ((getWidth() / INT_2) * Math.sin(Math.toRadians(ANGLE_90_DIV_START_ANGLE))));
        float cx = (float) (cx1 - mRadius * Math.cos(Math.toRadians(sweepAngle)));
        float cy = (float) (cy1 - mRadius * Math.sin(Math.toRadians(sweepAngle)));

        drawBackGround(canvas, radius);//画背景
        drawResetIcon(canvas, cx1, cy1, radius);//画重置的图标
        drawAutoIcon(canvas, cx1, cy1, radius);//画自动的图标,就是圈“A”那个图标
        drawArcGradition(canvas, cx1, cy1, radius);//画刻度
        drawIndicator(canvas, cx, cy);//画滑动的那个指示器图标
    }

控件的样式画完后,还需要处理点击事件

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                startY = event.getY();
                if (event.getX() > (END_ANGLE - mStepN)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                float distance = event.getX() - startX;
                sweepAngle = (float) (lastAngle + ((distance * ANGLE_180) / (Math.PI * mRadius)));//根据滑动的距离计算扫过的弧度,这个弧度可以用来算出当前指示器的坐标,然后就可以绘制指示器了
                Log.d(TAG,"zhongxj: distanch: " + distance + "sweepAngle: " + sweepAngle);
                //边界值控制
                if (sweepAngle > (END_ANGLE - mStepN)) {
                    sweepAngle = END_ANGLE - mStepN;
                }

                if (sweepAngle < START_ANGLE) {
                    sweepAngle = START_ANGLE;
                }
                int index = getIndex(sweepAngle);
                android.util.Log.d(TAG, "zhongxj: index====>: " + index);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:

                float cx1 = getWidth() >> 1;
                float cy1 = (float) (mRadius * Math.cos(Math.toRadians(ANGLE_90_DIV_START_ANGLE)) +
                        ((mRadius / INT_2) * Math.sin(Math.toRadians(ANGLE_90_DIV_START_ANGLE))));
                float cx = (float) (cx1 - (mRadius * Math.cos(Math.toRadians(END_ANGLE))));
                float cy = (float) (cy1 - (mRadius * Math.sin(Math.toRadians(END_ANGLE))));
//计算点击的区域
                if (startX >= cx && startX <= cx + 50
                        && startY >= cy && startY <= cy + 50) {
                    resetValue2Defalut();
                    return true;
                }
							//这个地方是做惯性滑动的,也就是说当我没指示器超过两个刻度间的1/2时,就自动滑动到第二个刻度,否则回弹回第一个刻度
                float angle = (float) Math.toRadians(sweepAngle - START_ANGLE);
                float step3Radius = (float) Math.toRadians(mStepN);
                int count = (int) (angle / step3Radius);
                float mod = angle % step3Radius;
                if (mod > step3Radius / INT_2) {
                    sweepAngle = START_ANGLE + (count + 1) * mStepN;
                } else {
                    sweepAngle = START_ANGLE + (count) * mStepN;
                }

                if (sweepAngle > (END_ANGLE - mStepN)) {
                    sweepAngle = END_ANGLE - mStepN;
                }

                if (sweepAngle < START_ANGLE) {
                    sweepAngle = START_ANGLE;
                }

                lastAngle = sweepAngle;
                startY = 0;
                startX = 0;
                invalidate();
                break;
        }

        return true;
    }

大致的弧形seekBar的实现就是这样的,其实很简单,就是更具几何原理将每个刻度的坐标值算出来绘制出来,当我们点击滑动屏幕的时候,可以根据我们在屏幕上的滑动距离得到对应的弧度,根据弧度计算出指示器的坐标值,再绘制出来就可以啦

4.代码demo地址

源码地址

你可能感兴趣的:(Android,自定义控件,android,java)