上次在微信小程序上看到了你画我猜的那个小程序,就寻思着自己也做一个这样的界面,话不多说,先看效果图
里面包含了3个主要的自定义view,分别是画板,宽度选择器,颜色选择器,这三个view互相关联
开始上代码吧
1.先看我的页面布局
2.宽度选择器的view
我们需要设置一个枚举状态来判断当前画板是处于什么时期,所以我们在Drawview中设置了一个枚举
public enum State{
PEN,
ERASER
}
这两个状态分别是画笔和橡皮擦。
我们还需要一个状态来判断当前选中的是哪个
private int mCurrentIndex = 0;
然后再ondraw事件中判断当前是什么画笔状态,当前按下的是第几个
for (int i = 0; i < rectList.size(); i++){
Rect rect = rectList.get(i);
float radius = widthArray[i];
switch (mCurrentState){
case PEN:
mChoosePaint.setColor(Color.WHITE);
if (mCurrentIndex == i){
canvas.drawCircle(rect.centerX(), rect.centerY(), radius + 10, mChoosePaint);
}
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, mPaint);
break;
case ERASER:
mChoosePaint.setColor(Color.BLACK);
if (mCurrentIndex == i){
canvas.drawCircle(rect.centerX(), rect.centerY(), radius + 10, mChoosePaint);
}
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, mWhitePaint);
break;
}
}
剩下的就是处理点击事件啦,下面是完整的代码
public class StrokeWidthChooseView extends View {
private Paint mPaint;
private Paint mChoosePaint;
private Paint mWhitePaint;
private int mWidth;
private int mHeight;
private int[] widthArray = new int[]{10, 20, 30, 40};
private List rectList;
private List regionList;
private int mPaintColor = Color.parseColor("#000000");
private int mCurrentIndex = 0;
private int downX, downY;
private DrawView drawView;
private DrawView.State mCurrentState = DrawView.State.PEN;
public StrokeWidthChooseView(Context context) {
this(context, null);
}
public StrokeWidthChooseView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
}
private void initPaint() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mChoosePaint = new Paint();
mChoosePaint.setAntiAlias(true);
mChoosePaint.setStyle(Paint.Style.FILL);
mWhitePaint = new Paint();
mWhitePaint.setAntiAlias(true);
mWhitePaint.setStyle(Paint.Style.FILL);
mWhitePaint.setColor(Color.WHITE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
rectList = new ArrayList<>();
regionList = new ArrayList<>();
int perWidth = mWidth / widthArray.length;
Rect rect;
Region region;
Rect chooseRect;
for (int i = 0; i < widthArray.length; i++){
rect = new Rect(perWidth * i + perWidth / 2 - widthArray[i],
mHeight / 2 - widthArray[i],
perWidth * i + perWidth / 2 + widthArray[i],
mHeight / 2 + widthArray[i]);
chooseRect = new Rect(perWidth * i + perWidth / 2 - widthArray[widthArray.length - 1],
mHeight / 2 - widthArray[widthArray.length - 1],
perWidth * i + perWidth / 2 + widthArray[widthArray.length - 1],
mHeight / 2 + widthArray[widthArray.length - 1]);
region = new Region(chooseRect);
rectList.add(rect);
regionList.add(region);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(mPaintColor);
for (int i = 0; i < rectList.size(); i++){
Rect rect = rectList.get(i);
float radius = widthArray[i];
switch (mCurrentState){
case PEN:
mChoosePaint.setColor(Color.WHITE);
if (mCurrentIndex == i){
canvas.drawCircle(rect.centerX(), rect.centerY(), radius + 10, mChoosePaint);
}
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, mPaint);
break;
case ERASER:
mChoosePaint.setColor(Color.BLACK);
if (mCurrentIndex == i){
canvas.drawCircle(rect.centerX(), rect.centerY(), radius + 10, mChoosePaint);
}
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, mWhitePaint);
break;
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
for (int i = 0; i < regionList.size(); i++){
Region region = regionList.get(i);
if (region.contains(x, y) && region.contains(downX, downY)){
mCurrentIndex = i;
switch (mCurrentState){
case PEN:
drawView.setmStrokeWidth(widthArray[i]);
break;
case ERASER:
drawView.setmEraserWidth(widthArray[i]);
break;
}
}
}
invalidate();
break;
}
return true;
}
public void setWidthArray(int[] widthArray) {
this.widthArray = widthArray;
}
public void setmPaintColor(int mPaintColor) {
this.mPaintColor = mPaintColor;
drawView.setmPaintColor(mPaintColor);
invalidate();
}
public void setDrawView(DrawView drawView) {
this.drawView = drawView;
drawView.setmStrokeWidth(widthArray[0]);
drawView.setmEraserWidth(widthArray[0]);
}
public void setmCurrentState(DrawView.State mCurrentState) {
this.mCurrentState = mCurrentState;
invalidate();
if (mCurrentState == DrawView.State.ERASER){
drawView.setmEraserWidth(widthArray[mCurrentIndex]);
}else{
drawView.setmStrokeWidth(widthArray[mCurrentIndex]);
}
}
}
3.颜色选择view
这个自定义view其实就是一个类似HorizontalScrollView的效果
我们需要一组颜色的数组,以及存放每个正方形的数组
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Rect rect;
for (int i = 0; i < colors.length; i++){
int centerX = 20 * i + radius * 2 * i + radius;
int centerY = h / 2;
rect = new Rect(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
rectList.add(rect);
}
}
然后再ondraw中将所有正方形绘制出来,当然,选中的颜色的正方形下方有一个长方形
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect rect;
Rect chooseRect;
switch (mCurrentState){
case ERASER:
for (int i = 0; i < rectList.size(); i++){
mPaint.setColor(colors[i]);
rect = rectList.get(i);
canvas.drawRect(rect, mPaint);
}
break;
case PEN:
for (int i = 0; i < rectList.size(); i++){
mPaint.setColor(colors[i]);
rect = rectList.get(i);
if (currentIndex == i){
chooseRect = new Rect(rect.left, rect.bottom - 10, rect.right, rect.bottom);
rect = new Rect(rect.left, rect.top - 30, rect.right, rect.bottom - 30);
canvas.drawRect(chooseRect, mPaint);
canvas.drawRect(rect, mPaint);
}else{
canvas.drawRect(rect, mPaint);
}
}
break;
}
}
最后在处理点击事件的时候,我发现我的手机在不管怎么按,都会有move事件,所有我做了一些判断,判断move的长度是否大于10
下面是完整代码
public class ColorHorizontalScrollView extends View{
private Paint mPaint;
private int radius = 50;
private List rectList;
private List regionList;
private float startX, startY;
private float dx, dy;
private StrokeWidthChooseView strokeWidthChooseView;
private int currentIndex = -1;
private DrawView.State mCurrentState = DrawView.State.PEN;
private onStateChnaged onStateChnagedListener;
private int[] colors = new int[]{
Color.parseColor("#fd039d"),
Color.parseColor("#ff4d3f"),
Color.parseColor("#fda602"),
Color.parseColor("#fff001"),
Color.parseColor("#000000"),
Color.parseColor("#00b181"),
Color.parseColor("#004bfe"),
Color.parseColor("#2c6281"),
Color.parseColor("#4e4c61"),
Color.parseColor("#edd93f"),
Color.parseColor("#666666"),
Color.parseColor("#66b502"),
Color.parseColor("#66fecb"),
Color.parseColor("#03c1fe"),
Color.parseColor("#966b59"),
Color.parseColor("#fda7a4"),
Color.parseColor("#f42728"),
Color.parseColor("#2c6281"),
Color.parseColor("#4e4c61"),
Color.parseColor("#edd93f"),
Color.parseColor("#666666"),
Color.parseColor("#c9c9c9"),
Color.parseColor("#8efbf6"),
Color.parseColor("#78d1b8"),
Color.parseColor("#bb18fd"),
Color.parseColor("#ffffcc"),
Color.parseColor("#fdcdb7"),
Color.parseColor("#993300"),
};
public ColorHorizontalScrollView(Context context) {
this(context, null);
}
public ColorHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.FILL);
rectList = new ArrayList<>();
regionList = new ArrayList<>();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Rect rect;
for (int i = 0; i < colors.length; i++){
int centerX = 20 * i + radius * 2 * i + radius;
int centerY = h / 2;
rect = new Rect(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
rectList.add(rect);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect rect;
Rect chooseRect;
switch (mCurrentState){
case ERASER:
for (int i = 0; i < rectList.size(); i++){
mPaint.setColor(colors[i]);
rect = rectList.get(i);
canvas.drawRect(rect, mPaint);
}
break;
case PEN:
for (int i = 0; i < rectList.size(); i++){
mPaint.setColor(colors[i]);
rect = rectList.get(i);
if (currentIndex == i){
chooseRect = new Rect(rect.left, rect.bottom - 10, rect.right, rect.bottom);
rect = new Rect(rect.left, rect.top - 30, rect.right, rect.bottom - 30);
canvas.drawRect(chooseRect, mPaint);
canvas.drawRect(rect, mPaint);
}else{
canvas.drawRect(rect, mPaint);
}
}
break;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d("TAG", "ACTION_DOWN");
startX = event.getX();
startY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
Log.d("TAG", "ACTION_MOVE");
dx = event.getX() - startX;
dy = event.getY() - startY;
if (Math.abs(dx) - Math.abs(dy) > ViewConfiguration.getTouchSlop()){
if (getScrollX() + (-dx) < 0 || getScrollX() + (-dx) > getWidth()){
return true;
}
this.scrollBy((int) -dx, 0);
startX = event.getX();
startY = event.getY();
}
break;
case MotionEvent.ACTION_UP:
if (Math.abs(dx) <= 10 && Math.abs(dy) <= 10){
for (int i = 0; i < rectList.size(); i++){
if (rectList.get(i).contains((int) startX + getScrollX(), (int) startY + getScrollY())){
Log.d("TAG", "有包含的");
Rect rect;
rectList.clear();
for (int j = 0; j < colors.length; j++){
int centerX = 20 * j + radius * 2 * j + radius;
int centerY = getHeight() / 2;
rect = new Rect(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
rectList.add(rect);
}
currentIndex = i;
onStateChnagedListener.onPen();
strokeWidthChooseView.setmPaintColor(colors[i]);
invalidate();
}
}
}
startX = 0;
startY = 0;
dx = 0;
dy = 0;
break;
}
return true;
}
public void setStrokeWidthChooseView(StrokeWidthChooseView strokeWidthChooseView) {
this.strokeWidthChooseView = strokeWidthChooseView;
}
public void setmCurrentState(DrawView.State mCurrentState) {
this.mCurrentState = mCurrentState;
invalidate();
}
public interface onStateChnaged{
void onPen();
}
public void setOnStateChnagedListener(onStateChnaged onStateChnagedListener) {
this.onStateChnagedListener = onStateChnagedListener;
}
}
4.最关键的画板的view
先说一下思路,绘制主要是通过path来完成的,橡皮擦则是利用PorterDuffXfermode来完成的,关于这个,大家就自行百度一下吧,我这里就不介绍了,返回的功能就相对比较复杂了,我没有在path类中找到合适的api,如果大家有的话,可以告诉我一声,谢谢啦。我现在的做法就是将每一步绘制的时候的画笔路径,状态,颜色和宽度都存储到一个stack中,每按一下返回,就弹出一个数据,然后利用剩下的path重绘界面,下面是完整代码
public class DrawView extends View{
private Paint drawPaint;
private Paint eraserPaint;
private Paint backPaint;
private Path mPath;
private float mx, my;
private Canvas mCanvas;
private static final float TOUCH_TOLERANCE = 4;
private Bitmap cacheBitmap;
public enum State{
PEN,
ERASER
}
private State mCurrentState = State.PEN;
private int mStrokeWidth;
private int mEraserWidth;
private int mPaintColor = Color.BLACK;
private Stack paths;
private Stack states;
private Stack colors;
private Stack widths;
public DrawView(Context context) {
this(context, null);
}
public DrawView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
drawPaint = new Paint();
drawPaint.setAntiAlias(true);
drawPaint.setStyle(Paint.Style.STROKE);
drawPaint.setStrokeCap(Paint.Cap.ROUND);
drawPaint.setColor(mPaintColor);
drawPaint.setStrokeJoin(Paint.Join.ROUND);
backPaint = new Paint();
backPaint.setColor(Color.WHITE);
backPaint.setAntiAlias(true);
eraserPaint = new Paint();
eraserPaint.setAlpha(0);
eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//eraserPaint.setColor(Color.RED);
eraserPaint.setAntiAlias(true);
eraserPaint.setDither(true);
eraserPaint.setStyle(Paint.Style.STROKE);
eraserPaint.setStrokeJoin(Paint.Join.ROUND);
mPath = new Path();
paths = new Stack<>();
states = new Stack<>();
colors = new Stack<>();
widths = new Stack<>();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
cacheBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(cacheBitmap);
mCanvas.drawColor(Color.TRANSPARENT);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(cacheBitmap, 0, 0, backPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
TouchDown(x, y);
break;
case MotionEvent.ACTION_MOVE:
TouchMove(x, y);
break;
case MotionEvent.ACTION_UP:
TouchUp();
break;
}
invalidate();
return true;
}
private void TouchUp() {
mPath.lineTo(mx, my);
drawPath();
Path path = new Path();
path.set(mPath);
paths.push(path);
if (mCurrentState == State.PEN){
states.push(0);
int color = drawPaint.getColor();
colors.push(color);
widths.push(drawPaint.getStrokeWidth());
}else{
states.push(1);
int color = eraserPaint.getColor();
colors.push(color);
widths.push(eraserPaint.getStrokeWidth());
}
}
private void TouchMove(float x, float y) {
float dx = Math.abs(x - mx);
float dy = Math.abs(y - my);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE){
mPath.quadTo(mx, my, (x + mx) / 2, (y + my) / 2);
mx = x;
my = y;
drawPath();
}
}
private void TouchDown(float x, float y) {
mPath.reset();
mPath.moveTo(x, y);
mx = x;
my = y;
drawPath();
}
public void setCurrentState(State state) {
this.mCurrentState = state;
invalidate();
}
public State getmCurrentState() {
return mCurrentState;
}
private void drawPath(){
switch (mCurrentState){
case PEN:
mCanvas.drawPath(mPath, drawPaint);
break;
case ERASER:
mCanvas.drawPath(mPath, eraserPaint);
break;
}
}
public void setmStrokeWidth(int mStrokeWidth) {
this.mStrokeWidth = mStrokeWidth;
drawPaint.setStrokeWidth(mStrokeWidth);
}
public void setmPaintColor(int mPaintColor) {
this.mPaintColor = mPaintColor;
drawPaint.setColor(mPaintColor);
}
public void clearPath(){
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mPath.reset();
paths.clear();
states.clear();
colors.clear();
widths.clear();
invalidate();
}
public void back(){
if (!paths.empty()){
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mPath.reset();
paths.pop();
states.pop();
colors.pop();
widths.pop();
Log.d("TAG", "size ==> " + paths.size());
Paint paint1 = new Paint();
Paint paint2 = new Paint();
paint1.setAntiAlias(true);
paint1.setStyle(Paint.Style.STROKE);
paint1.setStrokeCap(Paint.Cap.ROUND);
paint1.setStrokeJoin(Paint.Join.ROUND);
paint2.setAlpha(0);
paint2.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
paint2.setAntiAlias(true);
paint2.setDither(true);
paint2.setStyle(Paint.Style.STROKE);
paint2.setStrokeJoin(Paint.Join.ROUND);
for (int i = 0; i < paths.size(); i++){
Log.d("TAG", "empty --> " + paths.get(i).isEmpty());
if (states.get(i) == 0){
paint1.setColor(colors.get(i));
paint1.setStrokeWidth(widths.get(i));
mCanvas.drawPath(paths.get(i), paint1);
}else{
paint2.setColor(colors.get(i));
paint2.setStrokeWidth(widths.get(i));
mCanvas.drawPath(paths.get(i), paint2);
}
}
invalidate();
}
}
public void setmEraserWidth(int mEraserWidth) {
this.mEraserWidth = mEraserWidth;
eraserPaint.setStrokeWidth(mEraserWidth);
}
}
5.activity
在使用之前,我们需要将这三个view互相关联起来
strokeWidthChooseView.setDrawView(drawView);
colorHorizontalScrollView.setStrokeWidthChooseView(strokeWidthChooseView);
colorHorizontalScrollView.setOnStateChnagedListener(this);
下面是activity中的完整代码
public class MainActivity extends AppCompatActivity implements ColorHorizontalScrollView.onStateChnaged {
private DrawView drawView;
private ColorHorizontalScrollView colorHorizontalScrollView;
private StrokeWidthChooseView strokeWidthChooseView;
private ImageButton ibBack;
private ImageButton ibEraser;
private ImageButton ibClear;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initListener();
}
private void initView(){
drawView = findViewById(R.id.draw_view);
colorHorizontalScrollView = findViewById(R.id.colorHorizontalScrollView);
strokeWidthChooseView = findViewById(R.id.strokeWidthChooseView);
ibBack = findViewById(R.id.ib_back);
ibEraser = findViewById(R.id.ib_eraser);
ibClear = findViewById(R.id.ib_clear);
strokeWidthChooseView.setDrawView(drawView);
colorHorizontalScrollView.setStrokeWidthChooseView(strokeWidthChooseView);
colorHorizontalScrollView.setOnStateChnagedListener(this);
}
private void initListener(){
ibBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
drawView.back();
}
});
ibEraser.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (drawView.getmCurrentState() == DrawView.State.ERASER){
becomePen();
}else if (drawView.getmCurrentState() == DrawView.State.PEN){
becomeEraser();
}
}
});
ibClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("提示")
.setMessage("确定清空画布嘛")
.setNegativeButton("不行", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.setPositiveButton("好的", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
drawView.clearPath();
}
}).create().show();
}
});
}
@Override
public void onPen() {
becomePen();
}
private void becomePen(){
drawView.setCurrentState(DrawView.State.PEN);
strokeWidthChooseView.setmCurrentState(DrawView.State.PEN);
colorHorizontalScrollView.setmCurrentState(DrawView.State.PEN);
ibEraser.setImageResource(R.mipmap.rubbish);
}
private void becomeEraser(){
drawView.setCurrentState(DrawView.State.ERASER);
strokeWidthChooseView.setmCurrentState(DrawView.State.ERASER);
colorHorizontalScrollView.setmCurrentState(DrawView.State.ERASER);
ibEraser.setImageResource(R.mipmap.pen);
}
}
可能还会很多问题,请大家多多指正。。。
附上下载地址:https://download.csdn.net/download/wagg1993323/10290791