Android手摸手实现一个画板功能(一)——View的拖拽

一、概述 从之前项目中抽取出来的一个“画板”功能模块,就是可以在一个空白布局上,添加不同的元素,实现自由组合,暂时没想到啥好名字,姑且叫它“画板”吧。
??主要实现了View的拖拽、缩放、旋转、复制、View导出图片、文本编辑、磁力连接线、上一步和下一步状态备忘等功能。该项目主要涉及的知识点:View的事件分发、手势多点触控、View坐标系、备忘录设计模式等。
??由于该项目是为特定pad机型定制项目,未做其他机型兼容性处理,但是这并不影响本文对其原理的讲解,建议使用1200 x 1920平板模拟器或真机运行工程以获得最佳体验。
??无图言屌?上图:
效果图.gif 二、解析 2.1 侧边栏长按拖拽到画布 思路大概是酱紫:
??第一步,为侧边栏的每个Imageview设置OnLongClickListener、OnTouchListener;
??第二步,长按时生成一个新的Imageview对象,根据当前长按的Imageview的id,设置相应的ImageResource,并添加到画布中;
??第三步,为刚刚生成的Imageview对象设置OnTouchListener,在onTouch方法中,不断的更新ImageView的xy坐标,从而实现view的拖拽。
??看代码:
2.1.1 setOnLongClickListener()、setOnTouchListener()

ImageView allImageView = (ImageView) findViewById(R.id.allIcon); allImageView.setOnTouchListener(mTouchListener); allImageView.setOnLongClickListener(mLongClickListener); ImageView smileImageView = (ImageView) findViewById(R.id.smileIcon); smileImageView.setOnTouchListener(mTouchListener); smileImageView.setOnLongClickListener(mLongClickListener); ImageView jewelryImageView = (ImageView) findViewById(R.id.jewelryIcon); jewelryImageView.setOnTouchListener(mTouchListener); jewelryImageView.setOnLongClickListener(mLongClickListener); ImageView hotImageView = (ImageView) findViewById(R.id.hotIcon); hotImageView.setOnTouchListener(mTouchListener); hotImageView.setOnLongClickListener(mLongClickListener); ImageView lineImageView = (ImageView) findViewById(R.id.lineIcon); lineImageView.setOnTouchListener(mTouchListener); lineImageView.setOnLongClickListener(mLongClickListener); ImageView rect = (ImageView) findViewById(R.id.rectIcon); rect.setOnTouchListener(mTouchListener); rect.setOnLongClickListener(mLongClickListener);

2.1.2 长按事件处理:
private View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { final ImageView imageView = new ImageView(MainActivity.this); mCurrentImageView = imageView; ViewInfo viewInfo = new ViewInfo(v.getId(), 0); viewInfo.type = ViewInfo.TYPE_IMAGEVIEW; viewInfo.color = mCurrentColor; viewInfo.realId = ++mRealInfoId; imageView.setTag(viewInfo); imageView.setScaleType(ImageView.ScaleType.FIT_XY); setImageResource(imageView, true); int[] location = new int[2]; v.getLocationOnScreen(location); locationX = location[0]; locationY = location[1]; imageView.setX(locationX + 5); imageView.setY(locationY + 5); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(v.getWidth(), v.getHeight()); mRootView.addView(imageView, params); mViewList.add(imageView); imageView.setOnTouchListener(new MyTouchListener(imageView)); return true; } }; //根据不同的id设置不同的图片资源 public void setImageResource(ImageView v, boolean focus) { ViewInfo viewInfo = (ViewInfo) v.getTag(); switch (viewInfo.id) { case R.id.allIcon: realSetImageResource(v, viewInfo, focus, R.drawable.all_selected, R.drawable.ic_all_black, R.drawable.ic_all_green, R.drawable.ic_all_red); break; case R.id.smileIcon: realSetImageResource(v, viewInfo, focus, R.drawable.smile_selected, R.drawable.ic_smile_black, R.drawable.ic_smile_green, R.drawable.ic_smile_red); break; case R.id.jewelryIcon: realSetImageResource(v, viewInfo, focus, R.drawable.jewelry_selected, R.drawable.ic_jewelry_black, R.drawable.ic_jewelry_green, R.drawable.ic_jewelry_red); break; case R.id.hotIcon: realSetImageResource(v, viewInfo, focus, R.drawable.hot_selected, R.drawable.ic_hot_black, R.drawable.ic_hot_green, R.drawable.ic_hot_red); break; case R.id.lineIcon:if (mLineBitmap == null) {mLineBitmapBlack = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); mLineBitmapGreen = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); mLineBitmapRed = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Paint paint = new Paint(); paint.setColor(Color.BLACK); paint.setStrokeWidth(STROKE_WIDTH); Canvas canvas = new Canvas(mLineBitmapBlack); canvas.drawLine(0, 50, 100, 50, paint); paint.setColor(Color.RED); canvas = new Canvas(mLineBitmapRed); canvas.drawLine(0, 50, 100, 50, paint); paint.setColor(Color.GREEN); canvas = new Canvas(mLineBitmapGreen); canvas.drawLine(0, 50, 100, 50, paint); mLineBitmap = mLineBitmapBlack; if (viewInfo.color == 2) { mLineBitmap = mLineBitmapRed; } else if (mCurrentColor == 1) { mLineBitmap = mLineBitmapGreen; } } if (focus) { v.setImageResource(R.drawable.line_selected); } else { v.setImageBitmap(mLineBitmap); } break; case R.id.rectIcon: if (focus) { v.setBackgroundResource(R.drawable.border_shape_focus); } else { v.setBackgroundResource(R.drawable.border_shape); } break; default: break; } }

2.1.3 处理View的拖拽
private View.OnTouchListener mTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, final MotionEvent event) { int action = event.getAction(); if (mCurrentImageView == null && MotionEvent.ACTION_DOWN != action) { return false; }switch (action) { case MotionEvent.ACTION_DOWN: fdownX = event.getX(); fdownY = event.getY(); LogUtils.d("fdownX: " + fdownX + " ###fdownY: " + fdownY); getLineCoordinate(); break; case MotionEvent.ACTION_MOVE: float disX = event.getX() - fdownX - OFFSET; float disY = event.getY() - fdownY - OFFSET; LogUtils.d("disX: " + disX + " ###disY: " + disY + " ###getX: " + event.getX() + " ###getY: " + event.getY()); mCurrentImageView.setX(mCurrentImageView.getX() + disX); mCurrentImageView.setY(mCurrentImageView.getY() + disY); LogUtils.i("mCurrentImageView.getX(): " + mCurrentImageView.getX() + " ###mCurrentImageView.getY(): " + mCurrentImageView.getY()); fdownX = event.getX() - OFFSET; fdownY = event.getY() - OFFSET; LogUtils.v("fdownX: " + fdownX + " ###fdownY: " + fdownY); imageW = mCurrentImageView.getWidth(); imageH = mCurrentImageView.getHeight(); mCurrentImageView.setBackgroundResource(android.R.color.transparent); setImageResource(mCurrentImageView, true); return true; case MotionEvent.ACTION_UP: float x = mCurrentImageView.getX(); float y = mCurrentImageView.getY(); if (x <= 212) { cancelMoveView(x, y); return true; } else { if (x > 212 && x < 312) { x = 312; } else if (x > (mDisplayMetrics.widthPixels - 100)) { x = mDisplayMetrics.widthPixels - 100; }if (y <= 106) { y = 106; } else if (y > mDisplayMetrics.heightPixels - 100 - mStatusBarHeight) { y = mDisplayMetrics.heightPixels - 100 - mStatusBarHeight; } mCurrentImageView.setX(x - 312); mCurrentImageView.setY(y - 107); setImageResource(mCurrentImageView, false); mRootView.removeView(mCurrentImageView); mContent.addView(mCurrentImageView); createMemento(mCurrentImageView, false, true); if (((ViewInfo) mCurrentImageView.getTag()).id == R.id.rectIcon) { mCurrentImageView.setBackgroundResource(R.drawable.border_shape); } else { mCurrentImageView.setBackgroundResource(android.R.color.transparent); } } mCurrentImageView = null; break; case MotionEvent.ACTION_CANCEL: float x1 = mCurrentImageView.getX(); float y1 = mCurrentImageView.getY(); cancelMoveView(x1, y1); break; } return false; } };

处理拖拽的难点在于View新的x、y坐标计算,如果能够准确计算出View新的坐标,那么拖拽问题就可迎刃而解!
??首先拿到ACTION_DOWN事件按下的(x,y),对应fdownX、fdownY,其次在ACTION_MOVE时获取新的(x,y),通过新的(x,y)-旧的(x,y),就可以得到移动距离disX、disY,再将View的(x,y)坐标设置成:原来的坐标+移动距离,就可以实现View移动,从而实现拖拽;最后,别忘了,ACTION_MOVE事件是会持续触发的,所以每一个新的坐标相对于下一次移动坐标,都会变成旧的坐标,因此拖拽完View之后,还需要对手指的按下位置重新赋值。
??核心代码如下:
//OFFSET:由于体验问题,手指按在View上会遮挡住当前View,所以设置了一个偏移量来错开一定距离,该值可以不设置 case MotionEvent.ACTION_DOWN: fdownX = event.getX(); fdownY = event.getY(); LogUtils.d("fdownX: " + fdownX + " ###fdownY: " + fdownY); getLineCoordinate(); break; case MotionEvent.ACTION_MOVE: float disX = event.getX() - fdownX - OFFSET; float disY = event.getY() - fdownY - OFFSET; LogUtils.d("disX: " + disX + " ###disY: " + disY + " ###getX: " + event.getX() + " ###getY: " + event.getY()); mCurrentImageView.setX(mCurrentImageView.getX() + disX); mCurrentImageView.setY(mCurrentImageView.getY() + disY); LogUtils.i("mCurrentImageView.getX(): " + mCurrentImageView.getX() + " ###mCurrentImageView.getY(): " + mCurrentImageView.getY()); fdownX = event.getX() - OFFSET; fdownY = event.getY() - OFFSET; LogUtils.v("fdownX: " + fdownX + " ###fdownY: " + fdownY); imageW = mCurrentImageView.getWidth(); imageH = mCurrentImageView.getHeight(); mCurrentImageView.setBackgroundResource(android.R.color.transparent); setImageResource(mCurrentImageView, true); return true;

三、一句话总结 View的移动本质上就是x、y坐标值的变换,拖拽就是在ontouch()事件中,改变View的x、y值。
??由于本文的篇幅已经较长,为了能够让各位大佬获得更好的阅读体验(我要偷懒了_),笔者打算将其他几个知识点分到其他章节讲解,现提供完整工程,可以先睹为快,地址如下:
DrawLayoutSample
【Android手摸手实现一个画板功能(一)——View的拖拽】喜欢就star一下吧,fork也行,你开心就好,如果有啥问题欢迎在issue或者评论区讨论。

    推荐阅读