????????本篇將介紹如何為我們的畫板應用添加分頁展示功能,讓用戶可以創建多個畫布并在它們之間輕松切換。這章沒有啥知識點的講解,主要介紹一下每頁保存的數據結構是什么樣的。
一、ListView
????????多頁數據的管理我們使用ListView。之前有文章講過ListView這里就不多贅述了,感興趣的讀者可以看看。Android最常用的控件ListView(詳解)?。
直接上圖例和代碼:
//綁定適配器(傳入handler)
adapter = new PictureAdapter(mContext,R.layout.list_item,listDate,handler);
viewMember.lv_tables.setAdapter(adapter);
(1)PictureView.java
//保存某一頁的視圖信息
public class PictureView {//保存比例信息Matrix matrixMain = new Matrix();//保存撤銷和恢復的信息private ArrayList<PaintDates> paintedList = new ArrayList<>();public ArrayList<MessageStrokes> getCancelList() {return cancelList;}public void setCancelList(ArrayList<MessageStrokes> cancelList) {this.cancelList = cancelList;}public ArrayList<MessageStrokes> getRecoverList() {return recoverList;}public void setRecoverList(ArrayList<MessageStrokes> recoverList) {this.recoverList = recoverList;}private ArrayList<MessageStrokes> cancelList = new ArrayList<>();private ArrayList<MessageStrokes> recoverList = new ArrayList<>();//設置一個專門為撤銷,回退服務的list//用來保存每一個操作的意義(可能是單筆的,可能是多筆)//view上private Bitmap cacheBitmap;private Canvas cacheCanvas ;public PictureView(int width, int height) {cacheBitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_4444);cacheCanvas = new Canvas(cacheBitmap);}public ArrayList<PaintDates> getPaintedList() {return paintedList;}public void setPaintedList(ArrayList<PaintDates> paintedList) {this.paintedList = paintedList;}public Bitmap getCacheBitmap() {return cacheBitmap;}public void setCacheBitmap(Bitmap cacheBitmap) {this.cacheBitmap = cacheBitmap;}public Canvas getCacheCanvas() {return cacheCanvas;}public void setCacheCanvas(Canvas cacheCanvas) {this.cacheCanvas = cacheCanvas;}}
PictureView
?是一個數據模型類,用于保存畫板中某一頁的完整狀態信息。
- (
cacheBitmap
?和?cacheCanvas
):保存當前頁面的最終渲染結果
paintedList:
存儲所有的筆畫數據
cancelList
:存儲已執行但可撤銷的操作
recoverList
:存儲已撤銷但可恢復的操作
matrixMain
:保存縮放、平移、旋轉等變換信息
(2)PictureAdapter.java
//適配器
public class PictureAdapter extends ArrayAdapter<PictureView> {//用來判斷當前View上顯示的時哪個(默認為第一個)public int localNum = 1;private Handler handler;public PictureAdapter(@NonNull Context context, int resource, @NonNull List<PictureView> objects, Handler handler) {super(context, resource, objects);this.handler = handler;}@SuppressLint("SetTextI18n")@NonNull@Overridepublic View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {PictureView p = getItem(position);View view;//新增一個內部類 ViewHolder,用于對控件的實例進行緩存ViewHolder viewHolder;if (convertView==null){//為每一個子項加載設定的布局view= LayoutInflater.from(getContext()).inflate(R.layout.list_item,parent,false);viewHolder= new ViewHolder();//分別獲取 imageview 和 textview 的實例viewHolder.image =view.findViewById(R.id.iv_image);viewHolder.imageNum =view.findViewById(R.id.tv_num);viewHolder.imageDelete=view.findViewById(R.id.bt_delete_item);viewHolder.layout = view.findViewById(R.id.fl_item);view.setTag(viewHolder);//將 viewHolder 存儲在 view 中}else {view=convertView;viewHolder= (ViewHolder) view.getTag();//重新獲取 viewHolder}// 設置要顯示的內容viewHolder.image.setImageBitmap(p.getCacheBitmap());viewHolder.imageNum.setText((position+1)+"");if((position+1)==localNum){//008FFBviewHolder.imageNum.setTextColor(Color.parseColor("#008FFB"));}else {viewHolder.imageNum.setTextColor(Color.WHITE);}//按鈕點擊事件(使用handler)viewHolder.imageDelete.setOnClickListener(v->{//創建一個線程Thread t = new Thread(() -> {Message m = handler.obtainMessage();m.what = 0x101;m.arg1 = position;handler.sendMessage(m);});t.start();});viewHolder.layout.setOnClickListener(v->{Thread t =new Thread(() -> {Message m = handler.obtainMessage();m.what = 0x102;m.arg1 = position;handler.sendMessage(m);});t.start();});return view;}private static class ViewHolder {TextView imageNum;ImageView image;ImageButton imageDelete;FrameLayout layout;}}
PictureAdapter中的點擊事件,通過Handler傳遞。
(3)Handler
@SuppressLint("HandlerLeak")
private void initHandler() {handler = new Handler(Looper.getMainLooper()){@SuppressLint("SetTextI18n")@Overridepublic void handleMessage(@NonNull Message msg) {switch (msg.what){case 0x101://刪除//彈出提升框now = msg.arg1;new AlertDialog.Builder(mContext).setTitle("提示").setMessage("確定要刪除 "+ (now+1) +"號視圖嗎?").setPositiveButton("確定", (dialogInterface, i) -> {//是否為刪除的為當前顯示的視圖System.out.println("AAAAAAAAAA: "+ (now+1) +" "+NowNum);if((now+1)==NowNum&&now==0){if(total == 1){Toast.makeText(mContext, "您無法刪除最后一個視圖", Toast.LENGTH_SHORT).show();}else {total--;//清空內存clearBitmap(listDate.get(now));listDate.remove(now);adapter.localNum = NowNum;blackboardView1.updateView(NowNum-1);}}else if((now+1)==NowNum&&now!=0){//向上移動total--;NowNum--;clearBitmap(listDate.get(now));listDate.remove(now);adapter.localNum = NowNum;blackboardView1.updateView(NowNum-1);}else if((now+1)!=NowNum&&(now+1)>NowNum){//不動total--;clearBitmap(listDate.get(now));listDate.remove(now);}else {//整體上移total--;NowNum--;clearBitmap(listDate.get(now));listDate.remove(now);adapter.localNum = NowNum;blackboardView1.updateView(NowNum-1);}viewMember.tv_whereForNum.setText(numToString(NowNum)+"/"+numToString(total));adapter.notifyDataSetChanged();}).setNegativeButton("取消",null).show();break;case 0x102://點擊試圖切換now = msg.arg1;NowNum = now+1;adapter.localNum = NowNum;//發送消息進行上傳(目前感覺沒必要上傳)
// Message message = new Message();
// message.what = 16;
// message.arg1 = NowNum;
// System.out.println("popopopo nowNum:"+NowNum);
// //operateHandler//同時更新的還有底部的數字viewMember.tv_whereForNum.setText(numToString(NowNum)+"/"+numToString(total));
// oldBitmap = blackboardView1.cacheBitmap;
// operateHandler.sendEmptyMessage(100);//通知截屏上傳(在沒更新之前)blackboardView1.updateView(NowNum-1);adapter.notifyDataSetChanged();break;case 0x103://漫游:顯示比例數值viewMember.tv_zoomNum.setText(FTOString((Float) msg.obj));break;case 0x104://down的是時候不讓獲取獲取焦點viewMember.bt_tables.setEnabled(false);viewMember.bt_last.setEnabled(false);viewMember.bt_next.setEnabled(false);viewMember.bt_add.setEnabled(false);viewMember.tv_whereForNum.setEnabled(false);Resources resources_table = mContext.getResources();if (viewMember.lv_tables.getVisibility() == View.VISIBLE) {viewMember.lv_tables.setVisibility(View.GONE);viewMember.tv_whereForNum.setTextColor(Color.WHITE);}if(viewMember.ll_more.getVisibility()==View.VISIBLE){viewMember.ll_more.setVisibility(View.GONE);Drawable imageDrawable = resources_table.getDrawable(R.drawable.tables_uncheck);viewMember.bt_tables.setBackground(imageDrawable);}//開始工具類的按鈕//1.首先要讓下面一排子的東西點不了viewMember.bt_pen.setEnabled(false);viewMember.bt_eraser.setEnabled(false);viewMember.bt_revoke.setEnabled(false);viewMember.bt_recover.setEnabled(false);viewMember.bt_zoom.setEnabled(false);//2.布局恢復if(viewMember.ll_penWidth.getVisibility() == View.VISIBLE){//這個就證明在畫筆的行列viewMember.bt_width_1.setEnabled(false);viewMember.bt_width_2.setEnabled(false);viewMember.bt_width_3.setEnabled(false);viewMember.bt_width_4.setEnabled(false);viewMember.bt_width_5.setEnabled(false);viewMember.bt_penColor.setEnabled(false);if(viewMember.ll_colorAndAlpha.getVisibility() == View.VISIBLE){viewMember.ll_colorAndAlpha.setVisibility(View.GONE);}}if(viewMember.ll_eraser.getVisibility() == View.VISIBLE){viewMember.bt_son_eraser.setEnabled(false);viewMember.bt_handwriting_eraser.setEnabled(false);viewMember.sb_clear_sliding.setEnabled(false);}break;case 0x105://up的時候解封viewMember.bt_tables.setEnabled(true);viewMember.bt_last.setEnabled(true);viewMember.bt_next.setEnabled(true);viewMember.bt_add.setEnabled(true);viewMember.tv_whereForNum.setEnabled(true);viewMember.bt_pen.setEnabled(true);viewMember.bt_eraser.setEnabled(true);viewMember.bt_revoke.setEnabled(true);viewMember.bt_recover.setEnabled(true);viewMember.bt_zoom.setEnabled(true);if(viewMember.ll_penWidth.getVisibility() == View.VISIBLE){//這個就證明在畫筆的行列viewMember.bt_width_1.setEnabled(true);viewMember.bt_width_2.setEnabled(true);viewMember.bt_width_3.setEnabled(true);viewMember.bt_width_4.setEnabled(true);viewMember.bt_width_5.setEnabled(true);viewMember.bt_penColor.setEnabled(true);}if(viewMember.ll_eraser.getVisibility() == View.VISIBLE){viewMember.bt_son_eraser.setEnabled(true);viewMember.bt_handwriting_eraser.setEnabled(true);viewMember.sb_clear_sliding.setEnabled(true);}break;case 0x106: //電子筆清除屏幕new AlertDialog.Builder(mContext).setTitle("提示").setMessage("確定要清屏嗎?").setPositiveButton("確定", (dialogInterface, i) -> {blackboardView1.clear();blackboardView1.isDialog = false;}).setNegativeButton("取消",(dialogInterface, i) -> {blackboardView1.isDialog = false;}).show().setCanceledOnTouchOutside (false);blackboardView1.clear_hardware();//清除其他筆畫}super.handleMessage(msg);}};
}
Handler 涉及到了功能:這里涉及到后面要講的功能,這里簡單說下
0x101 :當ListView點擊刪除時調用,彈出 AlertDialog 要求用戶確定操作。
0x102 :點擊切換視圖,主界面顯示對應頁的畫布。
0x103 :放大縮小時,顯示比例數值。比如50% , 300%。
0x104 :用戶畫線的時候,不允許操作ListView。
0x105 :沒有寫畫時允許操作。
0x106:電子筆點擊按鈕后,調用清屏功能。
(4)更新畫布 updateView
//切換視圖,刷新
public void updateView(int whereView){//對所有數據進行更新ViewNum = whereView;mPaintedList = mListDate.get(ViewNum).getPaintedList();mCancelList = mListDate.get(ViewNum).getCancelList();mRecoverList = mListDate.get(ViewNum).getRecoverList();cacheBitmap = mListDate.get(ViewNum).getCacheBitmap();cacheCanvas = mListDate.get(ViewNum).getCacheCanvas();mMatrixMain = mListDate.get(ViewNum).matrixMain;//傳一下handlermMatrixMain.getValues(mainDate);Message m = this.handler.obtainMessage();m.what = 0x103;m.obj = mainDate[0];this.handler.sendMessage(m);cacheCanvas.drawColor(0,PorterDuff.Mode.CLEAR);bottomCanvas.drawColor(0,PorterDuff.Mode.CLEAR);invalidateReason = REASON_RE;invalidate();
}
將 PictureView 中的對象賦值給當前視圖即可。
二、PictureView中使用的實體類
這里具體介紹一下 PaintDates 和 MessageStrokes 具體內容。
(1)PaintDates.java
//保存每一筆的情況
//之后要實現筆鋒效果(保存的就不是paint和path了)
public class PaintDates {//沒必要每次都new一個Paint:就透明的與width不同Paint mPaint;Path mPath; //專門為透明度服務List<PathAndWidth> mOnePaths ;//保存每一筆畫的偏移ArrayList<Matrix> mMatrixS = new ArrayList<>();//設置一個model來判斷是這個類是點還是線(經歷了move就是線,沒有就是點)private int lineModel = 1;//首先開始為點final int POINT = 1;//點final int LINE = 2 ;//線final int DOTTED_LINE = 3;//虛線//保存起點的x,y;float mx;float my;float mXToMatrix;float mYToMatrix;//初始寬度(為點和虛線提供)float mWidth;//是否為待刪除狀態(為筆畫刪除提供服務)private boolean isDelete = false;private boolean isCut = false;//設置一個與他同病相憐的兄弟集合(說白了保存id)public boolean isCut() {return isCut;}public void setCut(boolean cut) {isCut = cut;}public boolean isDelete() {return isDelete;}public void setDelete(boolean delete) {isDelete = delete;}public PaintDates(Paint paint, List<PathAndWidth> path, float x, float y,float width) {mPaint = paint;mOnePaths = path;mx = x;my = y;mXToMatrix = x;mYToMatrix = y;mWidth = width;}public PaintDates(PaintDates pd){mPaint = pd.mPaint;mPath = pd.mPath;mOnePaths = new ArrayList<>();for (int i = 0; i < pd.mOnePaths.size() ; i++) {mOnePaths.add(new PathAndWidth(pd.mOnePaths.get(i)));}for (int i = 0; i <pd.mMatrixS.size() ; i++) {mMatrixS.add(new Matrix(pd.mMatrixS.get(i)));}mx = pd.mx;my = pd.my;mXToMatrix = pd.mXToMatrix;mYToMatrix = pd.mYToMatrix;mWidth = pd.mWidth;lineModel = pd.getLineModel();}public void draw(Canvas canvas){if(lineModel == POINT){drawPoint(canvas);}else if(lineModel == LINE){drawLine(canvas);}else {drawDottedLine(canvas);}}private void drawDottedLine(Canvas canvas) {canvas.drawPath(mPath,mPaint);}//實現包裹效果//為了實現有筆峰的包裹效果,應該使用一個公式:前一個線的寬度=m,當前線的寬度=n -> m/n + 1.public void drawPlus(Canvas canvas){Paint coverPaint = new Paint(mPaint);int A = coverPaint.getAlpha();coverPaint.setColor(Color.parseColor("#000000")); //先暫時換個黑色coverPaint.setAlpha(A);if(lineModel == POINT){//點的高光操作coverPaint.setStrokeWidth(mWidth*2f);canvas.drawPoint(mx,my,coverPaint);}else if(lineModel == LINE){for (PathAndWidth mPath:mOnePaths) {coverPaint.setStrokeWidth(mPath.width*2f);canvas.drawPath(mPath.path,coverPaint);if(mPath.addPaths!=null){for (int i = 0; i <mPath.addPaths.size() ; i++) {//coverPaint.setStrokeWidth(mPath.width*3f);canvas.drawPath(mPath.addPaths.get(i),coverPaint);}}}}else {//虛線包裹coverPaint.setStrokeWidth(mWidth*2f);canvas.drawPath(mPath,coverPaint);}}//實現橡皮擦的高光效果public void drawDottingRed(Canvas canvas,float p){Paint dotRedPaint = new Paint(mPaint);dotRedPaint.setStrokeCap(Paint.Cap.BUTT);dotRedPaint.setStrokeJoin(Paint.Join.BEVEL);PathEffect effect = new DashPathEffect(new float[]{40f,20f,10f,20f},p);dotRedPaint.setXfermode(null);dotRedPaint.setPathEffect(effect);dotRedPaint.setColor(Color.RED);dotRedPaint.setAlpha(100);canvas.drawPath(mPath,dotRedPaint);}//畫點的邏輯public void drawPoint(Canvas canvas){mPaint.setStrokeWidth(mWidth);canvas.drawPoint(mx,my,mPaint);}//畫線的邏輯public void drawLine(Canvas canvas){for (PathAndWidth mPath:mOnePaths) {mPaint.setStrokeWidth(mPath.width);if(mPath.addPaths != null){
// Paint RedPaint = new Paint(mPaint);
// RedPaint.setColor(Color.RED);for (int i = 0; i <mPath.addPaths.size() ; i++) {canvas.drawPath(mPath.addPaths.get(i),mPaint);}}canvas.drawPath(mPath.path,mPaint);}}//畫最后一段即可public void drawPatch(Canvas canvas){PathAndWidth pw = mOnePaths.get(mOnePaths.size()-1);mPaint.setStrokeWidth(pw.width);canvas.drawPath(pw.path,mPaint);if(pw.addPaths!=null){for (int i = 0; i < pw.addPaths.size(); i++) {//canvas.drawPath(pw.addPaths.get(i),mPaint);canvas.drawPath(pw.addPaths.get(i),mPaint);}}}//添加前面一段路徑的筆鋒pathpublic void drawFrontAddPath(Canvas canvas){PathAndWidth pw = mOnePaths.get(mOnePaths.size()-2);mPaint.setStrokeWidth(pw.width);if(pw.addPaths!=null){for (int i = 0; i < pw.addPaths.size(); i++) {canvas.drawPath(pw.addPaths.get(i),mPaint);}}}public static class PathAndWidth{Path path;//添加的pathArrayList<Path> addPaths;//這個path已經無法滿足需求Float width ;//形成path的后一個點float x;float y;//此點的變形float xToMatrix;float yToMatrix;//判斷這個線是否要分割boolean isCut = false;//比例float BL = -1;public PathAndWidth(PathAndWidth paw){if(paw.path!=null){path = new Path(paw.path);width = paw.width;}x = paw.x;y = paw.y;xToMatrix = paw.xToMatrix;yToMatrix = paw.yToMatrix;addPaths = paw.addPaths;}public PathAndWidth(Path path, Float width,float x,float y) {this.path = path;this.width = width;this.x = x;this.y = y;//在沒有zoom的情況下與原始點相同xToMatrix = x;yToMatrix = y;}//這是為透明度服務的public PathAndWidth(float x,float y){this.x = x;this.y = y;//在沒有zoom的情況下與原始點相同xToMatrix = x;yToMatrix = y;}}public int getLineModel() {return lineModel;}public void setLineModel(int lineModel) {this.lineModel = lineModel;}//顏色變化選項(后續有要求在搞)}//思路1:每兩個點之間保存一段路徑(性能要求非常高)//思路2:保存點的信息化橢圓(需要保存一個方形)
核心成員變量及其作用:
變量名 | 類型 | 作用 |
---|---|---|
mPaint | Paint | 保存繪制這一筆時所用的畫筆樣式(顏色、透明度、抗鋸齒等) |
mOnePaths | List<PathAndWidth> | 這是最關鍵的數據。它保存了構成這一筆的所有筆觸段(PathAndWidth ?對象)。每個筆觸段都包含一小段路徑 (Path ) 和繪制該段路徑時動態變化的筆觸寬度,以此來實現筆鋒效果(壓感、速度感應)。 |
mMatrixS | ArrayList<Matrix> | 保存這一筆畫所經歷過的所有變換矩陣(如平移、縮放、旋轉)。這使得該筆畫能夠跟隨畫布進行變換,而自身的原始數據保持不變。 |
lineModel | int | 標識這一筆的類型:POINT ?(一個點)、LINE ?(一條連續的線)、DOTTED_LINE ?(一條虛線)。繪制和擦除邏輯會根據不同類型而變化。 |
mx, my | float | 記錄這一筆的起始點坐標。對于POINT 類型,這就是點的位置;對于LINE ,這是moveTo 的起點。 |
mWidth | float | 記錄這一筆的初始(或基礎)寬度。主要用于繪制POINT 和DOTTED_LINE ,因為LINE 的寬度由mOnePaths 中的每個PathAndWidth 動態管理。 |
isDelete ,?isCut | boolean | 狀態標志。用于實現筆畫刪除和筆畫分割功能。前面橡皮擦那章解釋過 |
核心方法及其作用:
方法名 | 作用 |
---|---|
draw(Canvas canvas) | 核心繪制方法。根據lineModel 調用對應的繪制方法(drawPoint ,?drawLine ,?drawDottedLine ),將這一筆畫到傳入的Canvas 上。 |
drawPlus(Canvas canvas) | 繪制包裹高光效果。通常用于實現筆畫選中狀態。它會用原筆畫兩倍的寬度和特定顏色(代碼中為黑色)再畫一遍,形成“包裹”或“高亮”效果,提示用戶該筆畫被選中。 |
drawDottingRed(Canvas canvas, float p) | 繪制虛線效果。用于橡皮擦功能。當用戶使用橡皮擦時,可能用紅色的虛線來預覽即將被擦除的筆畫區域。參數p 用于控制虛線模式的偏移,實現動畫效果。 |
drawPatch(Canvas canvas) | 僅繪制最后一小段路徑。用于實時繪制(即用戶手指還在移動時)。為了提高性能,在用戶快速繪畫時,不需要重繪整個復雜路徑,只需繪制最新的一小段(mOnePaths 的最后一個元素)。 |
drawFrontAddPath(...) | 繪制前一段路徑的筆鋒。這是一個更細粒度的優化,用于確保在連續繪制時,筆鋒的銜接部分也能被正確繪制,避免出現斷點。 |
PathAndWidth (路徑與寬度)
這是一個內部靜態類,是?PaintDates
?的組成部分。它可以被稱為筆觸段數據持有者。它的存在是實現筆鋒效果的關鍵。
核心成員變量及其作用:
變量名 | 類型 | 作用 |
---|---|---|
path | Path | 保存一小段貝塞爾曲線路徑(由?quadTo 生成)。 |
width | Float | 保存繪制這一小段路徑時所用的筆觸寬度。筆鋒效果就是通過路徑不斷變化的同時,寬度也隨之變化(模擬壓感)來實現的。 |
addPaths | ArrayList<Path> | 附加路徑。為了實現筆鋒效果,前幾章有介紹 |
x, y | float | 記錄這一小段路徑的終點坐標。 |
xToMatrix, yToMatrix | float | 記錄經過變換矩陣作用后,終點坐標應該所在的位置。用于坐標轉換計算。 |
isCut | boolean | 標識此筆觸段是否處于被分割的狀態。 |
(2)MessageStrokes.java
//負責保存每一個操作
public class MessageStrokes {int MassageType; //信息種類ArrayList<IdAndStrokes> paintStrokes;//保存每個筆畫的Matrix matrix;Matrix mainMatrix;//用于保存右側的數字public MessageStrokes(int massageType) {MassageType = massageType;}static class IdAndStrokes{int id ;int num ;//針對于橡皮擦單獨設置,用來判斷需要刪除此ID幾次。PaintDates pd ;public IdAndStrokes(int id,PaintDates pd) {this.id = id;this.pd = pd;}}
}
//對于筆畫刪除而言,一定是倒著刪除。所以恢復的時候一定是正著來(id+筆畫)
這個類的主要作用是:封裝并保存一個完整的用戶操作,用于實現撤銷 (Undo) 和重做 (Redo) 功能,后面介紹撤銷恢復時詳細說明。本章節篇幅較少,主要是介紹多畫布的框架,為后面的章節打好基礎。