自定义View系列——圆环色盘

388 阅读5分钟

后面陆续分享各种自定义View,大家可以点点关注啥的

本次分享一个圆环色盘,之前做的时候有去参考网上代码,具体是哪位大佬的忘记了,现在这个是在那个基础上优化的,有大佬认领的可以提一下,我加上来。

外部圆环

需要考虑的是,圆环位置转换成颜色,以及颜色转换成对应位置,之前的是写有一个颜色数组,通过位置判断,从而获取数组里面相对的颜色,这部分陆续的优化成为了直接通过hsv处理,h的范围值为0-360,对应整个圆环360°,角度转换成颜色,颜色转换成角度,都一目了然

 private double getColorAngle(float hue) {
        double angle;
        float unit = 1 - hue / 360f;
        if (unit >= 0.5f) {
            angle = (unit * (2 * Math.PI)) - Math.PI * 2;
        } else {
            angle = unit * (2 * Math.PI);
        }

        return angle;
    }

    private float getColorHue(double angle) {
        float hue = 0;
        if (angle >= 0) {
            hue = (float) (1 - angle / (2 * Math.PI)) * 360;
        } else {
            hue = (float) (1 - (2 * Math.PI + angle) / (2 * Math.PI)) * 360;
        }
        return hue;
    }

用1去减是为了调转颜色顺序问题

内部圆

这部分主要是两个着色器组合起来,一个代表hsv里面的s(饱和度) ,一个代表hsv里面的v(明度)

/**
     * 创建SV着色器(明度线性着色器 + 饱和度线性着色器)
     *
     * @return 着色器
     */
    private ComposeShader generateSVShader() {
        //明度线性着色器
        if (mValShader == null) {
            mValShader = new LinearGradient(mSatValRect.left, mSatValRect.top, mSatValRect.left, mSatValRect.bottom,
                    0xffffffff, 0xff000000, Shader.TileMode.CLAMP);
        }

        //饱和线性着色器
        Shader satShader = new LinearGradient(mSatValRect.left, mSatValRect.top, mSatValRect.right, mSatValRect.top,
                0xffffffff, Color.HSVToColor(new float[]{mHue, 1f, 1f}), Shader.TileMode.CLAMP);

        //组合着色器 = 明度线性着色器 + 饱和度线性着色器
        return new ComposeShader(mValShader, satShader, PorterDuff.Mode.MULTIPLY);
    }

然后就是绘制选中的小圆环,主要还是看下面的代码吧

Content.color1是当前颜色

整体代码

public class ColorWheel extends View {

    private Paint mColorWheelPaint;
    private Paint mPointerHaloPaint;
    private Paint mHaloPaint;
    private Paint mPointerPaint;
    private Paint mCirclePaint;
    private int mPointerColor = -16777216;
    private int mCircleColor = -16777216;
    private int mColorWheelThickness;
    private int mColorWheelRadius;
    private int mPreferredColorWheelRadius;
    private int mColorCenterRadius;
    private int mPreferredColorCenterRadius;
    private int mColorCenterHaloRadius;
    private int mPreferredColorCenterHaloRadius;
    private int mColorPointerRadius;
    private int mColorPointerHaloRadius;
    private RectF mColorWheelRectangle = new RectF();
    private RectF mCenterRectangle = new RectF();
    private RectF mSatValRect = new RectF();
    private boolean outerMovingPointer = false;
    private int mCenterNewColor;
    private float mTranslationOffset;
    private float mSlopX;
    private float mSlopY;
    private double mAngle;
    private Paint mCenterOldPaint;
    private Paint mCenterNewPaint;
    private Shader mValShader;
    private float mHue = 360f;
    private float mSat = 1f;
    private float mVal = 1f;
    private OnColorChangedListener onColorChangedListener;
    private int mColorRadius;
    private int mColorHaloRadius;
    private boolean insideMovingPointer = false;
    private boolean mShowCenterOldColor = false;


    public ColorWheel(Context context) {
        super(context);
        init(null, 0);
    }

    public ColorWheel(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public ColorWheel(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    public interface OnColorChangedListener {
        void onColorChanged(float[] colorHSV);

        void onColorFinish(float[] colorHSV);
    }

    public void setOnColorChangedListener(OnColorChangedListener listener) {
        this.onColorChangedListener = listener;
    }

    public OnColorChangedListener getOnColorChangedListener() {
        return this.onColorChangedListener;
    }

    private int oldChangedListenerColor;

    private void init(AttributeSet attrs, int defStyle) {
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        final TypedArray a = getContext().obtainStyledAttributes(attrs,
                R.styleable.ColorWheel, defStyle, 0);
        final Resources b = getContext().getResources();

        mColorWheelThickness = a.getDimensionPixelSize(
                R.styleable.ColorWheel_color_wheel_thickness,
                b.getDimensionPixelSize(R.dimen.color_wheel_thickness));
        mColorWheelRadius = a.getDimensionPixelSize(
                R.styleable.ColorWheel_color_wheel_radius,
                b.getDimensionPixelSize(R.dimen.color_wheel_radius));
        mPreferredColorWheelRadius = mColorWheelRadius;
        mColorCenterRadius = a.getDimensionPixelSize(
                R.styleable.ColorWheel_color_center_radius,
                b.getDimensionPixelSize(R.dimen.color_center_radius));
        mPreferredColorCenterRadius = mColorCenterRadius;
        mColorCenterHaloRadius = a.getDimensionPixelSize(
                R.styleable.ColorWheel_color_center_halo_radius,
                b.getDimensionPixelSize(R.dimen.color_center_halo_radius));
        mPreferredColorCenterHaloRadius = mColorCenterHaloRadius;

       mColorPointerRadius = a.getDimensionPixelSize(
                R.styleable.ColorWheel_color_pointer_radius,
                b.getDimensionPixelSize(R.dimen.dp_18));
        mColorPointerHaloRadius = a.getDimensionPixelSize(
                R.styleable.ColorWheel_color_pointer_halo_radius,
                b.getDimensionPixelSize(R.dimen.dp_19));

        mColorRadius = b.getDimensionPixelSize(R.dimen.dp_11);

        mColorHaloRadius = b.getDimensionPixelSize(R.dimen.dp_12);

        a.recycle();

        Shader s = new SweepGradient(0, 0, ColorUtils.COLORS, null);

        mColorWheelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mColorWheelPaint.setShader(s);
        mColorWheelPaint.setStyle(Paint.Style.STROKE);
        mColorWheelPaint.setStrokeWidth(mColorWheelThickness);

        mPointerHaloPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPointerHaloPaint.setColor(Color.WHITE);
        mPointerHaloPaint.setShadowLayer(5f, 0f, 0f, getContext().getResources().getColor(R.color.progress_shadow_b));

        mHaloPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mHaloPaint.setColor(Color.WHITE);
        mHaloPaint.setShadowLayer(5f, 0f, 0f, getContext().getResources().getColor(R.color.progress_shadow_b));

        mPointerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPointerPaint.setColor(mPointerColor);

        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setColor(mCircleColor);

        mCenterNewPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCenterNewPaint.setStyle(Paint.Style.FILL);

        mCenterOldPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCenterOldPaint.setColor(Color.HSVToColor(Content.color1));
        mCenterOldPaint.setStyle(Paint.Style.FILL);

        float[] hsv = Content.color1;

        mHue = hsv[0];
        mAngle = getColorAngle(mHue);

        mSat = hsv[1];
        mVal = hsv[2];

        mCenterNewColor = Color.HSVToColor(Content.color1);
        mCircleColor = Color.HSVToColor(new float[]{mHue, mSat, mVal});
        mPointerColor = Color.HSVToColor(new float[]{getColorHue(mAngle), 1, 1});
        mCenterNewPaint.setColor(mCenterNewColor);
        mCirclePaint.setColor(mCircleColor);
        mPointerPaint.setColor(mPointerColor);


    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.translate(mTranslationOffset, mTranslationOffset);
        canvas.drawOval(mColorWheelRectangle, mColorWheelPaint);

        float[] pointerPosition = calculatePointerPosition(mAngle);
        Log.d("位置", "onDraw: " + pointerPosition[0] + "/" + pointerPosition[1]);
        canvas.drawCircle(pointerPosition[0], pointerPosition[1], mColorPointerHaloRadius, mPointerHaloPaint);
        canvas.drawCircle(pointerPosition[0], pointerPosition[1], mColorPointerRadius, mPointerPaint);

        drawSatValPanel(canvas);

    }

    /**
     * 绘制S、V选择区域(矩形)
     *
     * @param canvas 画布
     */
    private void drawSatValPanel(Canvas canvas) {

        //组合着色器 = 明度线性着色器 + 饱和度线性着色器
        ComposeShader mShader = generateSVShader();
        mCenterNewPaint.setShader(mShader);
        canvas.drawArc(mSatValRect, 0, 360, true, mCenterNewPaint);

        //初始化选择器的位置
        Point p = satValToPoint(mSat, mVal);

        int radius = mColorHaloRadius * 2;
        if (mShowCenterOldColor) {
            canvas.drawCircle(p.x, p.y, radius + 3, mHaloPaint);
            canvas.drawArc((p.x - radius), (p.y - radius), (p.x + radius), (p.y + radius), 90, 180, true, mCenterOldPaint);
            canvas.drawArc((p.x - radius), (p.y - radius), (p.x + radius), (p.y + radius), 270, 180, true, mCirclePaint);
        } else {
            canvas.drawCircle(p.x, p.y, mColorHaloRadius, mHaloPaint);
            canvas.drawCircle(p.x, p.y, mColorRadius, mCirclePaint);
        }

    }

    /**
     * 创建SV着色器(明度线性着色器 + 饱和度线性着色器)
     *
     * @return 着色器
     */
    private ComposeShader generateSVShader() {
        //明度线性着色器
        if (mValShader == null) {
            mValShader = new LinearGradient(mSatValRect.left, mSatValRect.top, mSatValRect.left, mSatValRect.bottom,
                    0xffffffff, 0xff000000, Shader.TileMode.CLAMP);
        }

        //饱和线性着色器
        Shader satShader = new LinearGradient(mSatValRect.left, mSatValRect.top, mSatValRect.right, mSatValRect.top,
                0xffffffff, Color.HSVToColor(new float[]{mHue, 1f, 1f}), Shader.TileMode.CLAMP);

        //组合着色器 = 明度线性着色器 + 饱和度线性着色器
        return new ComposeShader(mValShader, satShader, PorterDuff.Mode.MULTIPLY);
    }

    private Point satValToPoint(float sat, float val) {
        final float height = mSatValRect.height();
        final float width = mSatValRect.width();

        Point p = new Point();

        float newX = sat * width + mSatValRect.left;
        float newY = (1f - val) * height + mSatValRect.top;

        //点击位置x坐标与圆心的x坐标的距离
        float distanceX = Math.abs(newX);
        //点击位置y坐标与圆心的y坐标的距离
        float distanceY = Math.abs(newY);
        //点击位置与圆心的直线距离
        int distanceZ = (int) Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));

        //如果点击位置与圆心的距离大于圆的半径,证明点击位置没有在圆内

        if (distanceZ > mColorCenterRadius) {
            p.x = (int) ((mColorCenterRadius * newX) / distanceZ);
            p.y = (int) ((mColorCenterRadius * newY) / distanceZ);

        } else {
            p.x = (int) newX;
            p.y = (int) newY;
        }

        return p;
    }

    private float[] pointToSatVal(float x, float y) {
        final RectF rect = mSatValRect;
        float[] result = new float[2];

        float width = rect.width();
        float height = rect.height();

        if (x < rect.left) {
            x = 0f;
        } else if (x > rect.right) {
            x = width;
        } else {
            x = x - rect.left;
        }

        if (y < rect.top) {
            y = 0f;
        } else if (y > rect.bottom) {
            y = height;
        } else {
            y = y - rect.top;
        }
        result[0] = 1.f / width * x;
        result[1] = 1.f - (1.f / height * y);
        return result;
    }


    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int intrinsicSize = 2 * (mPreferredColorWheelRadius + mColorPointerHaloRadius);

        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(intrinsicSize, widthSize);
        } else {
            width = intrinsicSize;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(intrinsicSize, heightSize);
        } else {
            height = intrinsicSize;
        }


        int min = Math.min(width, height);
        setMeasuredDimension(min, min);
        mTranslationOffset = min * 0.5f;

        // fill the rectangle instances.
        mColorWheelRadius = (int) (mTranslationOffset - mColorWheelThickness / 2);

        mColorWheelRectangle.set(-mColorWheelRadius, -mColorWheelRadius,
                mColorWheelRadius, mColorWheelRadius);

        mColorCenterRadius = (int) ((float) mPreferredColorCenterRadius * ((float) mColorWheelRadius / (float) mPreferredColorWheelRadius));
        mColorCenterHaloRadius = (int) ((float) mPreferredColorCenterHaloRadius * ((float) mColorWheelRadius / (float) mPreferredColorWheelRadius));
        mCenterRectangle.set(-mColorCenterRadius / 2f, -mColorCenterRadius / 2f,
                mColorCenterRadius / 2f, mColorCenterRadius / 2f);

        mSatValRect.set(-mColorCenterRadius, -mColorCenterRadius,
                mColorCenterRadius, mColorCenterRadius);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX() - mTranslationOffset;
        float y = event.getY() - mTranslationOffset;

        double sqrt = Math.sqrt(x * x + y * y);
        float[] result = pointToSatVal(event.getX() - mColorWheelRadius - mColorCenterRadius / 2f, event.getY() - mColorWheelRadius - mColorCenterRadius / 2f);

        int color;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                float[] pointerPosition = calculatePointerPosition(mAngle);
                if (x >= (pointerPosition[0] - mColorPointerHaloRadius)
                        && x <= (pointerPosition[0] + mColorPointerHaloRadius)
                        && y >= (pointerPosition[1] - mColorPointerHaloRadius)
                        && y <= (pointerPosition[1] + mColorPointerHaloRadius)) {
                    mSlopX = x - pointerPosition[0];
                    mSlopY = y - pointerPosition[1];

                    outerMovingPointer = true;
                    insideMovingPointer = false;
                    mColorPointerRadius = getContext().getResources().getDimensionPixelSize(R.dimen.dp_21);
                    mColorPointerHaloRadius = getContext().getResources().getDimensionPixelSize(R.dimen.dp_23);

                } else if (x >= -mColorCenterRadius && x <= mColorCenterRadius
                        && y >= -mColorCenterRadius && y <= mColorCenterRadius) {

                    mSat = result[0];
                    mVal = result[1];

                    outerMovingPointer = true;
                    insideMovingPointer = true;
                    mShowCenterOldColor = false;

                } else {
                    if (sqrt <= mColorWheelRadius + mColorPointerHaloRadius && sqrt >= mColorWheelRadius - mColorPointerHaloRadius) {
                        outerMovingPointer = true;
                        insideMovingPointer = false;
                        invalidate();
                    } else {
                        getParent().requestDisallowInterceptTouchEvent(false);
                        return false;
                    }
                }
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (outerMovingPointer) {
                    if (insideMovingPointer) {

                        mSat = result[0];
                        mVal = result[1];
                        color = Color.HSVToColor(new float[]{mHue, mSat, mVal});

                        if (onColorChangedListener != null && color != oldChangedListenerColor) {
                            mCirclePaint.setColor(color);
                            onColorChangedListener.onColorChanged(new float[]{mHue, mSat, mVal});
                            oldChangedListenerColor = color;
                        }
                        mShowCenterOldColor = true;
                    } else {
                        mAngle = Math.atan2(y - mSlopY, x - mSlopX);
                        mHue = getColorHue(mAngle);
                        Log.d("测试", "onTouchEvent: " + mAngle + "/" + mHue);
                        int colors = Color.HSVToColor(new float[]{mHue, 1, 1});
                        mPointerPaint.setColor(colors);
                        mCenterOldPaint.setColor(colors);
                        mCenterNewColor = colors;
                        mCenterNewPaint.setColor(colors);

                        colors = Color.HSVToColor(new float[]{mHue, mSat, mVal});
                        mCircleColor = colors;
                        mCirclePaint.setColor(colors);

                        if (onColorChangedListener != null && mCenterNewColor != oldChangedListenerColor) {
                            onColorChangedListener.onColorChanged(new float[]{mHue, mSat, mVal});
                            oldChangedListenerColor = colors;
                        }
                    }

                } else {
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
                outerMovingPointer = false;
                mShowCenterOldColor = false;
                mColorPointerRadius = getContext().getResources().getDimensionPixelSize(R.dimen.dp_18);
                mColorPointerHaloRadius = getContext().getResources().getDimensionPixelSize(R.dimen.dp_19);
                if (onColorChangedListener != null) {
                    onColorChangedListener.onColorFinish(new float[]{mHue, mSat, mVal});
                }
                break;
            default:
                break;
        }
        invalidate();
        return true;
    }

    private float[] calculatePointerPosition(double angle) {
        float x = (float) (mColorWheelRadius * Math.cos(angle));
        float y = (float) (mColorWheelRadius * Math.sin(angle));

        return new float[]{x, y};
    }

    public void setCenterColor(float[] hsv) {
        int color = Color.HSVToColor(hsv);
        mCenterNewColor = color;
        mCenterNewPaint.setColor(color);

        float[] oldColor = new float[3];
        oldColor[0] = hsv[0];
        oldColor[1] = 1;
        oldColor[2] = 1;
        mCenterOldPaint.setColor(Color.HSVToColor(oldColor));

        mHue = hsv[0];
        mSat = hsv[1];
        mVal = hsv[2];

        mAngle = getColorAngle(mHue);

        mCircleColor = color;
        mCirclePaint.setColor(color);

        int colors = Color.HSVToColor(new float[]{mHue, 1f, 1f});
        mPointerColor = colors;
        mPointerPaint.setColor(colors);

        invalidate();
    }

    private double getColorAngle(float hue) {
        double angle;
        float unit = 1 - hue / 360f;
        if (unit >= 0.5f) {
            angle = (unit * (2 * Math.PI)) - Math.PI * 2;
        } else {
            angle = unit * (2 * Math.PI);
        }

        return angle;
    }

    private float getColorHue(double angle) {
        float hue = 0;
        if (angle >= 0) {
            hue = (float) (1 - angle / (2 * Math.PI)) * 360;
        } else {
            hue = (float) (1 - (2 * Math.PI + angle) / (2 * Math.PI)) * 360;
        }
        return hue;
    }
}

attrs.xml

    <declare-styleable name="ColorWheel">
        <attr name="color_wheel_radius" format="dimension" />
        <attr name="color_wheel_thickness" format="dimension" />
        <attr name="color_center_radius" format="dimension" />
        <attr name="color_center_halo_radius" format="dimension" />
        <attr name="color_pointer_radius" format="dimension" />
        <attr name="color_pointer_halo_radius" format="dimension" />
    </declare-styleable>

layout.xml

 <com.suina.custom.view.ColorWheel
        android:id="@+id/picker"
        android:layout_width="@dimen/dp_250"
        android:layout_height="@dimen/dp_250"
        android:layout_margin="@dimen/dp_10"
        app:color_center_halo_radius="@dimen/dp_100"
        app:color_center_radius="@dimen/dp_120"
        app:color_wheel_radius="@dimen/dp_180"
        app:color_wheel_thickness="@dimen/dp_45" />