当前位置: 首页 > news >正文

Android 项目:画图白板APP开发(六)——分页展示 - 教程

        本篇将介绍如何为我们的画板应用添加分页展示功能,让用户可以创建多个画布并在它们之间轻松切换。这章没有啥知识点的讲解,主要介绍一下每页保存的数据结构是什么样的。

一、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 paintedList = new ArrayList<>();
public ArrayList getCancelList() {
return cancelList;
}
public void setCancelList(ArrayList cancelList) {
this.cancelList = cancelList;
}
public ArrayList getRecoverList() {
return recoverList;
}
public void setRecoverList(ArrayList recoverList) {
this.recoverList = recoverList;
}
private ArrayList cancelList = new ArrayList<>();
private ArrayList 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 getPaintedList() {
return paintedList;
}
public void setPaintedList(ArrayList 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 {
//用来判断当前View上显示的时哪个(默认为第一个)
public int localNum = 1;
private Handler handler;
public PictureAdapter(@NonNull Context context, int resource, @NonNull List objects, Handler handler) {
super(context, resource, objects);
this.handler = handler;
}
@SuppressLint("SetTextI18n")
@NonNull
@Override
public 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){
//008FFB
viewHolder.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")
@Override
public 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;
//传一下handler
mMatrixMain.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 mOnePaths ;
//保存每一笔画的偏移
ArrayList 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 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  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  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:保存点的信息化椭圆(需要保存一个方形)

核心成员变量及其作用:

变量名类型作用
mPaintPaint保存绘制这一笔时所用的画笔样式(颜色、透明度、抗锯齿等)
mOnePathsList<PathAndWidth>这是最关键的数据。它保存了构成这一笔的所有笔触段PathAndWidth 对象)。每个笔触段都包含一小段路径 (Path) 和绘制该段路径时动态变化的笔触宽度,以此来实现笔锋效果(压感、速度感应)。
mMatrixSArrayList<Matrix>保存这一笔画所经历过的所有变换矩阵(如平移、缩放、旋转)。这使得该笔画能够跟随画布进行变换,而自身的原始数据保持不变。
lineModelint标识这一笔的类型POINT (一个点)、LINE (一条连续的线)、DOTTED_LINE (一条虚线)。绘制和擦除逻辑会根据不同类型而变化。
mx, myfloat记录这一笔的起始点坐标。对于POINT类型,这就是点的位置;对于LINE,这是moveTo的起点。
mWidthfloat记录这一笔的初始(或基础)宽度。主要用于绘制POINTDOTTED_LINE,因为LINE的宽度由mOnePaths中的每个PathAndWidth动态管理。
isDeleteisCutboolean状态标志。用于实现笔画删除笔画分割功能。前面橡皮擦那章解释过

核心方法及其作用:

方法名作用
draw(Canvas canvas)核心绘制方法。根据lineModel调用对应的绘制方法(drawPointdrawLinedrawDottedLine),将这一笔画到传入的Canvas上。
drawPlus(Canvas canvas)绘制包裹高光效果。通常用于实现笔画选中状态。它会用原笔画两倍的宽度和特定颜色(代码中为黑色)再画一遍,形成“包裹”或“高亮”效果,提示用户该笔画被选中。
drawDottingRed(Canvas canvas, float p)绘制虚线效果。用于橡皮擦功能。当用户使用橡皮擦时,可能用红色的虚线来预览即将被擦除的笔画区域。参数p用于控制虚线模式的偏移,实现动画效果。
drawPatch(Canvas canvas)仅绘制最后一小段路径。用于实时绘制(即用户手指还在移动时)。为了提高性能,在用户快速绘画时,不需要重绘整个复杂路径,只需绘制最新的一小段(mOnePaths的最后一个元素)。
drawFrontAddPath(...)绘制前一段路径的笔锋。这是一个更细粒度的优化,用于确保在连续绘制时,笔锋的衔接部分也能被正确绘制,避免出现断点。

PathAndWidth (路径与宽度)

这是一个内部静态类,是 PaintDates 的组成部分。它可以被称为笔触段数据持有者。它的存在是实现笔锋效果的关键。

核心成员变量及其作用:

变量名类型作用
pathPath保存一小段贝塞尔曲线路径(由 quadTo生成)。
widthFloat保存绘制这一小段路径时所用的笔触宽度。笔锋效果就是通过路径不断变化的同时,宽度也随之变化(模拟压感)来实现的。
addPathsArrayList<Path>附加路径。为了实现笔锋效果,前几章有介绍
x, yfloat记录这一小段路径的终点坐标
xToMatrix, yToMatrixfloat记录经过变换矩阵作用后,终点坐标应该所在的位置。用于坐标转换计算。
isCutboolean标识此笔触段是否处于被分割的状态。

(2)MessageStrokes.java

//负责保存每一个操作
public class MessageStrokes {
int MassageType;  //信息种类
ArrayList 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) 功能,后面介绍撤销恢复时详细说明。本章节篇幅较少,主要是介绍多画布的框架,为后面的章节打好基础。

http://www.hskmm.com/?act=detail&tid=13602

相关文章:

  • ESP32 读取旋转编码器
  • mysql/oracle LEFT JOIN 取时间最大的数据
  • 6月6日证书 - 工信部人才交流中心PostgreSQL中级PGCP高级PGCM认证
  • 基于遗传算法与非线性规划的混合优化算法在电力系统最优潮流中的实现
  • 【下一款产品】
  • 数1的个数
  • 通过ML.Net调用Yolov5的Onnx模型
  • Java-如何在Eclipse开发-数组
  • 常用数据生成器
  • 基于RSSI修正的定位算法分析
  • c# 反射动态添加Attribute
  • MyBatis-Plus 全方位深度指南:从入门到精通
  • 鸿蒙项目实战(十):web和js交互
  • 【9.24 直播】集群数据管理实战:时序数据库 IoTDB 数据分区、同步与备份详解
  • 函数计算进化之路:AI 应用运行时的状态剖析
  • 01_进程与线程
  • 第六届医学人工智能国际学术会议(ISAIMS 2025)
  • redis 6.0 多线程
  • docker 常用命令与端口映射
  • linux重启mysql服务,几种常见的方法
  • opencv学习记录3
  • 统计分析神器 NCSS 2025 功能亮点+图文安装教程
  • mysql常用语句,常用的语句整理
  • 当写脚本循环更新几百万数据发现很慢怎么办 - 孙龙
  • 服装采购跟单系统的高效管理实践 - 详解
  • 和汽车相关的国内期刊
  • 服务器CPU、内存、磁盘、网络使用率,东方通CPU使用率东方通内存使用率监控脚本
  • 3 网络基础知识+web基础知识+部署Server
  • wxpython图形界面_01_最小基本结构
  • 服务器总资源监控脚本