安卓畫筆筆鋒的實現(xiàn)探索(三)田字格Demo

Demo的下載地址

下載地址

效果圖:如果對的效果不太明確的地方,請移步上兩篇文章

效果圖

Demo中做了哪些事情

1、提供畫板,以及收起畫板的動作
2、插入空格
3、換行
4、刪除或者長按刪除
5、切換筆的樣式
6、根據(jù)手指抬起來自動插入已繪制的圖形到EditText中
7、待續(xù)

1、提供畫板,以及收起畫板的動作

使用DrawViewLayout 繼承FrameLayout

public class DrawViewLayout extends FrameLayout implements View.OnClickListener, View.OnLongClickListener {

    private RelativeLayout mShowKeyboard;
    private RelativeLayout mGotoPreviousStep;
    private RelativeLayout mClearCanvas;
    private NewDrawPenView mDrawView;
    private RelativeLayout mSaveBitmap;
    private ViewStub mViewStub;
    private View mChild;
    private Context mContext;
    private ImageView mUpOrDownIcon;
    private LayoutInflater mInflater;
    private int mPenConfig;
    private boolean mIsShowKeyB;

    public DrawViewLayout(@NonNull Context context) {
        this(context, null);
    }


    public DrawViewLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initView();

    }
    private void initView() {
        mInflater = LayoutInflater.from(getContext());
        mChild = mInflater.inflate(R.layout.brush_weight_layout, this, false);
        addView(mChild);
        mShowKeyboard = (RelativeLayout) findViewById(R.id.rll_show_keyb_container);
        mGotoPreviousStep = (RelativeLayout) findViewById(R.id.rll_show_space_container);//空格
        mClearCanvas = (RelativeLayout) findViewById(R.id.rll_show_newline_container);
        mSaveBitmap = (RelativeLayout) findViewById(R.id.rll_show_delete_container);
        mViewStub = (ViewStub) findViewById(R.id.draw_view);
        //需要關(guān)心的selector的id
        mUpOrDownIcon = (ImageView) findViewById(R.id.rll_show_keyb_container_icon);
        setOnClickListenerT();
    }

布局文件使用的ViewStub輕量級的控件:使用Viewstub的在不需要彈出鍵盤的時候,渲染不占內(nèi)存

    <ViewStub
        android:id="@+id/draw_view"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_280"
        android:layout_gravity="center"
        android:layout_marginLeft="@dimen/dp_40"
        android:layout_marginRight="@dimen/dp_40"
        android:layout_marginTop="@dimen/dp_10"
        android:layout_marginBottom="@dimen/dp_10"
        android:layout="@layout/draw_view_layout"
        android:visibility="visible"/>

判斷是否填充過,沒有的話就不填充,第一版設(shè)計的時候,產(chǎn)品經(jīng)理說不用進(jìn)入頁面就彈出軟件盤,那使用ViewStub,只要沒有彈出來,渲染不占內(nèi)存,后續(xù)產(chǎn)品經(jīng)理說要彈出來,所以這里進(jìn)入的時候,我都設(shè)計出彈出鍵盤

  if (mViewStub.getParent() != null) {
            mViewStub.inflate();
       }
//隱藏軟鍵盤
  if (mDrawView.getVisibility() == GONE) {
            mIsShowKeyB=true;
            mViewStub.setVisibility(VISIBLE);
            mUpOrDownIcon.setSelected(true);
            Toast.makeText(mContext,"顯示鍵盤",Toast.LENGTH_SHORT).show();
            mDrawView.setVisibility(VISIBLE);
        } else if (mDrawView.getVisibility() == VISIBLE) {
            mIsShowKeyB=false;
            Toast.makeText(mContext,"隱藏鍵盤",Toast.LENGTH_SHORT).show();
            mDrawView.setVisibility(GONE);
            mViewStub.setVisibility(GONE);
            mUpOrDownIcon.setSelected(false);
        }

2、插入空格

在這里說個開發(fā)中的趣事,當(dāng)說要插入一個空格的時候,無非就是往EditText中插入一個空格吧,就用代碼插入,但是后面ui說,你咋插入的空格那么小,我說本來就只有插入這樣小啊,我能咋辦,我也很絕望,后續(xù)使用的是插入一張空白的圖

   @Override
    public void needSpace() {
        NewDrawPenView view = mDrawViewLayout.getSaveBitmap();
        if (view != null) {
            if (view.getHasDraw()) {
                mBitmap = view.getBitmap();
                mHandler.post(runnableUi);
                //保持一個聯(lián)系
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mIsCreateBitmap = true;
                        if (mCreatBimap == null) {
                            mCreatBimap = creatBimap();
                        }
                        mHandler.post(runnableUi);
                    }
                }, 100);
            } else {
                mIsCreateBitmap = true;
                if (mCreatBimap == null) {
                    mCreatBimap = creatBimap();
                }
                mHandler.post(runnableUi);
            }
        }
        mHandler.removeCallbacks(runnable);
    }

創(chuàng)建一個空白的bitmap 這里其實不用創(chuàng)建一個和手機設(shè)備一樣大的空白的bitmap,只創(chuàng)建一個和文本生成的字體一樣大的就行了,總感覺浪費性能,哈哈

//創(chuàng)建一個bitmap 
    private Bitmap creatBimap() {
        ColorDrawable drawable = new ColorDrawable(Color.TRANSPARENT);
        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        Bitmap bitmap = Bitmap.createBitmap(dm.widthPixels, dm.heightPixels, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.draw(canvas);
        return bitmap;
    }

把bitmap插入到edittext的線程

  Runnable runnableUi = new Runnable() {
        @Override
        public void run() {
            if (mIsCreateBitmap) {
                //110
                mBitmapResize = BitmapDrawUtils.resizeImage(mCreatBimap, mAllHandDrawSize, mAllHandDrawSize);
                mIsCreateBitmap = false;
            } else {
                mBitmapResize = BitmapDrawUtils.resizeImage(mBitmap, mAllHandDrawSize, mAllHandDrawSize);
            }
            if (mBitmapResize != null) {
                //根據(jù)Bitmap對象創(chuàng)建ImageSpan對象
                ImageSpan imageSpan = new ImageSpan(FieldCharacterShapeActivity.this, mBitmapResize);
                //創(chuàng)建一個SpannableString對象,以便插入用ImageSpan對象封裝的圖像
                full_name = LAST_NAME + System.currentTimeMillis();
                String s =FONT_NAME_HEAD + full_name + FONT_NAME_TAIL;
                SpannableString spannableString = new SpannableString(s);
                //  用ImageSpan對象替換face
                spannableString.setSpan(imageSpan, 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                //將選擇的圖片追加到EditText中光標(biāo)所在位置
                //                EditText ed = mSvContent.getFocusEditText();
                EditText ed = mRetContent.getLastFocusEdit();
                int index = ed.getSelectionStart(); //獲取光標(biāo)所在位置
                Editable edit_text = ed.getEditableText();
                if (index < 0 || index >= edit_text.length()) {
                    edit_text.append(spannableString);
                } else {
                    edit_text.insert(index, spannableString);
                }
                testStorage();
            }
            mDrawViewLayout.clearScreen();
        }
    };
   /**
     * 圖片縮放
     * @param originalBitmap 原始的Bitmap
     * @param newWidth 自定義寬度
     * @return 縮放后的Bitmap
     */
    public static Bitmap resizeImage(Bitmap originalBitmap, int newWidth, int newHeight){
        if (originalBitmap==null||originalBitmap.getWidth()==0||originalBitmap.getHeight()==0){
            return null;
        }
        int width = originalBitmap.getWidth();
        int height = originalBitmap.getHeight();
        //定義欲轉(zhuǎn)換成的寬、高
//            int newWidth = 200;
//            int newHeight = 200;
        //計算寬、高縮放率
        float scanleWidth = (float)newWidth/width;
        float scanleHeight = (float)newHeight/height;
        //創(chuàng)建操作圖片用的matrix對象 Matrix
        Matrix matrix = new Matrix();
        matrix.postScale(scanleWidth,scanleHeight);
        // 創(chuàng)建新的圖片Bitmap
        Bitmap resizedBitmap = Bitmap.createBitmap(originalBitmap,0,0,width,height,matrix,true);
        return resizedBitmap;
    }

3、換行

獲取ScrollView最后的一個EditText,對了,這里沒說,由于,需要滑動,所以自定義了一個ScrollView,可以插入很多控件,圖片,文字和語音等等,好多的控件,由于這里不需要那么多,所以只保留了一個EditText

    /**
     * 這里是換行的需要
     */
    @Override
    public void creatNewLine() {
        EditText ed = mRetContent.getLastFocusEdit();
        int index = ed.getSelectionStart();
        Editable editable = ed.getText();
        editable.insert(index, "\n");
    }

4、刪除或者長按刪除

代碼如下

    @Override
    public void deleteOnClick() {
        if (mRetContent.getLastFocusEdit().getSelectionStart() == 0) {
            mRetContent.onBackspacePress(mRetContent.getLastFocusEdit());
            if (mHandler!=null) {
                mHandler.removeCallbacks(runnable);
            }
        } else {
            SystemUtils.sendKeyCode(67);
        }
    }
 /**
     * <pre>
     * 使用Instrumentation接口:對于非自行編譯的安卓系統(tǒng),無法獲取系統(tǒng)簽名,只能在前臺模擬按鍵,不能后臺模擬
     * 注意:調(diào)用Instrumentation的sendKeyDownUpSync方法必須另起一個線程,否則無效
     * @param keyCode
     *            按鍵事件(KeyEvent)的按鍵值
     * </pre>
     */
    public static void sendKeyCode(final int keyCode) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 創(chuàng)建一個Instrumentation對象
                    Instrumentation inst = new Instrumentation();
                    // 調(diào)用inst對象的按鍵模擬方法
                    inst.sendKeyDownUpSync(keyCode);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

長按刪除的事件呢,其實無非就是讓這個方法不斷的執(zhí)行SystemUtils.sendKeyCode(67);在這里我使用枚舉的單利模式,其實這樣做不太好,因為枚舉的單利比較消耗內(nèi)存

package com.shiming.pen.field_character;

import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;

import com.shiming.pen.R;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


/**
 * @author shiming
 * @version v1.0 create at 2017/9/13
 * @des 使用模式為enum的單利模式
 */
public enum Executor {
    INSTANCE;
    private ScheduledExecutorService mScheduledExecutorService;
    private DrawViewLayout.IActionCallback mCallback;

    public void setCallback(DrawViewLayout.IActionCallback callback) {
        mCallback = callback;
    }

    @SuppressLint("HandlerLeak")
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            int viewId = msg.what;
            switch (viewId) {
                case R.id.rll_show_delete_container:
                    if (mCallback == null)
                        return;
                    mCallback.deleteOnLongClick();
                    break;
            }
        }
    };

    public void upData(int id) {
        final int vid = id;
        //只有一個線程,用來調(diào)度執(zhí)行將來的任務(wù)
        mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        //多少時間執(zhí)行一次
        mScheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                Message msg = new Message();
                msg.what = vid;
                handler.sendMessage(msg);
            }
        }, 0, 100, TimeUnit.MILLISECONDS);    //每間隔100ms發(fā)送Message
    }

    public void stop() {
        if (mScheduledExecutorService != null) {
            mScheduledExecutorService.shutdownNow();
            mScheduledExecutorService = null;
        }
    }
}

執(zhí)行的方法這里

  @Override
    public void deleteOnLongClick() {
        if (mRetContent.getLastFocusEdit().getSelectionStart() == 0) {
            mRetContent.onBackspacePress(mRetContent.getLastFocusEdit());
            if (mHandler!=null) {
                mHandler.removeCallbacks(runnable);
            }
        } else {
            SystemUtils.sendKeyCode(67);
        }
    }

5、切換筆的樣式

這里筆的樣式能有三種的情況,鋼筆,水彩筆,和橡皮擦,由于我這里每次都自動的保存了圖片,所以最后一次都是橡皮擦的模式,所以我只要判斷不是Pen的模式,就給它賦予這種的模式,同時還需更具點擊事件的變化而變化

  @Override
    public void onClick(View v) {
        int penConfig = mDrawViewLayout.getPenConfig();
        switch (v.getId()){
            case R.id.btn_change_pen:
                if (penConfig== IPenConfig.STROKE_TYPE_PEN){
                    penConfig=IPenConfig.STROKE_TYPE_BRUSH;
                }else {
                    penConfig=IPenConfig.STROKE_TYPE_PEN;
                }
                mDrawViewLayout.setPenConfig(penConfig);
                break;
        }
    }
   public void setPenConfig(int penConfig) {
       mDrawView.setCanvasCode(penConfig);
        mPenConfig=penConfig;
    }

最底層的方法 在這里 ,關(guān)鍵的地方需要invalidate一次,這樣自定義view才知道需要變換

    public void setCanvasCode(int canvasCode) {
        mCanvasCode = canvasCode;
        switch (mCanvasCode) {
            case IPenConfig.STROKE_TYPE_PEN:
                mStokeBrushPen = new SteelPen(mContext);
                break;
            case IPenConfig.STROKE_TYPE_BRUSH:
                mStokeBrushPen = new BrushPen(mContext);
                break;

        }
        //設(shè)置
        if (mStokeBrushPen.isNull()){
            mStokeBrushPen.setPaint(mPaint);
        }
        invalidate();
    }

6、根據(jù)手指抬起來自動插入已繪制的圖形到EditText中

在onTouchEvent事件中,記錄手指抬起來的時間,同時記錄下必須需要停止的時間

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        mIsCanvasDraw = true;
        //測試過程中,當(dāng)使用到event的時候,產(chǎn)生了沒有收到事件的問題,所以在這里需要obtian的一下
        MotionEvent event2 = MotionEvent.obtain(event);
        switch (event2.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                //每次都是這個筆,因為項目里面就只有這個筆,如果多了,這里需要改動
                setCanvasCode(CANVAS_NORMAL);
                mVisualStrokePen.onDown(mVisualStrokePen.createMotionElement(event2));
                mGetTimeListner.stopTime();
                break;
            case MotionEvent.ACTION_MOVE:
                mVisualStrokePen.onMove(mVisualStrokePen.createMotionElement(event2));
                mGetTimeListner.stopTime();
                break;
            case MotionEvent.ACTION_UP:
                long time = System.currentTimeMillis();
                mGetTimeListner.getTime(time);
                mVisualStrokePen.onUp(mVisualStrokePen.createMotionElement(event2),mCanvas);
                break;
            default:
                break;
        }
        invalidate();
        return true;
    }

接口回調(diào)

        mDrawView.setGetTimeListener(new NewDrawPenView.TimeListener() {
            @Override
            public void getTime(long l) {
                mIActionCallback.getUptime(l);
            }

            @Override
            public void stopTime() {
                mIActionCallback.stopTime();
            }
        });

需要做什么,其實就是一個任務(wù)

    @Override
    public void getUptime(long l) {
        mOldTime = l;
        mHandler.postDelayed(runnable, 100);
    }

    @Override
    public void stopTime() {
        mHandler.removeCallbacks(runnable);
    }

當(dāng)大于時間了,就使用handler發(fā)送一個消息

   final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            long l1 = System.currentTimeMillis();
            if ((l1 - mOldTime) > HADN_DRAW_TIME) {
                mHandler.removeCallbacks(runnable);
                Message msg = mHandler.obtainMessage();
                msg.obj = true;
                msg.what = 0x123;
                mHandler.sendMessage(msg);
            } else {
                mHandler.postDelayed(this, 100);
            }

        }
    };

處理handler消息,關(guān)鍵就是這個 mBitmap = view.clearBlank(100);

 @SuppressLint("HandlerLeak")//麻痹
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            int what = msg.what;
            switch (what) {
                case 0x123:
                    try {
                        boolean obj = (boolean) msg.obj;
                        if (obj) {
                            NewDrawPenView view = mDrawViewLayout.getSaveBitmap();
                            if (view != null) {
                                //邊距強行掃描
                                mBitmap = view.clearBlank(100);
                                mHandler.post(runnableUi);
                            }
                        }
                    } catch (Exception e) {

                    } finally {
                        mHandler.removeCallbacks(runnable);
                    }
                    break;
                case 0x124:
                    mRetContent.setVisibilityEdit(View.VISIBLE);
                    mRetContent.setVisibilityClose(View.VISIBLE);
                    mRetContent.getLastFocusEdit().setCursorVisible(true);
                    mRetContent.getLastFocusEdit().requestFocus();
                    break;
                case 0x125:
                    break;
            }
        }
    };

關(guān)于這個方法clearBlank(int ),這個方法不是我寫的,我又一次研究別人的一個Demo,測試下了這個效果非常牛逼,本來這個方法打算廢棄掉,但是最后覺得太可惜了,寫這個田字格的Demo又提出來了。其實還有種方法可以實現(xiàn),就是我們知道畫布的大小,同時也知道,需要生成的小圖的大小,我們可以動態(tài)的算,這就是另外一種方法,在不同設(shè)備上有點小許差異,但是也還可以

 /**
     * 逐行掃描 清楚邊界空白。功能是生成一張bitmap位于正中間,不是位于頂部,此關(guān)鍵的是我們畫布需要
     * 成透明色才能生效
     * @param blank 邊距留多少個像素
     * @return tks github E-signature
     */
    public Bitmap clearBlank(int blank) {
        if (mBitmap != null) {
            int HEIGHT = mBitmap.getHeight();//1794
            int WIDTH = mBitmap.getWidth();//1080
            int top = 0, left = 0, right = 0, bottom = 0;
            int[] pixs = new int[WIDTH];
            boolean isStop;
            for (int y = 0; y < HEIGHT; y++) {
                mBitmap.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1);
                isStop = false;
                for (int pix : pixs) {
                    if (pix != mBackColor) {

                        top = y;
                        isStop = true;
                        break;
                    }
                }
                if (isStop) {
                    break;
                }
            }
            for (int y = HEIGHT - 1; y >= 0; y--) {
                mBitmap.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1);
                isStop = false;
                for (int pix : pixs) {
                    if (pix != mBackColor) {
                        bottom = y;
                        isStop = true;
                        break;
                    }
                }
                if (isStop) {
                    break;
                }
            }
            pixs = new int[HEIGHT];
            for (int x = 0; x < WIDTH; x++) {
                mBitmap.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT);
                isStop = false;
                for (int pix : pixs) {
                    if (pix != mBackColor) {
                        left = x;
                        isStop = true;
                        break;
                    }
                }
                if (isStop) {
                    break;
                }
            }
            for (int x = WIDTH - 1; x > 0; x--) {
                mBitmap.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT);
                isStop = false;
                for (int pix : pixs) {
                    if (pix != mBackColor) {
                        right = x;
                        isStop = true;
                        break;
                    }
                }
                if (isStop) {
                    break;
                }
            }
            if (blank < 0) {
                blank = 0;
            }
            left = left - blank > 0 ? left - blank : 0;
            top = top - blank > 0 ? top - blank : 0;
            right = right + blank > WIDTH - 1 ? WIDTH - 1 : right + blank;
            bottom = bottom + blank > HEIGHT - 1 ? HEIGHT - 1 : bottom + blank;
            return Bitmap.createBitmap(mBitmap, left, top, right - left, bottom - top);
        } else {
            return null;
        }
    }

最后,謝謝提出問題的大佬

GitHub:https://github.com/Shimingli/WritingPen

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,351評論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,695評論 4 61
  • 2017年新年簽我抽到的是 溫柔。 是啊,感覺自己越來越不夠溫柔了,內(nèi)在,表象都缺少些柔和的東西。 女人的溫柔是女...
    素顏hb閱讀 376評論 0 0
  • 春天 你是我的棉被 溫暖如春 感受到愛情的幸福 甜甜蜜蜜 夏天 你是我的風(fēng)扇 神清氣爽 體會到呵護(hù)的感覺 快快樂樂...
    我愛吃任何魚閱讀 278評論 0 3
  • 緣起 使用lambda代碼會變得簡短些,看起來舒服些。個人建議在不是很熟悉的情況下不使用這個,因為太爽會忘記原來這...
    明慢慢閱讀 724評論 0 0

友情鏈接更多精彩內(nèi)容