# 玩玩Andoid的拖拽——实现一款万能遥控器

• 绘制手机

• 实现拖动

• 修正位置

## 1.绘制手机

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measure(widthMeasureSpec), measure(heightMeasureSpec));
// 手机高度为View高度减去上下间隔24dp
int phoneHeight = getMeasuredHeight() - dp2px(24);
// 手机内容区域高 ：手机高度 - 手机头尾（48dp）- 手机屏幕间距（5dp） * 2）
mPhoneContentHeight = phoneHeight - dp2px(58);
// 手机内容区域宽 ：手机内容区域高/ 7 * 4（手机内容区域为4：7）
mPhoneContentWidth = mPhoneContentHeight / HEIGHT_COUNT * WIDTH_COUNT;
// 手机宽度为手机内容区域宽 + 手机屏幕间距 * 2
mPhoneWidth = mPhoneContentWidth + dp2px(10);
// 绘制起始点
startX = (getMeasuredWidth() - mPhoneWidth) / 2;
}

1.绘制手机外壳

mPhonePaint.setColor(Color.parseColor(BORDER_COLOR));
mPhonePaint.setStyle(Paint.Style.STROKE);
mPhonePaint.setStrokeWidth(2);
int i = dp2px(12);
mRectF.left = startX;
mRectF.right = getMeasuredWidth() - startX;
mRectF.top = i;
mRectF.bottom = getMeasuredHeight() - i;
canvas.drawRoundRect(mRectF, i, i, mPhonePaint);

// 绘制手机上下两条线分隔线
canvas.drawLine(startX, i * 3, getMeasuredWidth() - startX, i * 3, mPhonePaint);
canvas.drawLine(startX, getMeasuredHeight() - i * 3, getMeasuredWidth() - startX, getMeasuredHeight() - i * 3, mPhonePaint);

2.绘制手机上的按钮及模块

// 绘制手机上方听筒、摄像头
mRectF.left = getMeasuredWidth() / 2 - dp2px(25);
mRectF.right = getMeasuredWidth() / 2 + dp2px(25);
mRectF.top = dp2px(22);
mRectF.bottom = dp2px(26);
canvas.drawRoundRect(mRectF, dp2px(2), dp2px(2), mPhonePaint);
canvas.drawCircle(getMeasuredWidth() / 2 - dp2px(40), i * 2, i / 3, mPhonePaint);
canvas.drawCircle(getMeasuredWidth() / 2 + dp2px(40), i * 2, i / 3, mPhonePaint);
// 绘制手机下方按键
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() - i * 2, i / 2, mPhonePaint);
canvas.drawRect(startX + mPhoneWidth / 5, getMeasuredHeight() - dp2px(29), startX + mPhoneWidth / 5 + dp2px(10), getMeasuredHeight() - dp2px(19), mPhonePaint);
mBackPath.moveTo(getMeasuredWidth() - startX - mPhoneWidth / 5, getMeasuredHeight() - dp2px(30));
mBackPath.lineTo(getMeasuredWidth() - startX - mPhoneWidth / 5 - dp2px(10), getMeasuredHeight() - dp2px(24));
mBackPath.lineTo(getMeasuredWidth() - startX - mPhoneWidth / 5, getMeasuredHeight() - dp2px(18));
mBackPath.close();
canvas.drawPath(mBackPath, mPhonePaint);

3.绘制网格（4 * 7的田字格）田字格外框为实线，里侧为虚线


DashPathEffect mDashPathEffect = new DashPathEffect(new float[] {10, 10}, 0);
// 手机屏幕间距5pd
int j = dp2px(5);
// 格子的宽高
int size = mPhoneContentHeight / HEIGHT_COUNT;

// 横线
for (int z = 0; z <= HEIGHT_COUNT; z++){
mPhonePaint.setPathEffect(null);
mPhonePaint.setColor(Color.parseColor(SOLID_COLOR));
mPhonePaint.setStrokeWidth(1);
// 实线
canvas.drawLine(startX + j, dp2px(41) + z * size,
getMeasuredWidth() - startX - j, dp2px(41) + z * size, mPhonePaint);
// 虚线
if (z != HEIGHT_COUNT){
mPhonePaint.setPathEffect(mDashPathEffect);
mPhonePaint.setColor(Color.parseColor(DASHED_COLOR));
canvas.drawLine(startX + j, dp2px(41) + z * size + size / 2,
getMeasuredWidth() - startX - j, dp2px(41) + z * size + size / 2, mPhonePaint);
}
}

// 竖线
for (int z = 0; z <= WIDTH_COUNT; z++){
mPhonePaint.setPathEffect(null);
mPhonePaint.setColor(Color.parseColor(SOLID_COLOR));
mPhonePaint.setStrokeWidth(1);
// 实线
canvas.drawLine(startX + j + z * size, dp2px(41),
startX + j + z * size, getMeasuredHeight() - dp2px(41), mPhonePaint);
// 虚线
if (z != WIDTH_COUNT){
mPhonePaint.setPathEffect(mDashPathEffect);
mPhonePaint.setColor(Color.parseColor(DASHED_COLOR));
canvas.drawLine(startX + j + z * size + size / 2, dp2px(41),
startX + j + z * size + size / 2, getMeasuredHeight() - dp2px(41), mPhonePaint);
}
}


## 2.实现拖动

public class DraggableButton extends AppCompatImageView{

private Paint mPaint;
private Rect mRect = new Rect();
// 文本按钮的文字
private String text;

public DraggableButton(Context context) {
this(context, null);
}

public DraggableButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public DraggableButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeWidth(1);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth(), height = getMeasuredHeight();
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}

@Override
protected void onDraw(Canvas canvas) {
int radius = getWidth() / 2;
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.parseColor("#cdcdcd"));
// 绘制圆形边框
// 按下时有背景变化
if (isPressed()){
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.parseColor("#20000000"));
}
// 有文字时的文字绘制
if (!TextUtils.isEmpty(text)){
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
mPaint.getTextBounds(text, 0, text.length(), mRect);

int textHeight = mRect.bottom - mRect.top;
int textWidth = mRect.right - mRect.left;
canvas.drawText(text, radius - textWidth / 2, radius + textHeight / 2, mPaint);
}

super.onDraw(canvas);
}

@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
// View的状态有发生改变的触发
invalidate();
}

public String getText() {
return text;
}

public void setText(String text) {
this.text = text;
}
}


### 1.开始拖动

v.startDrag(dragData,  // 要拖动的数据
null,      // 本地数据（不需要用到）
0);        // 标志位（目前未启用，设为0）


public class DraggableInfo implements Serializable{

/**
* 0 : 1 * 1 图片
* 1 : 1 * 1 文字
* 2 : 3 * 3 图片
* 3 : 1 * 2 图片
*/
private int type;
private String text;
private int pic;
private int id;

public DraggableInfo(String text, int pic, int id, int type) {
this.text = text;
this.pic = pic;
this.id = id;
this.type = type;
}

}

Intent intent = new Intent();
intent.putExtra("data", draggableInfo);
// 利用Intent来传递数据
ClipData dragData = ClipData.newIntent("value", intent);

View.DragShadowBuilder myShadow = new View.DragShadowBuilder(view);

public class MyDragShadowBuilder extends View.DragShadowBuilder {

/**
* 拖动的阴影图像
*/

private int width, height;

super(v);
// 将view转为Drawable
v.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(v.getDrawingCache(true));
v.destroyDrawingCache();
v.setDrawingCacheEnabled(false);
}

/**
* 用于设置拖动阴影的大小和触摸点位置
* @param size
* @param touch
*/
@Override
public void onProvideShadowMetrics(Point size, Point touch){

width = getView().getWidth();
height = getView().getHeight();

// 设置阴影大小
// 设置长宽值，通过size参数返回给系统。
size.set(width, height);

// 把触摸点的位置设为拖动阴影的中心
touch.set(width / 2, height / 2);
}

/**
* 绘制拖动阴影
* @param canvas
*/
@Override
// 在系统传入的Canvas上绘制Drawable
}
}


public static void startDrag(View view){
DraggableInfo tag = (DraggableInfo) view.getTag();
if (tag == null){
tag = new DraggableInfo("Text", 0, 0, 1);
}
Intent intent = new Intent();
intent.putExtra("data", tag);
ClipData dragData = ClipData.newIntent("value", intent);
// 震动反馈，不需要震动权限
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
}else{
}
}

### 2.响应拖动

// 拖拽有效区域
frameLayout = new FrameLayout(context);
frameLayout.setBackgroundColor(Color.parseColor(CONTENT_COLOR));
// 注册监听
frameLayout.setOnDragListener(this);
addView(frameLayout);

onLayout 中我们确定它的大小

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
frameLayout.layout(startX, dp2px(36), getMeasuredWidth() - startX, getMeasuredHeight() - dp2px(36));
}

@Override
public boolean onDrag(View v, DragEvent event) {
final int action = event.getAction();
switch(action) {
case DragEvent.ACTION_DRAG_STARTED:
// 判断是否是需要接收的数据
if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
Log.e(TAG, "开始拖动");
}else {
return false;
}
break;
case DragEvent.ACTION_DRAG_ENTERED:
Log.e(TAG, "进入区域");
break;
case DragEvent.ACTION_DRAG_EXITED:
Log.e(TAG, "移出区域");
break;
case DragEvent.ACTION_DRAG_ENDED:
Log.e(TAG, "停止拖动");
break;
case DragEvent.ACTION_DRAG_LOCATION:
Log.e(TAG, "停留在区域中");
break;
case DragEvent.ACTION_DROP:
Log.e(TAG, "释放拖动View");
break;
default:
return false;
}
return true;
}

## 3.修正位置

DraggableInfo data = (DraggableInfo) event.getClipData().getItemAt(0).getIntent().getSerializableExtra("data");

### 1.确定大小

    /**
* 计算控件位置
*/
private void compute(int type, Rect rect, DragEvent event){

int size = mPhoneContentWidth / WIDTH_COUNT - dp2px(10);
int x = (int) event.getX();
int y = (int) event.getY();

if (type == ONE_BY_ONE_PIC || type == ONE_BY_ONE_TEXT){
// 1 * 1 方格
rect.left = x - size / 2;
rect.top = y - size / 2;
rect.right = x + size / 2;
rect.bottom = y + size / 2;
}else if (type == THREE_BY_THREE_PIC){
// 3 * 3 方格
rect.left = x - size * 3 / 2;
rect.top = y - size * 3 / 2;
rect.right = x + size * 3 / 2;
rect.bottom = y + size * 3 / 2;
}else if (type == ONE_BY_TWO_PIC){
// 1 * 2 方格
rect.left = x - size / 2;
rect.top = y - size;
rect.right = x + size / 2;
rect.bottom = y + size;
}
}


### 2.修正位置

    /**
* 调整控件位置
*/
private void adjust(int type, Rect rect, DragEvent event){
// 最小单元格宽高
int size = mPhoneContentWidth / WIDTH_COUNT / 2;
// 手机屏幕间距
// 1 * 1方格宽高
int width = size * 2 - dp2px(10);

int offsetX = (rect.left - padding) % size;
if (offsetX < size / 2){
rect.left = rect.left + padding - offsetX;
}else {
rect.left = rect.left + padding - offsetX + size;
}

int offsetY = (rect.top - padding) % size;
if (offsetY < size / 2){
rect.top = rect.top + padding - offsetY;
}else {
rect.top = rect.top + padding - offsetY + size;
}

if (type == ONE_BY_ONE_PIC || type == ONE_BY_ONE_TEXT){
rect.right = rect.left + width;
rect.bottom = rect.top + width;

}else if (type == ONE_BY_TWO_PIC){
rect.right = rect.left + width;
rect.bottom = rect.top + width * 2;
}else if (type == THREE_BY_THREE_PIC){
rect.top = rect.top + padding * 2;
rect.left = rect.left + padding * 2;
rect.right = rect.left + width * 3;
rect.bottom = rect.top + width * 3;
}

//超出部分修正(超出部分)
if (rect.right > frameLayout.getWidth() || rect.bottom > frameLayout.getHeight()){

int currentX = (int) event.getX();
int currentY = (int) event.getY();

int centerX = frameLayout.getWidth() / 2;
int centerY = frameLayout.getHeight() / 2;

if (currentX <= centerX && currentY <= centerY){
//左上角区域

}else if (currentX >= centerX && currentY <= centerY){
//右上角区域
rect.left = rect.left - size;
rect.right = rect.right - size;
}else if (currentX <= centerX && currentY >= centerY){
//左下角区域
rect.top = rect.top - size;
rect.bottom = rect.bottom - size;
}else if (currentX >= centerX && currentY >= centerY){
//右下角区域
if (rect.right > frameLayout.getWidth()){
rect.left = rect.left - size;
rect.right = rect.right - size;
}
if (rect.bottom > frameLayout.getHeight()){
rect.top = rect.top - size;
rect.bottom = rect.bottom - size;
}
}
}
}


### 3.避免重叠

     /**
* 判断是否重叠
*/
private boolean isOverlap(Rect rect){
for (Rect mRect : mRectList){
if (!isEqual(mRect)){
if (isRectOverlap(mRect, rect)){
return true;
}
}
}
return false;
}

/**
* 判断两Rect是否重叠
*/
private boolean isRectOverlap(Rect oldRect, Rect newRect) {
return (oldRect.right > newRect.left &&
newRect.right  > oldRect.left &&
oldRect.bottom > newRect.top &&
newRect.bottom > oldRect.top);
}

/**
* 判断与拖拽的Rect是否相等
*/
private boolean isEqual(Rect rect) {
if (dragRect == null){
return false;
}

return (rect.left == dragRect.left &&
rect.right == dragRect.right &&
rect.top == dragRect.top &&
rect.bottom == dragRect.bottom);
}


    /**
* 是否在有效区域
*/
private boolean isEffectiveArea(Rect rect){
return rect.left >= 0 && rect.top >= 0 && rect.right >= 0 && rect.bottom >= 0 &&
rect.right <= frameLayout.getWidth() && rect.bottom <= frameLayout.getHeight();
}

// 计算
compute(data.getType(), rect, event);
// 调整
// 不重叠且在有效区域
if (isEffectiveArea(rect) && !isOverlap(rect)){
//添加
}

### 4.其他

 case DragEvent.ACTION_DRAG_LOCATION:
compute(info.getType(), mRect, event);
if (isEffectiveArea(mRect) && !isOverlap(mRect)){
}else {
}
try {
} catch (InterruptedException e) {
}
invalidate();
break;

onDraw中

    if (shadowRect != null){
int type = info.getType();
mPhonePaint.setStyle(Paint.Style.FILL);
mPhonePaint.setColor(Color.WHITE);

if (type == ONE_BY_ONE_TEXT){
//文字类型绘制
String text = info.getText();
mPhonePaint.setTextSize(width / 4);
mPhonePaint.getTextBounds(text, 0, text.length(), mTextRect);

int textHeight = mTextRect.bottom - mTextRect.top;
int textWidth = mTextRect.right - mTextRect.left;
canvas.drawText(text, shadowRect.left + width / 2 - textWidth / 2, shadowRect.top + width / 2 + textHeight / 2, mPhonePaint);
}else {
//图片类型绘制
if (type == ONE_BY_ONE_PIC){
// 1 * 1 方格
}else if (type == THREE_BY_THREE_PIC){
// 3 * 3 方格
}else if (type == ONE_BY_TWO_PIC){
}
}