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;
}
}