安卓黑板功能实现

前两年的疫情爆发让线上教育特别火,很多公司选择线上办公,很多学校线上教学,不过后来受政策影响很大。这些机构学校依赖的线上教育软件功能强大又繁杂,功能不仅有基础的书写板书,还能添加图片,打字、播放视频、3D模型…作为安卓程序员要实现那些功能可能需要很多多的时间和精力。
废话不多说,那么本人这次分享一下这黑板的基本实现。包含的功能也不多,包含了手绘,输入文字,选择图层,删除,图片缩放,添加小黑板,缩放、拖拽、删除小黑板,混动整个黑板(大概一个屏幕的50倍高度),每个功能对应下面按钮介绍。如图


黑板功能图

实现思路实是这样的,每个元素是用图层来实现的,每一个元素生成一张图片,这样才能拖动图片到另一张图片上,因为使用的是图片,所以这其中会有稍许频繁的图片操作,并不是很友好,元素内容用hashmap来管理,图片拖动是用ViewDragHelper来管理。
既然是黑板。那么自定义view流程还是要稍微熟悉的,这些不概述了。

  • 创建图层
/**

* 创建图层并添加到BoardLayout

*/

private void createImageView() {

creatBitmap();

    try {

Bitmap newbm =null;

        if(board.action==BoardAction.ACTION_DRAW){

cacheCanvas.drawPath(path,paint);

            cacheCanvas.save();

            cacheCanvas.restore(); // 存储

            //裁剪出需要保存的bitmap坐标和高宽需要加上画笔的宽度。不然会裁切到线条.*2表示左右两边都需要加线条宽度

            newbm = Bitmap.createBitmap(cacheBitmap, Math.max((int) ((int)minX-paint.getStrokeWidth()),0),Math.max((int) ((int)minY-paint.getStrokeWidth()),0),

                    (int) ((int)(maxX-Math.max(minX,0))+paint.getStrokeWidth()*2),(int) ((int)(maxY-Math.max(minY,0))+paint.getStrokeWidth()*2));

        }else if(board.action==BoardAction.ACTION_DRAW_TEXT){

cacheCanvas.drawText(drawText,preX,preY,paint);

            cacheCanvas.save();

            cacheCanvas.restore(); // 存储

            //裁剪出需要保存的bitmap坐标和高宽需要加上画笔的宽度。不然会裁切到线条.*2表示左右两边都需要加线条宽度

            newbm = Bitmap.createBitmap(cacheBitmap, (int)minX,(int)minY,

                    (int) ((int) (maxX-minX)+paint.getStrokeWidth()*2),(int)(maxY-minY));

        }

//向当前view中添加图片

        if(minX==0&&minY==0&&maxX==0&&maxY==0){

}else{

DragScaleView imageView=new DragScaleView(getContext());

            imageView.setImageBitmap(newbm);

            int l=Math.max((int) ((int)minX-paint.getStrokeWidth()),0);

            int t=Math.max((int) ((int)minY-paint.getStrokeWidth()),0);

            ViewInfo info=board.setViewInfo(l,t,l+newbm.getWidth(),t+newbm.getHeight(),newbm.getWidth(),newbm.getHeight());

            if(board.action==BoardAction.ACTION_DRAW_TEXT){//输入的是文字需要记录文字内容和按下的xy坐标

                info.setContent(drawText);

                info.setPreX(preX);

                info.setPreY(preY);

                info.setType(ViewType.VIEW_TEXT);

                imageView.setOnClickListener(this);

            }else if(board.action==BoardAction.ACTION_DRAW){

info.setType(ViewType.VIEW_DRAW);

            }

board.addDragChildView(imageView,info);

            addView(imageView,smallBoardNum<=0?-1:getChildCount()-smallBoardNum);//如果没有小黑板就添加到小黑板的下面

            if(board.action==BoardAction.ACTION_DRAW_TEXT){//设置文字为选中状态

                board.chooseView(imageView);

            }

}

setMmPoint(0,0,0,0);

        cacheBitmap=null;

    }catch (Exception e){

Log.e("黑板创建图层错误",e.toString());

    }

}
/**

* 创建bitmap以便裁切

*/

private void creatBitmap() {

int view_height=board.boardHight+marginTop;//加上偏移量

    // 创建一个与该View相同大小的缓存区

    cacheBitmap = Bitmap.createBitmap(board.boardWidth, view_height,Bitmap.Config.ALPHA_8);

    cacheCanvas =new Canvas(cacheBitmap);

}
  • 按钮1
    选择功能主要是为了框选黑板上的元素内容,以便拖拽删除放大如图
    框选图

    ViewDragHelper捕获到某个元素后设置选中框,拖动时再将选中的元素一起移动,大致代码如下
dragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {

/**

    * 是否捕获childView:

    * 如果viewList包含child,那么捕获childView

    * 如果不包含child,就不捕获childView

*/

    @Override

    public boolean tryCaptureView(View child, int pointerId) {

if(board.action== BoardAction.ACTION_DRAW||board.action==BoardAction.ACTION_DRAW_TEXT||board.action==BoardAction.ACTION_MOVE){//绘制模式和文字模式

            return false;

        }else{

return board.containsView(child);

        }

}

@Override

    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {

super.onViewPositionChanged(changedView, left, top, dx, dy);

        for (Map.Entry entry :board.viewsMap.entrySet()) {

ViewInfo info=entry.getValue();

            if(info.isChoose()){

View view = entry.getKey();

                info.setLeft(info.getLeft()+dx);

                info.setTop(info.getTop()+dy);

                info.setRight(info.getRight()+dx);

                info.setBottom(info.getBottom()+dy);

                if(info.getType()== ViewType.VIEW_TEXT){//如果View是文字内容需要重置PreX和PreY

                    info.setPreX(info.getPreX()+dx);

                    info.setPreY(info.getPreY()+dy);

                }

board.viewsMap.put(view,info);

                if(view!=changedView){//非当前按下的view也需要移动

                    view.layout(info.getLeft(),info.getTop(), info.getRight(), info.getBottom());

                }

}

}

}

/**

    * 当捕获到child后的处理:

    * 获取child的监听

    */

    @Override

    public void onViewCaptured(View capturedChild, int activePointerId) {

super.onViewCaptured(capturedChild, activePointerId);

        jumpDraw=true;

        //遍历map,如果按下的view在选中状态的时候。不需要重新设置选中的view。该操作是在拖动view了

        for (Map.Entry entry :board.viewsMap.entrySet()) {

ViewInfo info=entry.getValue();

            if(!(entry.getKey()instanceof DragBoardView)){

DragScaleView view= (DragScaleView) entry.getKey();

                view.setCanTouch(false);//遍历禁止View可以操作

                if(!info.isChoose()&&capturedChild==entry.getKey()){

//得到一个被选中的View,该View可以实现缩放

                    board.setScaView(info.getType()==ViewType.VIEW_IMAGE?(DragScaleView) capturedChild:null);

                    board.setScaViewInfo(info);

                    board.chooseView(capturedChild);

                }

}

}

onDragDrop(true);

    }

/**

    * 当释放child后的处理:

    * 取消监听,不再处理

    */

    @Override

    public void onViewReleased(View releasedChild, float xvel, float yvel) {

super.onViewReleased(releasedChild, xvel, yvel);

        jumpDraw=false;

        onDragDrop(false);

    }

/**

    * 当前view的left

*/

    @Override

    public int clampViewPositionHorizontal(View child, int left, int dx) {

return left;

    }

/**

    * 到上边界的距离

    */

    @Override

    public int clampViewPositionVertical(View child, int top, int dy) {

return top;

    }

});

选中状态
/**

* 拖动选择框选中View并修改选中状态

* @param minX

* @param minY

* @param maxX

* @param maxY

*/

public void chooseView(float minX, float minY, float maxX, float maxY) {

int chooseViewNum=0;

    for (Map.Entry entry :viewsMap.entrySet()) {

if( !(entry.getKey()instanceof DragBoardView)){

ViewInfo info=entry.getValue();

            float zx =  Math.abs(minX+maxX-info.getLeft()-info.getRight()); //两个矩形重心在x轴上的距离的两倍

            float x = (Math.abs(minX-maxX)+Math.abs(info.getLeft()-info.getRight())); //两矩形在x方向的边长的和

            float zy =  Math.abs(minY+maxY-info.getTop()-info.getBottom()); //重心在y轴上距离的两倍

            float y =  (Math.abs(minY-maxY)+Math.abs(info.getTop()-info.getBottom())); //y方向边长的和

            DragScaleView view = (DragScaleView) entry.getKey();

            view.setCanTouch(false);

            if(action==BoardAction.ACTION_CHOOSE){

view.setClickable(false);//禁止点击事件

            }else if(action==BoardAction.ACTION_DRAW_TEXT&&info.getType()==ViewType.VIEW_TEXT){

view.setClickable(true);

            }

if(zx <= x && zy <= y){//view在选择框中

                view.setBackgroundResource(view_BG);

                info.setChoose(true);

                chooseViewNum++;

                if (chooseViewNum==1){//选中的view只能是一个的时候才能放大缩小,否则就清空

                    scaView=view;

                }

}else{

view.setBackgroundResource(0);

                info.setChoose(false);

            }

viewsMap.put(view,info);

        }

}

if(chooseViewNum==1){

setScaViewInfo(viewsMap.get(scaView));

    }else{

scaView=null;

    }

}
  • 按钮2
    书写功能类似老师在黑板上板书。
    板书功能
  1. 绘制的基本功能肯定是在onTouchEvent事件中处理逻辑
@Override

    public boolean onTouchEvent(MotionEvent event) {

dragHelper.processTouchEvent(event);

//        // 获取触摸事件的发生位置

        float x = event.getX();

        float y = event.getY();

        if(jumpDraw){

return true;

        }else{

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

//得到第一个点的xy坐标

                    setMmPoint(x,y,x,y);

                    preX = x;

                    preY = y;

                    if(board.action==BoardAction.ACTION_CHOOSE){

board.chooseRect.setRectVal((int)preX,(int)preY,(int)preX,(int)preY);

                    }else if(board.action==BoardAction.ACTION_MOVE){

}else{

if(drawType== BoardDrawType.DRAW_PATH){

path.moveTo(x, y); // 将绘图的起始点移到(x,y)坐标点的位置

                        }else if(drawType== BoardDrawType.DRAW_TEXT){//输入文字模式的时候点击黑板时回调展示输入的dialog

                            textChooseView=null;

                            onShowInputTextDialog("");

                        }else{

}

}

break;

                case MotionEvent.ACTION_MOVE:

if(board.action==BoardAction.ACTION_CHOOSE){

board.chooseRect.setRectVal((int) x, (int) y);

                    }else if(board.action==BoardAction.ACTION_MOVE) {

}else{

if(drawType== BoardDrawType.DRAW_PATH){

float dx = Math.abs(x -preX);

                            float dy = Math.abs(y -preY);

                            if (dx >=3 || dy >=3) {// 两点之间的距离大于等于3时,生成贝塞尔绘制曲线

                                path.quadTo(preX, preY, (x +preX) /2, (y +preY) /2);

                                preX = x;

                                preY = y;

                            }

}else{

}

}

if(x

minX=x;

                    }

if(x>maxX){

maxX=x;

                    }

if(y

minY=y;

                    }

if(y>maxY){

maxY=y;

                    }

break;

                case MotionEvent.ACTION_UP://拖动图片的时候抬起手势也会执行该方法。(jumpDraw先设置了false)

                    if(board.action==BoardAction.ACTION_CHOOSE){

if(minX==0&&minY==0&&maxX==0&&maxY==0){

}else{

board.chooseView(minX,minY,maxX,maxY);

                            board.chooseRect.rect=new Rect();

                            setMmPoint(0,0,0,0);

                        }

}else if(board.action==BoardAction.ACTION_MOVE) {

}else{

if(board.action==BoardAction.ACTION_DRAW){

createImageView();

                            path.reset();//清空所有path至原始状态。

                        }

}

break;

            }

}

invalidate();//view刷新

        return true;// 返回true表明处理方法已经处理该事件

    }
  1. onDraw绘制
@SuppressLint("DrawAllocation")

@Override

public void onDraw(Canvas canvas) {

if(board.action==BoardAction.ACTION_CHOOSE){

board.drawRec(canvas);

    }else if(board.action==BoardAction.ACTION_MOVE) {

board.drawRec(canvas);

    }else{

if(drawType== BoardDrawType.DRAW_PATH){

board.drawPath(canvas);

        }else if(drawType== BoardDrawType.DRAW_TEXT){

if(drawText.isEmpty()){

return;

            }

canvas.drawText(drawText,preX,preY,paint);

            Rect rect =new Rect();

            paint.getTextBounds(drawText, 0, drawText.length(), rect);

            int w = rect.width();

            int h = rect.height();

            Paint.FontMetrics fontMetrics=paint.getFontMetrics();

            minX=preX;

            maxX=minX+w;

            maxY=preY+fontMetrics.descent;

            minY=maxY+fontMetrics.top;

            createImageView();

            drawText="";

        }

}

}
  • 按钮3
    文字功能是直接打字在黑板上,获取到点击事件的坐标点后。弹出输入框输
    输入文字
  1. 输入框代码
public class InputTextMsgDialogextends AppCompatDialog {

private ContextmContext;

    private InputMethodManagerimm;

    public EditTextmessageTextView;

    private TextViewconfirmBtn;

    private RelativeLayoutrlDlg;

    private int mLastDiff =0;

    private TextViewtvNumber;

    private TextViewtvSend;

    private int maxNumber =200;

    public interface OnTextSendListener {

void onTextChange(String msg);

        void onTextSend(String msg);

    }

private OnTextSendListenermOnTextSendListener;

    public InputTextMsgDialog(@NonNull Context context, int theme) {

super(context, theme);

        this.mContext = context;

        this.getWindow().setWindowAnimations(R.style.class_main_menu_animstyle);

        init();

        setLayout();

    }

/**

    * 最大输入字数  默认200

*/

    @SuppressLint("SetTextI18n")

public void setMaxNumber(int maxNumber) {

this.maxNumber = maxNumber;

        tvNumber.setText("0/" + maxNumber);

    }

/**

    * 设置输入提示文字

    */

    public void setHint(String text) {

messageTextView.setHint(text);

    }

/**

    * 设置按钮的文字  默认为:发送

    */

    public void setBtnText(String text) {

confirmBtn.setText(text);

    }

private void init() {

setContentView(R.layout.class_edit_pop);

        messageTextView = (EditText) findViewById(R.id.edit_content);

        tvSend = (TextView) findViewById(R.id.chat_send_btn);

        final LinearLayout rldlgview = (LinearLayout) findViewById(R.id.rl_inputdlg_view);

        TextView other=(TextView) findViewById(R.id.other_view);

        imm = (InputMethodManager)mContext.getSystemService(Context.INPUT_METHOD_SERVICE);

        this.setOnDismissListener(new OnDismissListener() {

@Override

            public void onDismiss(DialogInterface dialog) {

hideKeyboard(messageTextView.getWindowToken(),mContext);

            }

});

        other.setOnClickListener(new View.OnClickListener() {

@Override

            public void onClick(View v) {

dismiss();

            }

});

        messageTextView.addTextChangedListener(new TextWatcher() {

@Override

            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override

            public void onTextChanged(CharSequence s, int start, int before, int count) {

}

@Override

            public void afterTextChanged(Editable s) {

String msg =messageTextView.getText().toString().trim();

                if (msg.length() >maxNumber) {

Toast.makeText(mContext, "超过最大字数限制", Toast.LENGTH_LONG).show();

return;

                }

if(mOnTextSendListener!=null){

mOnTextSendListener.onTextChange(msg);

                }

}

});

        tvSend.setOnClickListener(new View.OnClickListener() {

@Override

            public void onClick(View view) {

String msg =messageTextView.getText().toString().trim();

                if (msg.length() >maxNumber) {

Toast.makeText(mContext, "超过最大字数限制", Toast.LENGTH_LONG).show();

return;

                }

if (!TextUtils.isEmpty(msg)) {

if(mOnTextSendListener!=null){

mOnTextSendListener.onTextSend(msg);

                    }

//                    imm.showSoftInput(messageTextView, InputMethodManager.SHOW_FORCED);

//                    imm.hideSoftInputFromWindow(messageTextView.getWindowToken(), 0);

                    messageTextView.setText("");

                    dismiss();

                }else {

//                    Toast.makeText(mContext, "请输入文字", Toast.LENGTH_LONG).show();

//                    return;

                    dismiss();

                }

messageTextView.setText(null);

            }

});

        messageTextView.setOnEditorActionListener(new TextView.OnEditorActionListener() {

@Override

            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {

switch (actionId) {

case KeyEvent.KEYCODE_ENDCALL:

case KeyEvent.KEYCODE_ENTER:

if (messageTextView.getText().length() >maxNumber) {

Toast.makeText(mContext, "超过最大字数限制", Toast.LENGTH_LONG).show();

return true;

                        }

if (messageTextView.getText().length() >0) {

//                            imm.hideSoftInputFromWindow(messageTextView.getWindowToken(), 0);

//                            dismiss();

                            if(mOnTextSendListener!=null){

mOnTextSendListener.onTextSend(messageTextView.getText().toString());

                            }

}else {

//                            Toast.makeText(mContext, "请输入文字", Toast.LENGTH_LONG).show();

                        }

dismiss();

                        messageTextView.setText(null);

return true;

                    case KeyEvent.KEYCODE_BACK:

dismiss();

return false;

                    default:

return false;

                }

}

});

        messageTextView.setOnKeyListener(new View.OnKeyListener() {

@Override

            public boolean onKey(View view, int i, KeyEvent keyEvent) {

Log.d("My test", "onKey " + keyEvent.getCharacters());

return false;

            }

});

        rlDlg = findViewById(R.id.rl_outside_view);

        rlDlg.setOnClickListener(new View.OnClickListener() {

@Override

            public void onClick(View v) {

if (v.getId() != R.id.rl_inputdlg_view)

dismiss();

            }

});

        rldlgview.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {

@Override

            public void onLayoutChange(View view, int i, int i1, int i2, int i3, int i4, int i5, int i6, int i7) {

Rect r =new Rect();

                //获取当前界面可视部分

                getWindow().getDecorView().getWindowVisibleDisplayFrame(r);

                //获取屏幕的高度

                int screenHeight = getWindow().getDecorView().getRootView().getHeight();

                //此处就是用来获取键盘的高度的, 在键盘没有弹出的时候 此高度为0 键盘弹出的时候为一个正数

                int heightDifference = screenHeight - r.bottom;

                if (heightDifference <=0 &&mLastDiff >0) {

dismiss();

                }

mLastDiff = heightDifference;

            }

});

        rldlgview.setOnClickListener(new View.OnClickListener() {

@Override

            public void onClick(View v) {

//                imm.hideSoftInputFromWindow(messageTextView.getWindowToken(), 0);

                dismiss();

            }

});

        setOnKeyListener(new OnKeyListener() {

@Override

            public boolean onKey(DialogInterface dialogInterface, int keyCode, KeyEvent keyEvent) {

if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.getRepeatCount() ==0)

dismiss();

return false;

            }

});

    }

private void setLayout() {

getWindow().setGravity(Gravity.BOTTOM);

        WindowManager m = getWindow().getWindowManager();

        Display d = m.getDefaultDisplay();

        WindowManager.LayoutParams p = getWindow().getAttributes();

        p.width = WindowManager.LayoutParams.MATCH_PARENT;

        p.height = WindowManager.LayoutParams.MATCH_PARENT;

        getWindow().setAttributes(p);

        setCancelable(true);

        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);

    }

public void setmOnTextSendListener(OnTextSendListener onTextSendListener) {

this.mOnTextSendListener = onTextSendListener;

    }

@Override

    public void dismiss() {

imm.hideSoftInputFromWindow(messageTextView.getWindowToken(), 0);

        super.dismiss();

        //dismiss之前重置mLastDiff值避免下次无法打开

        mLastDiff =0;

    }

@Override

    public void show() {

super.show();

        openKeyboard(messageTextView,mContext);

        //延时加载dialog避免输入法无法弹出

        new Handler().postDelayed(new Runnable() {

@Override

            public void run() {

openKeyboard(messageTextView,mContext);

            }

},200);

    }

public void setContentText(String s){

messageTextView.setText(s);

        messageTextView.setSelection(s.length());//将光标移至文字末尾

    }

/**

    * 获取InputMethodManager,隐藏软键盘

    *

    * @param token

    */

    private void hideKeyboard(IBinder token, Context context) {

if (token !=null) {

InputMethodManager im = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);

            im.hideSoftInputFromWindow(token, InputMethodManager.HIDE_NOT_ALWAYS);

        }

}

/**

    * 获取InputMethodManager,打开软键盘

    *

*/

    private void openKeyboard(EditText et, Context context) {

et.setFocusable(true);

        et.setFocusableInTouchMode(true);

        et.requestFocus();

        InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);

        imm.showSoftInput(et, InputMethodManager.RESULT_UNCHANGED_SHOWN);

    }

}
  1. 布局文件class_edit_pop
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical" android:layout_width="match_parent"

    android:gravity="bottom"

    android:id="@+id/rl_outside_view"

    android:layout_height="match_parent">

        android:id="@+id/other_view"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:layout_above="@+id/rl_inputdlg_view"/>

        android:id="@+id/rl_inputdlg_view"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:background="@color/white"

        android:gravity="center_vertical"

        android:orientation="horizontal">

            android:id="@+id/edit_content"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:layout_weight="1"

            android:layout_marginLeft="15dp"

            android:background="@drawable/common_shape_line_100"

            android:layout_marginTop="8dp"

            android:layout_marginBottom="8dp"

            android:textColor="@color/black"

            android:maxLines="3"

            android:maxLength="200"

            android:layout_marginRight="15dp"

            android:hint="聊天输入"

            android:padding="10dp" />

            android:id="@+id/chat_send_btn"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:background="@color/teal_200"

            android:paddingTop="7dp"

            android:paddingBottom="7dp"

            android:paddingLeft="20dp"

            android:paddingRight="20dp"

            android:layout_marginRight="16dp"

            android:text="发送"/>

</RelativeLayout>
  • 按钮4
    删除黑板上的元素
    删除
/**

* 删除黑板中选中的view

*/

public void removeChooseView(ViewGroup group) {

for (Iterator> it =viewsMap.entrySet().iterator(); it.hasNext();){

Map.Entry entry = it.next();

        //删除key值为Two的数据

        if (entry.getValue().isChoose()) {

group.removeView(entry.getKey());

            it.remove();

        }

}

}
  • 按钮5
    插入图片功能每选择一张图片都放到黑板左上角,并且叠加到最上层,支持缩放
    添加图片

添加的图片使用的知乎Matisseglide

  • 按钮6
    拖动黑板需要自己代码设置黑板大小,才能滚动黑板到底部。demo中设置了50x屏幕宽度
    滚动黑板
boardParams =boardView.layoutParams as RelativeLayout.LayoutParams//取控件黑板的布局参数

boardParams!!.width = Util.getScreenInfo(this).widthPixels

boardParams!!.height=Util.getScreenInfo(this).heightPixels*50//50倍黑板

boardView.layoutParams =boardParams!!
  • 按钮7
    添加小黑板功能属于黑板中的另一个黑板,支持书写,放大缩小和拖拽
    小黑板功能
  1. 小黑板布局contr_view
<?xml version="1.0" encoding="utf-8"?>

<merge xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent">

        android:id="@+id/title_R"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:orientation="horizontal"

        android:background="@color/cardview_dark_background"

        android:padding="10dp">

            android:id="@+id/contr_title"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:text="标题"

            android:layout_centerInParent="true"/>

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:layout_alignParentRight="true">

                android:id="@+id/contr_small"

                android:layout_width="wrap_content"

                android:layout_height="wrap_content"

                android:textStyle="bold"

                android:textSize="20sp"

                android:padding="10dp"

                android:text="-"/>

                android:id="@+id/contr_close"

                android:layout_width="wrap_content"

                android:layout_height="wrap_content"

                android:textStyle="bold"

                android:textSize="20sp"

                android:layout_marginLeft="10dp"

                android:padding="10dp"

                android:text="X"/>

        android:id="@+id/contr_scrollView"

        android:layout_width="wrap_content"

        android:layout_height="match_parent"

        android:fillViewport="true">

            android:layout_width="match_parent"

            android:layout_height="match_parent"

            android:layout_centerInParent="true">

            <!--黑板-->



                android:id="@+id/contr_view"

                android:background="@color/design_default_color_secondary_variant"

                android:layout_width="match_parent"

                android:layout_height="match_parent">

</merge>
  1. 其中DragBoardView处理拖拽放大缩小
public class DragBoardViewextends LinearLayout/*implements View.OnTouchListener */{

private  final int touchDistance =40; //触摸边界的有效距离

    private  final int TOP =0x15;

    private  final int LEFT =0x16;

    private  final int BOTTOM =0x17;

    private  final int RIGHT =0x18;

    private  final int LEFT_TOP =0x11;

    private  final int RIGHT_TOP =0x12;

    private  final int LEFT_BOTTOM =0x13;

    private  final int RIGHT_BOTTOM =0x14;

    private  final int TITLE =0x19;

    protected int lastdistX;

    protected int lastlastY;

    private int dragDirection;

    protected int lastX;

    protected int lastY;

    private int oriLeft;

    private int oriRight;

    private int oriTop;

    private int oriBottom;

    private int offset =0; //可超出其父控件的偏移量

    private int ACTION_DOWN_VAL=0;//按下的时候的值。记录下来。

    private TextViewtitle;

    private TextViewsmall;

    private TextViewclose;

    private RelativeLayouttitle_R;

    private MScrollViewscrollView;

    private NomalBoardLayoutboard;

    private SmallBoardStatusListenerstatusListener;

    private boolean animting;//是否正在动画中

    public void setAnimting(boolean animting) {

this.animting = animting;

    }

public void setStatusListener(SmallBoardStatusListener statusListener) {

this.statusListener = statusListener;

    }

private int mHeight=0;

    private int mWidth=0;

    public DragBoardView(Context context) {

this(context,null);

    }

public DragBoardView(Context context, @Nullable AttributeSet attrs) {

this(context, attrs,0);

    }

public DragBoardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

        init();

    }

private void init() {

View parentView = LayoutInflater.from(getContext()).inflate(R.layout.contr_view, this, true);

        this.setOrientation(LinearLayout.VERTICAL);

        title=parentView.findViewById(R.id.contr_title);

        small=parentView.findViewById(R.id.contr_small);

        close=parentView.findViewById(R.id.contr_close);

        scrollView=parentView.findViewById(R.id.contr_scrollView);

        board=parentView.findViewById(R.id.contr_view);

        title_R=parentView.findViewById(R.id.title_R);

        scrollView.setScroll(false);

        board.setAction(BoardAction.ACTION_DRAW);

        close.setOnClickListener(new OnClickListener() {

@Override

            public void onClick(View v) {

statusListener.boardClose(DragBoardView.this);

            }

});

    }

@Override

    public boolean onInterceptTouchEvent(MotionEvent ev) {

dragDirection = getDirection(this, (int) ev.getX(),

                (int) ev.getY());

        if(ev.getAction()==MotionEvent.ACTION_DOWN){

ACTION_DOWN_VAL=dragDirection;//记录第一次按下的时候的值是标题栏TITLE的话,在ACTION_MOVE的时候才能拖动

        }

if(dragDirection!=-1&&dragDirection!=TITLE&&ACTION_DOWN_VAL!=TITLE){//避免拦截了关闭等按钮

            return true;

        }

return super.onInterceptTouchEvent(ev);

    }

@Override

    protected void onLayout(boolean changed, int l, int t, int r, int b) {

super.onLayout(changed, l, t, r, b);

    }

@Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

@Override

    public boolean onTouchEvent(MotionEvent event) {

if(animting){

return true;

        }

if(dragDirection==TITLE&&ACTION_DOWN_VAL==TITLE){

statusListener.boardMove(this,event);

        }else if(ACTION_DOWN_VAL!=-1/*&&ACTION_DOWN_VAL==TITLE*/){//第一次按下的时候不能在黑板中才可以执行

            int action = event.getAction()& MotionEvent.ACTION_MASK;

            if (action == MotionEvent.ACTION_DOWN) {

oriLeft = getLeft();

                oriRight = getRight();

                oriTop = getTop();

                oriBottom = getBottom();

                lastY = (int) event.getRawY();

                lastX = (int) event.getRawX();

            }

// 处理拖动事件

            delDrag(this, event, action);

        }

return true;

    }

/**

    * 处理拖动事件

    *

    * @param v

    * @param event

    * @param action

    */

    protected void delDrag(View v, MotionEvent event, int action) {

switch (action) {

case MotionEvent.ACTION_MOVE:

int dx = (int) event.getRawX() -lastX;

                int dy = (int) event.getRawY() -lastY;

                switch (dragDirection) {

case TITLE://标题栏

                        return;

                    case LEFT:// 左边缘

                        left(v, dx);

break;

                    case RIGHT:// 右边缘

                        right(v, dx);

break;

                    case BOTTOM:// 下边缘

                        bottom(v, dy);

break;

                    case TOP:// 上边缘

                        top(v, dy);

break;

                    case LEFT_BOTTOM:// 左下

                        left(v, dx);

                        bottom(v, dy);

break;

                    case LEFT_TOP:// 左上

                        left(v, dx);

                        top(v, dy);

break;

                    case RIGHT_BOTTOM:// 右下

                        right(v, dx);

                        bottom(v, dy);

break;

                    case RIGHT_TOP:// 右上

                        right(v, dx);

                        top(v, dy);

break;

                }

if (dragDirection != -1) {

statusListener.boardSizeChange(this,oriLeft, oriTop, oriRight, oriBottom);

                }

lastX = (int) event.getRawX();

                lastY = (int) event.getRawY();

break;

            case MotionEvent.ACTION_UP:

ACTION_DOWN_VAL=0;

                dragDirection =0;

                lastdistX=0;

                lastlastY=0;

break;

        }

}

/**

    * 触摸点为左边缘

    *

    * @param v

    * @param dx

    */

    private void left(View v, int dx) {

oriLeft += dx;

    }

/**

    * 触摸点为右边缘

    *

    * @param v

    * @param dx

    */

    private void right(View v, int dx) {

oriRight += dx;

    }

/**

    * 触摸点为下边缘

    *

    * @param v

    * @param dy

    */

    private void bottom(View v, int dy) {

oriBottom += dy;

    }

/**

    * 触摸点为上边缘

    *

    * @param v

    * @param dy

    */

    private void top(View v, int dy) {

oriTop += dy;

    }

/**

    * 获取触摸点flag

*

    * @param v

    * @param x

    * @param y

    * @return

    */

    protected int getDirection(View v, int x, int y) {

int left = v.getLeft();

        int right = v.getRight();

        int bottom = v.getBottom();

        int top = v.getTop();

        if (x

return LEFT_TOP;

        }

if(x>touchDistance&&xtouchDistance&&y

return TITLE;

        }

if (y

return RIGHT_TOP;

        }

if (x

return LEFT_BOTTOM;

        }

if (right - left - x

return RIGHT_BOTTOM;

        }

if (x

return LEFT;

        }

if (y

return TOP;

        }

if (right - left - x

return RIGHT;

        }

if (bottom - top - y

return BOTTOM;

        }

return -1;

    }

/**

    * 黑板移动监听

    */

    public interface SmallBoardStatusListener{

void boardMove(View v, MotionEvent event);

        void boardClose(View v);

        void boardSizeChange(View v,int l, int t, int r, int b);

    }
}
  1. boardMove回调中添加移动动画
@Override

public void boardMove(View v, MotionEvent event) {

setAction(BoardAction.ACTION_MOVE);

    float x =event.getX();

    float y = event.getY();

    switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

board.chooseRect.setRectVal(board.viewsMap.get(v).getLeft(), board.viewsMap.get(v).getTop(),board.viewsMap.get(v).getRight(),board.viewsMap.get(v).getBottom());

            left=board.viewsMap.get(v).getLeft();

            top=board.viewsMap.get(v).getTop();

            v.bringToFront();

break;

        case MotionEvent.ACTION_MOVE:

int dx= (int) (x-board.chooseRect.preX);

            int dy= (int) (y-board.chooseRect.preY);

            board.chooseRect.setRectVal(board.chooseRect.rect.left+dx,board.chooseRect.rect.top+dy,

                    board.chooseRect.rect.right+dx,board.chooseRect.rect.bottom+dy);

            invalidate();//view刷新

            break;

        case MotionEvent.ACTION_UP:

ViewInfo info=board.viewsMap.get(v);

            info.setLeft(board.chooseRect.rect.left);

            info.setTop(board.chooseRect.rect.top);

            info.setRight(board.chooseRect.rect.right);

            info.setBottom(board.chooseRect.rect.bottom);

            TranslateAnimation animation =new TranslateAnimation(0, info.getLeft()-left, 0, info.getTop()-top);//传入移动的距离而不是绝对值的坐标点

            animation.setDuration(300);

            v.setAnimation(animation);

            animation.setAnimationListener(new Animation.AnimationListener() {

@Override

                public void onAnimationStart(Animation animation) {

DragBoardView boardView=(DragBoardView)v;

                    boardView.setAnimting(true);

                }

@Override

                public void onAnimationEnd(Animation animation) {

DragBoardView boardView=(DragBoardView)v;

                    boardView.setAnimting(false);

                    animation.cancel();

                    v.clearAnimation();

                    board.viewsMap.put(v,info);

                    v.layout(info.getLeft(),info.getTop(),info.getRight(),info.getBottom());

                }

@Override

                public void onAnimationRepeat(Animation animation) {

}

});

break;

    }

board.chooseRect.preX=x;

    board.chooseRect.preY=y;

}

以上为部分代码

demo现目前只有这些功能,并且很多功能是有一些bug的,只是提供一种功能的思路。如果对你有帮助麻烦点个赞Star

demoBoard

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容