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

Android 项目:画图白板APP开发(三)——笔锋(多 Path 叠加)

        上一章讲解了如何获取更多信息点,正好是为本章功能服务的。“笔锋”是模拟真实书写体验的核心功能,它让画出的线条不再均匀平滑,而是能根据运笔的速度、方向和压力产生粗细、浓淡的自然变化,就像用毛笔、钢笔或铅笔在纸上书写一样。

一、点的宽度

点宽度的依据主要依据点的速度点的压力

(1)速度笔锋

1. VelocityTracker 的方法介绍

        VelocityTracker是android提供的用来记录滑动速度的一个类,可以监控手指移动的速度。下面是VelocityTracker的方法介绍。

static VelocityTracker obtain() //获取VelocityTracker对象//将需要追踪速度的触摸事件添加进来,可以是ACTION_DOWN, ACTION_MOVE, ACTION_UP中的任一个
void addMovement(MotionEvent event) /*** 用于计算添加进来的事件的速度,* units是速度的单位,通常设置为1000,意思是计算的的速度,单位是像素/秒* maxVelocity是速度的最大值,*/
void computeCurrentVelocity(int units, float maxVelocity) void computeCurrentVelocity(int units) /**获取id对应的手指在水平方向的速度,使用该方法前要调用computeCurrentVelocity()*/
float getXVelocity(int id)float getXVelocity()/**获取id对应的手指在垂直方向的速度,使用该方法前要调用computeCurrentVelocity()*/
float getYVelocity(int id)float getYVelocity()void clear() //重置velocityTracker,回到初始状态void recycle() //回收复用
2. VelocityTracker 具体使用
//声明 VelocityTracker 
private VelocityTracker velocityTracker ;//获取实例
velocityTracker = VelocityTracker.obtain();//基于已添加的事件计算当前速度
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1);
//computeCurrentVelocity():计算当前的速度值。
//这里的 1 表示每毫秒移动的像素数。1000:表示每秒移动的像素数//计算速度
Math.sqrt(velocityTracker.getXVelocity(pointerId)*velocityTracker.getXVelocity(pointerId)+velocityTracker.getYVelocity(pointerId)*velocityTracker.getYVelocity(pointerId));

使用数学公式实现速度的计算:

(2)压感笔锋

        压力的获取相对方便,只有使用笔写的时候 event.getPressure() 才会有意义,手写的结果通常1.0f:

// 获取压力值 (范围通常为 0~1)
float pressure = event.getPressure();// 获取工具类型 (判断是否是笔)
int toolType = event.getToolType(pointerIndex);

(3)宽度计算

  • 速度:速度越大,宽度越小
  • 压感:压力越大,宽度越大

宽度的计算可以通过 曲线函数法 :通过二次函数或者三次函数计算得到对应的宽度

比如:参数可以自行调整。

 private float WidthForVelocity(float x) {return Math.max(mWidth*(-0.01893f*x*x*x+0.224f*x*x-0.91937f*x+1.45f),mWidth*0.15f);}

二、曲线绘制

(1)二次贝塞尔曲线

二次贝塞尔曲在之前 画图白板APP开发(一)中介绍过,这里复制下代码

// 用于存储上一个触摸点的坐标
private float mPreviousX, mPreviousY;@Override
public boolean onTouchEvent(MotionEvent event) {// 获取当前触摸点的坐标float x = event.getX();float y = event.getY();// 根据不同的触摸动作进行处理switch (event.getAction()) {case MotionEvent.ACTION_DOWN:  // 手指按下事件// 将路径起点移动到当前触摸点mPath.moveTo(x, y);// 记录当前点作为下一个动作的前一个点mPreviousX = x;mPreviousY = y;break;case MotionEvent.ACTION_MOVE:  // 手指移动事件// 计算控制点坐标(取前一点和当前点的中点)float controlX = (x + mPreviousX) / 2;float controlY = (y + mPreviousY) / 2;// 使用二次贝塞尔曲线连接// 参数说明:// 前两个参数(mPreviousX,mPreviousY) - 控制点坐标// 后两个参数(controlX,controlY) - 结束点坐标mPath.quadTo(mPreviousX, mPreviousY, controlX, controlY);// 更新前一个点的坐标为当前点mPreviousX = x;mPreviousY = y;break;}// 请求重绘视图invalidate();// 返回true表示已消费该触摸事件return true;
}

结合计算得到的宽度,你就可以得到如下图片,宽度不一的曲线

注意:

这里不是通过一个Path绘制而成的,曲线的每一段为一个单独的Path。

//关键代码
float cx = (historicalX +mStartXs[pointerId]) / 2f;
float cy = (historicalY + mStartYs[pointerId]) / 2f;
if(Paints[pointerId] != null){Paints[pointerId].mOnePaths.add(new PaintDates.PathAndWidth(new Path(paths[pointerId]), mEndWidths[pointerId], historicalX, historicalY));Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-1).path.quadTo(mStartXs[pointerId], mStartYs[pointerId], cx, cy);
}

这里自己尝试手动写写,应该就很好理解。

三、笔锋实现(添加笔锋 Path)

(1)介绍

        上面的示例只实现了不同宽度笔迹的展现,压根没有实现笔锋效果。接下来尝试在已有 Path 的基础上,绘制笔锋通过在已有 Path 的两侧另外绘制 Path,形成笔锋效果

口头说可能不太清楚,直接上图片

        截取两段不同宽度的 Path ,红点为两段 Path 的衔接点,既然要实现笔锋,就需要想办法将其变为一个锥形   。我们在较大的区域多余的部分在创建两 Path 覆盖不就可以了吗?如下图所示:

注意:

上图看上去有很多衔接不恰的地方,如果绘制那不坑坑洼洼?

其实绘制的 Path 并非方形,而是如下图显示,这里只是为了方便绘图。绘制的 Path 也并非直线,而是贝塞尔曲线。

(2)确定添加的 Path (起始点和个数)

上面的猜想成立,那我们该如何确定另外的 Path 如何绘制呢?目前我们确定的是:

  1. 新增的 Path 宽度应该跟需要添加的 原始 Path 宽度一致(目的是为了尾部重合看不出差异)
  2. 新增的 Path 尾部的点需与原始 Path 尾部点重合

目前有两个问题:

1. 起始点应该如何确定?

2.需要添加的 Path 到底为多少个?

1. 起始点应该如何确定?

解答:

宽度不一的 Paht 对应的衔接点,我们可以看成一个大圆包裹着小圆,入下图所示:

红点为圆心(两条 Path 的交点)、两个圆分别对应宽度不一的 Path 

接下来我们来确定起始点(两个蓝色的点就是新增 Path 其实点的位置),我们的目的就是为了让新增的圆,跟大圆相切

2.需要添加的 Path 到底为多少个?

需要通过比较两个原始 Path 得到答案,如何倍数在3以内,新增两个就够了;如何倍数在3以外,在5以内,就需要添加 4 个 Path。

(3)代码

通过上面的思路,有了如下代码:

if(Math.abs(mEndWidths[pointerId]-mLastWidths[pointerId])>0.05f&&Paints[pointerId].mOnePaths.size()>=2){//第一步确定起点,有的化必定要画了if(mEndWidths[pointerId]<mLastWidths[pointerId]){Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-1).addPaths = new ArrayList<>();//起点在前,开始分段float BL = mLastWidths[pointerId]/mEndWidths[pointerId];if(BL>=1.0f&&BL<3.0f){//画两条线(求这两个点):这个是对当期的path进行添加//参数:基准点,首点,末点,前半径,后半径,addPaths (起点和终点都是center点)setTowAddPaths(mStartXs[pointerId], mStartYs[pointerId],mCenterXs[pointerId],mCenterYs[pointerId],cx,cy,mLastWidths[pointerId],mEndWidths[pointerId],Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-1).addPaths);}else {//画四条线(求这四个点)setFourAddPaths(mStartXs[pointerId], mStartYs[pointerId],mCenterXs[pointerId],mCenterYs[pointerId],cx,cy,mLastWidths[pointerId],mEndWidths[pointerId],Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-1).addPaths);}}else {//在压感笔锋的情况下第一个点就可能进来if(Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths == null){Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths = new ArrayList<>();}//起点在后,开始分段float BL = mEndWidths[pointerId]/mLastWidths[pointerId];if(BL>=1.0f&&BL<3.0f){//画两条线:这个是对前一个path精心添加if(Paints[pointerId].mOnePaths.size()<=2){//当这条线目前只有两条path时}else if(Paints[pointerId].mOnePaths.size()==3){//当这条线目前只有三条path时setTowAddPaths((Paints[pointerId].mx+mCenterXs[pointerId])/2f,(Paints[pointerId].my+mCenterYs[pointerId])/2f,Paints[pointerId].mx,Paints[pointerId].my,mCenterXs[pointerId],mCenterYs[pointerId],mLastWidths[pointerId],mEndWidths[pointerId],Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths);isThinToRough = true;}else {//当有三个点时float startX,startY,jdX,jdY;//System.out.println("AAAAAAAAAAAAAA来到了增大的笔锋");jdX = Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).x;jdY = Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).y;startX = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).x+Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-4).x)/2f;startY = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).y+Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-4).y)/2f;//System.out.println("CCCCCCCCCC"+startX+","+startY+","+jdX+","+jdY+","+mCenterXs[pointerId]+","+mCenterYs[pointerId]);setTowAddPaths(jdX,jdY,startX,startY,mCenterXs[pointerId],mCenterYs[pointerId],mLastWidths[pointerId],mEndWidths[pointerId],Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths);isThinToRough = true;}}else {//画四条线if(Paints[pointerId].mOnePaths.size()<=2){//当这条线目前只有两条path时}else if(Paints[pointerId].mOnePaths.size()==3){//当这条线目前只有三条path时setFourAddPaths((Paints[pointerId].mx+mCenterXs[pointerId])/2f,(Paints[pointerId].my+mCenterYs[pointerId])/2f,Paints[pointerId].mx,Paints[pointerId].my,mCenterXs[pointerId],mCenterYs[pointerId],mLastWidths[pointerId],mEndWidths[pointerId],Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths);isThinToRough = true;}else {//当有三个点时float startX,startY,jdX,jdY;//System.out.println("AAAAAAAAAAAAAA来到了增大的笔锋");jdX = Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).x;jdY = Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).y;startX = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).x+Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-4).x)/2f;startY = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).y+Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-4).y)/2f;//System.out.println("CCCCCCCCCC"+startX+","+startY+","+jdX+","+jdY+","+mCenterXs[pointerId]+","+mCenterYs[pointerId]);setFourAddPaths(jdX,jdY,startX,startY,mCenterXs[pointerId],mCenterYs[pointerId],mLastWidths[pointerId],mEndWidths[pointerId],Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths);isThinToRough = true;}}}
}else if (Math.abs(mEndWidths[pointerId]-mLastWidths[pointerId])<0.05f){//这种情况出现在笔画差不多的情况并且上一个已经添加了两条线//因为这个要针对手写笔锋就出错所以需要:排首if(Paints[pointerId]!=null&&Paints[pointerId].mOnePaths!=null){if(Paints[pointerId].mOnePaths.size()>3){//这是之前大的情况if(Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).width>mEndWidths[pointerId]){if(Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths!=null&&Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths.size()==2){Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths.clear(); //清空一下PointF Pjz = new PointF();Pjz.x = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).x+Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).x)/2f;Pjz.y = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).y+Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).y)/2f;PointF PStart = new PointF();PStart.x = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-4).x+Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).x)/2f;PStart.y = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-4).y+Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).y)/2f;PointF PEnd = new PointF();PEnd.x = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-1).x+historicalX)/2f;PEnd.y = (Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-1).y+historicalY)/2f;setTowAddPaths(Pjz.x,Pjz.y,PStart.x,PStart.y,cx,cy,Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-3).width,mEndWidths[pointerId],Paints[pointerId].mOnePaths.get(Paints[pointerId].mOnePaths.size()-2).addPaths);isThinToRough = true;}}else {//到时候还要写小的情况}}}
}

到这里都应该好理解,接下来详细讲解增加新线形成笔锋的逻辑,这里涉及到数学公式。

//增加新线形成笔锋
private void setTowAddPaths(float jdX,float jdY,float StartX, float StartY, float LastX, float LastY, float StartW,float LastW, ArrayList<Path> addPaths) {//第一步:求两个点System.out.println("CCCCCCCCCCCCCCCC jdX:"+jdX+", jdY:"+jdY+", StartX:"+StartX+", StartY:"+StartY+", LastX:"+LastX+", LastY:"+LastY);float k = -(StartX-LastX)/(StartY-LastY); //求斜率PointF p1 = new PointF();PointF p2 = new PointF(); //两焦点//还需要两个控制曲度的点PointF p3 = new PointF();PointF p4 = new PointF();//两个pathPath path1 = new Path();Path path2 = new Path();//判断是收还是尾部(比较那个大)if(StartW>LastW){double sqrt = Math.sqrt((StartW * StartW)*0.25 / (k * k + 1));float b1 = StartY - k*StartX;p1.x = StartX + (float) sqrt;p2.x = StartX - (float) sqrt;p1.y = k*p1.x + b1;p2.y = k*p2.x + b1;//求两个起始点float BL_W = LastW/StartW; // <1fp1.x = (StartX-p1.x)*BL_W + p1.x;p1.y = (StartY-p1.y)*BL_W + p1.y;p2.x = (StartX-p2.x)*BL_W + p2.x;p2.y = (StartY-p2.y)*BL_W + p2.y;//开始对控制点//k = -(jdX-LastX)/(jdY-LastY);//sqrt = Math.sqrt(((StartW+LastW)/2f * (StartW+LastW)/2f)*0.25 / (k * k + 1));b1 = jdY - k*jdX;p3.x = jdX + (float) sqrt;p4.x = jdX - (float) sqrt;p3.y = k*p3.x + b1;p4.y = k*p4.x + b1;BL_W = LastW/((StartW+LastW)/2f);//其实这个越小越饱满p3.x = (jdX-p3.x)*BL_W + p3.x;p3.y = (jdY-p3.y)*BL_W + p3.y;p4.x = (jdX-p4.x)*BL_W + p4.x;p4.y = (jdY-p4.y)*BL_W + p4.y;//开始画线-----------------------path1.moveTo(p1.x,p1.y);path1.quadTo(p3.x,p3.y,LastX,LastY);path2.moveTo(p2.x,p2.y);path2.quadTo(p4.x,p4.y,LastX,LastY);}else {double sqrt = Math.sqrt((LastW * LastW)*0.25 / (k * k + 1));float b2 = LastY - k*LastX;p1.x = LastX + (float) sqrt;p2.x = LastX - (float) sqrt;p1.y = k*p1.x + b2;p2.y = k*p2.x + b2;//求两个起始点float BL_W = StartW/LastW; //<1fp1.x = (LastX-p1.x)*BL_W + p1.x;p1.y = (LastY-p1.y)*BL_W + p1.y;p2.x = (LastX-p2.x)*BL_W + p2.x;p2.y = (LastY-p2.y)*BL_W + p2.y;//控制点b2 = jdY - k*jdX;p3.x = jdX + (float) sqrt;p4.x = jdX - (float) sqrt;p3.y = k*p3.x + b2;p4.y = k*p4.x + b2;BL_W = StartW/((StartW+LastW)/2f);//其实这个越小越饱满p3.x = (jdX-p3.x)*BL_W + p3.x;p3.y = (jdY-p3.y)*BL_W + p3.y;p4.x = (jdX-p4.x)*BL_W + p4.x;p4.y = (jdY-p4.y)*BL_W + p4.y;path1.moveTo(StartX,StartY);path1.quadTo(p3.x,p3.y,p1.x,p1.y);path2.moveTo(StartX,StartY);path2.quadTo(p4.x,p4.y,p2.x,p2.y);//看看传的点是个什么东西//System.out.println("CCCCCCCCCCCCCCCC jdX:"+jdX+", jdY:"+jdY+", StartX:"+StartX+", StartY:"+StartY+", LastX:"+LastX+", LastY:"+LastY);}addPaths.add(new Path(path1));addPaths.add(new Path(path2));
}//新增四条形成笔锋
private void setFourAddPaths(float jdX,float jdY,float StartX, float StartY, float LastX, float LastY, float StartW,float LastW, ArrayList<Path> addPaths) {float k = -(StartX-LastX)/(StartY-LastY); //求斜率PointF p1 = new PointF();PointF p2 = new PointF(); //两焦点//还需要两个控制曲度的点PointF p3 = new PointF();PointF p4 = new PointF();//两个pathPath path1 = new Path();Path path2 = new Path();if(StartW>LastW){double sqrt = Math.sqrt((StartW * StartW)*0.25 / (k * k + 1));float b1 = StartY - k*StartX;p1.x = StartX + (float) sqrt;p2.x = StartX - (float) sqrt;p1.y = k*p1.x + b1;p2.y = k*p2.x + b1;//求两个起始点float BL_W = LastW/StartW; // <1fp1.x = (StartX-p1.x)*BL_W + p1.x;p1.y = (StartY-p1.y)*BL_W + p1.y;p2.x = (StartX-p2.x)*BL_W + p2.x;p2.y = (StartY-p2.y)*BL_W + p2.y;//开始对控制点//k = -(jdX-LastX)/(jdY-LastY);//sqrt = Math.sqrt(((StartW+LastW)/2f * (StartW+LastW)/2f)*0.25 / (k * k + 1));b1 = jdY - k*jdX;p3.x = jdX + (float) sqrt;p4.x = jdX - (float) sqrt;p3.y = k*p3.x + b1;p4.y = k*p4.x + b1;BL_W = LastW/((StartW+LastW)/2f);//其实这个越小越饱满p3.x = (jdX-p3.x)*BL_W + p3.x;p3.y = (jdY-p3.y)*BL_W + p3.y;p4.x = (jdX-p4.x)*BL_W + p4.x;p4.y = (jdY-p4.y)*BL_W + p4.y;//开始画线-----------------------path1.moveTo(p1.x,p1.y);path1.quadTo(p3.x,p3.y,LastX,LastY);path2.moveTo(p2.x,p2.y);path2.quadTo(p4.x,p4.y,LastX,LastY);addPaths.add(new Path(path1));addPaths.add(new Path(path2));//在家两条线if(StartW-2*LastW>0.5f){setTowAddPaths(jdX, jdY,StartX,StartY,LastX,LastY,(StartW-LastW),LastW,addPaths);}}
}

重点讲解下 setTowAddPaths()

1.传入的参数

//参数:基准点,起点,终点,前半径,后半径,addPaths (起点和终点都是center点)

2.求起始点的斜率,从而推导出垂直的线跟大圆的交点

3.通过两个交点,求新增线的圆心

4.最后推导出新增的贝塞尔曲线

setFourAddPaths()就是在setTowAddPaths()基础上,减去两个半径,重新计算。

(4)效果

当然你也可以选择就在尾部添加一个笔锋。


文章转载自:

http://BR5d12iP.krfpj.cn
http://SU9R0imo.krfpj.cn
http://x6heGaEX.krfpj.cn
http://pCX4UBIx.krfpj.cn
http://X78rO0xF.krfpj.cn
http://9DjsLafQ.krfpj.cn
http://eX3tmHxA.krfpj.cn
http://qB0fnhvq.krfpj.cn
http://Ls5nDRt9.krfpj.cn
http://nMMussab.krfpj.cn
http://lVHc2LB3.krfpj.cn
http://P125UpD5.krfpj.cn
http://wTXBNC2m.krfpj.cn
http://gUgxlQSi.krfpj.cn
http://8emMNp4o.krfpj.cn
http://5lNDr85L.krfpj.cn
http://yj0vVQLc.krfpj.cn
http://ZhCCtBe9.krfpj.cn
http://CUJXMIGP.krfpj.cn
http://Kq68alBu.krfpj.cn
http://XjI3Oegg.krfpj.cn
http://tAdB0wOj.krfpj.cn
http://7nR4bCpG.krfpj.cn
http://7ZSPUYyt.krfpj.cn
http://7dQmxXBM.krfpj.cn
http://cUVfD7YN.krfpj.cn
http://c9wEyWFN.krfpj.cn
http://eB6O7iUX.krfpj.cn
http://wZBgJEpg.krfpj.cn
http://Qm5wlPPA.krfpj.cn
http://www.dtcms.com/a/366713.html

相关文章:

  • MySQL主从复制之进阶延时同步、GTID复制、半同步复制完整实验流程
  • Html重绘和重排
  • 25高教社杯数模国赛【C题国一学长思路+问题分析】
  • 观测云产品更新 | LLM 监测、查看器、事件中心、监控等
  • void*指针类型转换笔记
  • SpringBoot中 Gzip 压缩的两种开启方式:GeoJSON 瘦身实战
  • k8s基础(未完待续)
  • 拜占庭攻击与投毒攻击
  • Linux编写shell脚本,输入多个原文件名和新文件名,一次对多个文件重命名
  • 2025亚马逊卖家防恶搞指南:揪出恶意套路,3招守住店铺安全
  • Gmail 数据泄露安全警报以及启示
  • 23种设计模式——抽象工厂模式(Abstract Factory Pattern)详解
  • C++开发中的常用设计模式:深入解析与应用场景
  • Nginx 实战系列(一)—— Web 核心概念、HTTP/HTTPS协议 与 Nginx 安装
  • 移远EC200A OpenCPU笔记
  • 【bash】命令查看当前目录下文件个数
  • STM32G4 速度环开环,电流环闭环 IF模式建模
  • 发票、收据合并 PDF 小程序,报销上传 3 秒搞定
  • Beautiful.ai:AI辅助PPT工具高效搞定排版,告别熬夜做汇报烦恼
  • Redis的过期策略和Redis 内存淘汰策略
  • Uni-App + Vue onLoad与onLaunch执行顺序问题完整解决方案 – 3种实用方法详解
  • 【系统架构设计(13)】项目管理上:盈亏平衡分析与进度管理
  • android seekbar显示刻度
  • 深入内核交互:用 strace 看清 Android 每一个系统调用
  • Android实战进阶 - 富文本
  • iPhone17再爆猛料?苹果2025秋季发布会亮点抢先看
  • 北斗导航 | Android Studio开发NMEA0183上位机的技术方案
  • 邮件如何防泄密?这10个电子邮件安全解决方案真的好用,快收藏
  • 02-Media-4-mp4muxer.py 录制视频并保存为MP4文件的示例
  • 2025年数学建模国赛C题第二版本超详细解题思路