安卓自定义View-水平颜色选择器

107 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

在这里插入图片描述

实现效果如上图。分为两个部分,一个是画板View,因为添加了撤销上一步功能,所以每次绘制的path都是一个对象,这个对象包含这条path对应的画笔paint信息,然后保存在集合中;另一个是水平颜色选择器View。两个单独的View逻辑都很简单,下面直接贴出代码,包含注释;当然只是速写demo,有很多可以自行优化的地方

  • fragment及对应的xml

    import android.os.Bundle;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.SeekBar;
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import androidx.databinding.DataBindingUtil;
    import androidx.fragment.app.Fragment;
    import cc.catface.view.R;
    import cc.catface.view.databinding.AppViewFragmentBoardBinding;
    public class _BoardFragment extends Fragment {
        private AppViewFragmentBoardBinding binding;
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            binding = DataBindingUtil.inflate(inflater, R.layout.app_view_fragment_board, container, false, null);
            return binding.getRoot();
        }
        @Override
        public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
            binding.btReset.setOnClickListener(v -> {
                binding.bv.reset();
            });
            binding.sbWidth.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    binding.bv.setWidth(progress);
                }
                @Override
                public void onStartTrackingTouch(SeekBar seekBar) { }
                @Override
                public void onStopTrackingTouch(SeekBar seekBar) { }
            });
            binding.cbv.setCallback(new ColorBarView.Callback() {
                @Override
                public void onColor(int color) {
                    binding.bv.setColor(color);
                }
            });
        }
    }
    

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <cc.catface.view.board.BoardView
                android:id="@+id/bv"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
            <!--功能区-->
            <androidx.appcompat.widget.LinearLayoutCompat
                android:id="@+id/llFunction"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                app:layout_constraintBottom_toBottomOf="parent">
                <Button
                    android:id="@+id/btReset"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="撤销一步" />
            </androidx.appcompat.widget.LinearLayoutCompat>
            <SeekBar
                android:id="@+id/sbWidth"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:max="30"
                android:paddingStart="10dp"
                android:paddingEnd="10dp"
                app:layout_constraintBottom_toTopOf="@+id/llFunction" />
            <cc.catface.view.board.ColorBarView
                android:id="@+id/cbv"
                android:layout_width="match_parent"
                android:layout_height="20dp"
                android:paddingLeft="10dp"
                android:paddingRight="10dp"
                app:layout_constraintBottom_toTopOf="@+id/sbWidth" />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>
    
  • 画板View及每条path对应的bean

    import android.annotation.SuppressLint;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.graphics.Path;
    import android.os.Build;
    import android.util.ArrayMap;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    
    import androidx.annotation.Nullable;
    import androidx.annotation.RequiresApi;
    
    import java.util.Map;
    import java.util.function.BiConsumer;
    
    public class BoardView extends View {
    
        public BoardView(Context context) {
            this(context, null);
        }
    
        public BoardView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public BoardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        private float preX, preY;//用于绘制时缓存上次x,y坐标
    
        private Map<Integer, PathInfo> paths = new ArrayMap<>();//因为有撤销上一步功能,所以每次绘制的path都记录到容器中
        private int i = 0;//当前path的索引
        private Path path;//当前path
    
        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    path = new Path();
                    paths.put(++i, new PathInfo(getPaint(), path));
                    path.moveTo(event.getX(), event.getY());
                    preX = event.getX();
                    preY = event.getY();
                    return true;
                case MotionEvent.ACTION_MOVE:
                    float endX = (preX + event.getX()) / 2;
                    float endY = (preY + event.getY()) / 2;
                    paths.get(i).path.quadTo(preX, preY, endX, endY);
                    preX = event.getX();
                    preY = event.getY();
                    invalidate();
                    break;
            }
            return super.onTouchEvent(event);
        }
    
        @RequiresApi(api = Build.VERSION_CODES.N)
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (paint == null || path == null) return;
    
            //绘制容器内保存的path
            paths.forEach(new BiConsumer<Integer, PathInfo>() {
                @Override
                public void accept(Integer integer, PathInfo pathInfo) {
                    canvas.drawPath(pathInfo.path, pathInfo.paint);
                }
            });
        }
    
        /**
         * 设置方法
         */
        private Paint paint;
        private int color = Color.BLACK;//默认笔的颜色
        private float width = 1;//默认笔的宽度
    
        public void setColor(int color) {
            this.color = color;
        }
    
        public void setWidth(float width) {
            this.width = width;
        }
    
        private Paint getPaint() {
            paint = new Paint(Paint.ANTI_ALIAS_FLAG);
            paint.setStyle(Paint.Style.STROKE);
            paint.setColor(color);
            paint.setStrokeWidth(width);
            return paint;
        }
    
        public void reset() {
            if (i == 0) return;
            PathInfo pathInfo = paths.get(i);
            if (pathInfo == null) return;
            /*这里有个坑,如果是path1 = pathInfo.path,则path.reset后,path1的值也会变成空,使用path1 = new Path(pathInfo.path)*/
            //Path path = new Path(pathInfo.path);
            pathInfo.path.reset();
            --i;
            invalidate();
        }
    }
    

    import android.graphics.Paint;
    import android.graphics.Path;
    
    //每条path的信息
    public class PathInfo {
        public Paint paint;//当前path的颜色和宽度
        public Path path;
    
        public PathInfo(Paint paint, Path path) {
            this.paint = paint;
            this.path = path;
        }
    }
    
  • 水平颜色选择器View

    import android.animation.ArgbEvaluator;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.LinearGradient;
    import android.graphics.Paint;
    import android.graphics.RectF;
    import android.graphics.Shader;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    
    import androidx.annotation.Nullable;
    
    public class ColorBarView extends View {
    
        public ColorBarView(Context context) {
            this(context, null);
        }
    
        public ColorBarView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        private float width, height;//当前控件的宽高
        private float paddingLeft, paddingRight;//当前控件的左右padding
        private float realWidth;//计算padding后控件布局的实际宽即width减去paddingLeft减去paddingRight
        private RectF colorRectF;//水平颜色选择器绘制的矩形区域
    
        final private Paint scopePaint;//画水平矩形颜色区域的笔,笔的颜色需要根据水平位置进行平滑计算,这里使用Shader和LinearGradient
        final private Paint circlePaint;//画当前选中颜色标识的笔,笔的颜色需要动态计算,即算出水平颜色区域内在当前x位置的颜色值
        final private Paint circleOutPaint;//当前选中颜色标识外框
    
        //LinearGradient的颜色范围
        private int[] colors = {0XFFFFFFFF, 0XFFFF5C5C, 0XFFFFF600, 0XFF00FF31, 0XFF01CCFF, 0XFF3D38FF, 0XFFEE3DFF, 0XFFFF2C2C, 0XFF000000, 0XFF000000};//0xFFFFFFFF, 0xFFFF3030, 0xFFF4A460, 0xFFFFFF00, 0xFF66CD00, 0xFF458B00, 0xFF0000EE, 0xFF912CEE, 0xFF000000, 0XFF000000
        //颜色范围对应的位置坐标
        private float[] positions;
    
        public ColorBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            /*初始化三个画笔->可放到init()方法中*/
            scopePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            scopePaint.setStyle(Paint.Style.FILL);
            circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            scopePaint.setStyle(Paint.Style.FILL);
            circleOutPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            circleOutPaint.setStyle(Paint.Style.STROKE);
            circleOutPaint.setStrokeWidth(1);
            circleOutPaint.setColor(Color.parseColor("#FF333333"));
    
            post(() -> {
                width = getMeasuredWidth();
                height = getMeasuredHeight();
                paddingLeft = getPaddingLeft();
                paddingRight = getPaddingRight();
                realWidth = width - paddingLeft - paddingRight;
    
                colorRectF = new RectF(paddingLeft, height / 3.0f, width - paddingRight, height * 2.0f / 3);
    
                positions = new float[colors.length];
                for (int i = 0; i < positions.length; i++) {
                    positions[i] = (i + 0.0f) / (positions.length - 1);
                }
    
                scopePaint.setShader(new LinearGradient(0, 0, realWidth, 0, colors, positions, Shader.TileMode.CLAMP));
    
                invalidate();
            });
        }
    
        private float circleX;//根据event.getX()获取当前x坐标,即标识颜色的圆形的x坐标
        private int circleColor;//水平颜色选择器在当前circleX对应的颜色值
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_MOVE:
                    circleX = event.getX();
                    getColor(circleX);
                    invalidate();
            }
            return true;
        }
    
        /*获取当前位置的颜色值*/
        private void getColor(float x) {
            float scope = x / realWidth;
            float fraction;
    
            if (0 <= scope & scope < 1.0f / 9) {
                fraction = x * 9.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[0], colors[1]);
            } else if (1.0f / 9 <= scope & scope < 2.0f / 9) {
                fraction = (x - realWidth / 9.0f) * 9.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[1], colors[2]);
            } else if (2.0f / 9 <= scope & scope < 3.0f / 9) {
                fraction = (x - 2.0f * realWidth / 9) * 9.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[2], colors[3]);
            } else if (3.0f / 9 <= scope & scope < 4.0f / 9) {
                fraction = (x - 3.0f * realWidth / 9) * 9.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[3], colors[4]);
            } else if (4.0f / 9 <= scope & scope < 5.0f / 9) {
                fraction = (x - 4.0f * realWidth / 9) * 9.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[4], colors[5]);
            } else if (5.0f / 9 <= scope & scope < 6.0f / 9) {
                fraction = (x - 5.0f * realWidth / 9) * 9.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[5], colors[6]);
            } else if (6.0f / 9 <= scope & scope < 7.0f / 9) {
                fraction = (x - 6.0f * realWidth / 9) * 9.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[6], colors[7]);
            } else if (7.0f / 9 <= scope & scope < 8.0f / 9) {
                fraction = (x - 7.0f * realWidth / 9) * 8.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[7], colors[8]);
            } else if (8.0f / 9 <= scope & scope < 9.0f / 9) {
                fraction = (x - 8.0f * realWidth / 9) * 9.0f / realWidth;
                circleColor = (int) new ArgbEvaluator().evaluate(fraction, colors[8], colors[9]);
            }
    
            invalidate();
    
            if (callback != null) callback.onColor(circleColor);
        }
    
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (colorRectF == null) return;
    
            //颜色条
            canvas.drawRoundRect(colorRectF, 10, 10, scopePaint);
    
            //当前选中颜色的醒目圆框
            circlePaint.setColor(circleColor);
            if (circleX > realWidth) {//限制下x坐标范围
                circleX = realWidth;
            }
            canvas.drawCircle(circleX + paddingLeft, height / 2.0f, height / 3.0f, circlePaint);
            canvas.drawCircle(circleX + paddingLeft, height / 2.0f, height / 3.0f, circleOutPaint);
        }
    
    
        /**
         * 提供获取当前颜色值的接口
         */
        public interface Callback {
            void onColor(int color);
        }
    
        private Callback callback;
    
        public void setCallback(Callback callback) {
            this.callback = callback;
        }
    }
    

git参考:gitee.com/catface7/ca… => app_view模块的cc.catface.view.board包