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<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:保存点的信息化椭圆(需要保存一个方形)

核心成员变量及其作用:

变量名类型作用
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<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) 功能,后面介绍撤销恢复时详细说明。本章节篇幅较少,主要是介绍多画布的框架,为后面的章节打好基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/922106.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/922106.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智能眼镜产品成熟度分析框架与评估

引言 当前(2025年9月12日),智能眼镜(Smart Glasses)市场正处于快速演进阶段,从早期的新奇设备向主流消费电子转型。AI整合、AR显示和多模态交互的进步推动了这一转变。根据最新数据,2025年AI眼镜发货量预计达686万台,同比增长265%,全球市场规模从2024年的约19.3亿美元…

(网络编程)网络编程套接字 UDP的socket API 代码解析

网络编程基础 为什么需要网络编程?--丰富的网络资源 用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。与本地打开视频文件类似,只是视频文件这个资源的来源是网络。 相比本地资源来说,网络提供了更为丰富的网络资源:所谓的网络资源…

【STM32】状态机(State Machine)

这篇博客介绍 状态机&#xff08;State Machine&#xff09;&#xff0c;适合用于嵌入式开发、驱动开发、协议解析、按键识别等多种场景。 一、什么是状态机&#xff08;State Machine&#xff09;&#xff1f; 状态机&#xff08;State Machine&#xff09;是一种用于描述系统…

深度学习在离岗检测中的应用

离岗检测技术正逐步成为现代企业精细化管理和安全生产的重要工具。这项基于计算机视觉和人工智能的应用&#xff0c;通过自动化、实时化的监测方式&#xff0c;有效提升了工作纪律性和运营效率&#xff0c;为项目管理者和企业提供了创新的监管解决方案。在许多工作场景中&#…

Spring缓存(二):解决缓存雪崩、击穿、穿透问题

1. 缓存穿透问题与解决方案 1.1 什么是缓存穿透 缓存穿透是指查询一个不存在的数据&#xff0c;由于缓存中没有这个数据&#xff0c;每次请求都会直接打到数据库。 如果有恶意用户不断请求不存在的数据&#xff0c;就会给数据库带来巨大压力。 这种情况下&#xff0c;缓存失去了…

PHP 与 WebAssembly 的 “天然隔阂”

WebAssembly&#xff08;简称 WASM&#xff09;是一种低级二进制指令格式&#xff0c;旨在为高级语言提供高性能的编译目标&#xff0c;尤其在浏览器环境中实现接近原生的执行效率。它主要用于前端性能密集型场景&#xff08;如游戏引擎、视频编解码、3D 渲染等&#xff09;&am…

unity中通过拖拽,自定义scroll view中子物体顺序

1.在每个content的子物体上挂载DragHandler脚本&#xff0c;并且添加Canvs Group组件&#xff0c;设置见图2.DragHandler脚本内容&#xff1a;using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections.Generic; using System.Coll…

用 Matplotlib 绘制饼图:从基础语法到实战美化,全面掌握分类数据可视化技巧

用 Matplotlib 绘制饼图:从基础语法到实战美化,全面掌握分类数据可视化技巧 在数据分析与可视化的世界里,**“图胜千言”**早已成为共识。而在众多图表类型中,饼图(Pie Chart)以其直观的比例展示方式,成为展示分类数据分布的常见选择。无论是业务报表、用户画像,还是市…

基础算法之二分算法 --- 2

大家好&#xff0c;不同的时间&#xff0c;相同的地点&#xff0c;时隔多日我们又见面了。继上次的二分算法后&#xff0c;我们这次要来学习的是二分答案了。这个部分相较于前面的二分算法难度有相当的提升&#xff0c;希望大家有所准备。虽然难度增加了&#xff0c;但是博主还…

发挥nano banana的最大能力

1. 概述Nano Banana 简介&#xff1a;Nano Banana 是 Google DeepMind 开发的 AI 图像生成与编辑模型&#xff0c;集成在 Google Gemini 平台中&#xff08;具体为 Gemini 2.5 Flash 版本&#xff09;。它以高效的图像编辑能力闻名&#xff0c;尤其在角色一致性、光影理解和快速…

leetcode 面试题01.02判定是否互为字符重排

一、问题描述二、解题思路解法一&#xff1a;对s1和s2进行sort排序&#xff0c;返回s1是否等于s2&#xff1b;解法二&#xff1a;用哈希表分别来记录s1和s2中字符出现的次数&#xff0c;统计完后&#xff0c;判断两个哈希表是否相等;三、代码实现解法一&#xff1a;时间复杂度&…

Python Yolo8 物体识别

支持单张图片/图片目录批量预标注 默认使用cuda GPU .env HTTP_PROXYhttp://192.168.2.109:10808 HTTPS_PROXYhttp://192.168.2.109:10808pyproject.toml [project] name "yolo-test" version "0.1.0" description "Add your description here&quo…

LeetCode100-234回文链表

本文基于各个大佬的文章上点关注下点赞&#xff0c;明天一定更灿烂&#xff01;前言Python基础好像会了又好像没会&#xff0c;所有我直接开始刷leetcode一边抄样例代码一边学习吧。本系列文章用来记录学习中的思考&#xff0c;写给自己看的&#xff0c;也欢迎大家在评论区指导…

BUG排查流程

引言简述Bug排查的重要性分享个人或团队在Bug排查中的常见挑战引出日记形式记录的价值日记格式设计时间戳&#xff1a;记录问题发现和解决的时间节点问题描述&#xff1a;清晰定义Bug的现象和影响范围环境信息&#xff1a;操作系统、版本号、依赖库等关键配置复现步骤&#xff…

汽车功能安全 Functional Safety ISO 26262 测试之一

汽车电子电气系统的日益复杂使得功能安全成为保障车辆可靠性和驾乘安全的关键。 本文将围绕ISO 26262标准的核心内容展开&#xff0c;帮助大家理解如何通过系统化的方法控制风险&#xff0c;进行测试&#xff0c;确保产品安全。 01 什么是功能安全&#xff1f; 首先&#xff0c…

人形机器人赛道的隐形胜负手:低延迟视频链路如何决定机器人未来

一、引言&#xff1a;爆发前夜的人形机器人赛道 2025 年&#xff0c;被业内称为“人形机器人量产元年”。政策与资本的合力&#xff0c;让这条原本还带着科幻色彩的产业赛道&#xff0c;骤然进入现实加速期。国家层面&#xff0c;《“机器人”行动计划》明确提出要推动人形机器…

从iPhone 17取消SIM卡槽,看企业如何告别“数据孤岛”

9月10日&#xff0c;苹果公司如期召开秋季新品发布会&#xff0c;正式推出iPhone 17系列。除了性能和拍照的常规升级&#xff0c;一个看似不起眼但意义深远的改变引起了广泛关注——iPhone 17 Pro系列全面取消了实体SIM卡槽&#xff0c;只保留了eSIM功能。这一举动不仅仅是技术…

【JavaWeb01】Web介绍

文章目录1.导学2.Web开发介绍2.1 Web网站的工作流程2.2 前后端分离开发1.导学 2.Web开发介绍 2.1 Web网站的工作流程 浏览器根据请求的域名请求对应的前端服务器&#xff0c;前端服务器接收到请求之后&#xff0c;把对应的前端代码返回给服务器。浏览器中有解析前端代码的解析引…

链路预测算法MATLAB实现

链路预测算法MATLAB实现 链路预测是复杂网络分析中的重要任务&#xff0c;旨在预测网络中尚未连接的两个节点之间未来产生连接的可能性。 程序概述 MATLAB程序实现了以下链路预测算法&#xff1a; 基于局部信息的相似性指标&#xff08;Common Neighbors, Jaccard, Adamic-Adar…

淘宝商品详情 API 的安全强化与生态协同创新路径

一、安全强化&#xff1a;从 “被动防御” 到 “主动免疫” 的体系升级动态身份认证与权限颗粒化构建 “生物特征 设备指纹 行为基线” 的三重认证机制&#xff1a;结合用户操作习惯&#xff08;如点击间隔、滑动轨迹&#xff09;生成动态令牌&#xff0c;对高权限接口&#…