本文已参与「新人创作礼」活动,一起开启掘金创作之路。
实现效果如上图。分为两个部分,一个是画板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包