Android翻页效果原理实现之引入折线先以折页的方式对翻页过程进行一个细致的分析,然后再在下一节将折线变为曲线。折页的实现可分为两种方式,一种是纯计算,我们利用已知的条件根据各类公式定理计算出未知的值,第二种呢则是通过图形的组合巧妙地去获取图形的交并集来实现,第二种方式需要很好的空间想象力这里就先不说了,而第一种纯计算的方式呢又可以分为使用高等数学和解三角形两种方法,前者对于数学不好的童鞋来说不易理解,这里我们选择后者使用解三角形来计算,首先我们先来搞个简单的辅助图:图很简单,一看就懂,大家可以拿个本子或者书尝试折页,不管你如何折,折叠区域AOB和下一页显示的区域APB必定是完全相等的对吧,那么我们就可以得到一个惊人的事实:角AOB恒为直角,这时我们来添加一些辅助线便于理解:我们设折叠后的三角形AOB的短边长度为x而长边长度为y,由图可以得出以下运算:我们可以使用相同的方法去解得y的值,这里我使用的是等面积法,由图可知梯形MOBP的面积是三角形MOA、AOB、APB面积之和:我们可以使用相同的方法去解得y的值,这里我使用的是等面积法,由图可知梯形MOBP的面积是三角形MOA、AOB、APB面积之和:这样我们可以根据任意一点得出两边边长,我们来代码中实践一下看看是不是这样的呢?为了便于理解,这里我重新使用了一个新的FoldView:[java]viewplaincopyprint?publicclassFoldViewextendsView{publicFoldView(Contextcontext,AttributeSetattrs){super(context,attrs);}}那么尝试根据我们以上分析的原理来绘制这么一个折页的效果,获取事件点、获取控件宽高就不说了,我们重点来看看onDraw中的计算:[java]viewplaincopyprint?@OverrideprotectedvoidonDraw(Canvascanvas){//重绘时重置路径mPath.reset();//绘制底色canvas.drawColor(Color.WHITE);/**如果坐标点在右下角则不执行绘制*/if(pointX==0&&pointY==0){return;}/**额,这个该怎么注释好呢……根据图来*/floatmK=mViewWidth-pointX;floatmL=mViewHeight-pointY;//需要重复使用的参数存值避免重复计算floattemp=(float)(Math.pow(mL,2)+Math.pow(mK,2));/**计算短边长边长度*/floatsizeShort=temp/(2F*mK);floatsizeLong=temp/(2F*mL);/**生成路径*/mPath.moveTo(pointX,pointY);mPath.lineTo(mViewWidth,mViewHeight-sizeLong);mPath.lineTo(mViewWidth-sizeShort,mViewHeight);mPath.close();//绘制路径canvas.drawPath(mPath,mPaint);}每次绘制的时候我们需要重置Path不然上一次的Path就会跟这一次叠加在一起,效果如下:(PS:动态效果图MicrosoftWord2003不好上传只有截图)效果是大致出来了,但是我们发现有一处不对的地方,当我们非常靠左或非常靠下地折叠时:如果再往左折此时我们的Path就会消失掉,其实这跟我们我们现实中的折页是一样的,折页的过程是有限制的,如下图:右下角点P因为受装订线的制约,其半径最大只能为纸张的宽度,如果我们始终以该宽度为半径折页,那么点P的轨迹就可以形成曲线Q,图中半透明红色区域为一个半圆形,也就是说,我们的点P只能在该范围内才应当有效对吧,那么该如何做限制呢?很简单,我们只需在计算长短边长之前判断触摸点是否在该区域即可:[java]viewplaincopyprint?/***计算短边的有效区域*/privatevoidcomputeShortSizeRegion(){//短边圆形路径对象PathpathShortSize=newPath();//用来装载Path边界值的RectF对象RectFrectShortSize=newRectF();//添加圆形到PathpathShortSize.addCircle(0,mViewHeight,mViewWidth,Path.Direction.CCW);//计算边界pathShortSize.computeBounds(rectShortSize,true);//将Path转化为RegionmRegionShortSize.setPath(pathShortSize,newRegion((int)rectShortSize.left,(int)rectShortSize.top,(int)rectShortSize.right,(int)rectShortSize.bottom));}同样计算有效区域这个过程是在onSizeChanged中进行,我们说过尽量不要在一些重复调用的方法内执行没必要的计算,在onDraw里我们只需在绘制钱判断下当前触摸点是否在该区域内,如果不在,那么我们通过坐标x轴重新计算坐标y轴:[java]viewplaincopyprint?/**判断触摸点是否在短边的有效区域内*/if(!mRegionShortSize.contains((int)mPointX,(int)mPointY)){//如果不在则通过x坐标强行重算y坐标mPointY=(float)(Math.sqrt((Math.pow(mViewWidth,2)-Math.pow(mPointX,2)))-mViewHeight);//精度附加值避免精度损失mPointY=Math.abs(mPointY)+mValueAdded;}那么如何来计算y坐标呢?很简单,我们只需根据圆的方程求解即可,因为P的轨迹是个圆~在得到新的y坐标mPointY后我们还应该为其加上一点点的精度值mValueAdded来挽回因浮点计算而损失的精度。而对于过分往下折出现的问题我们使用限制下折最大值的方法来避免:[java]viewplaincopyprint?/**缓冲区域判断*/floatarea=mViewHeight-mBuffArea;if(mPointY=area){mPointY=area;}在控件下方接近底部的地方我们设定一个缓冲区域,触摸点永远不能到达该区域,因为没有必要也没有意义,再往下就要划出控件了,别浪费多余的计算,运行效果大致如下:大致的效果出来了,我们还需要做一些补充工作,当触摸点在右下角某个区域时如果我们抬起手指,那么就让“纸张”自动滑下去,同理当触摸点在左边某个区域时我们让“纸张”自动翻过去,这里我们约定这两个区域分别是控件右下角宽高四分之一的区域和控件左侧八分之一的区域(当然你可以约定你自己的控件行为,这里我就哪简单往哪走了~):那么在上一节中我们也有类似的效果,这里我们依葫芦画瓢,当手指抬起时判断当前事件点是否位于右下角自滑区域内,如果在那么以当前事件点为坐标点A右下角为坐标点B根据两点式我们可以获得一条直线方程:此后根据不断自加递增的x坐标不断计算对应的y坐标直至点滑至右下角为止,既然涉及到事件,So我们在onTouchEvent处理:[java]viewplaincopyprint?caseMotionEvent.ACTION_UP://手指抬起时候/**获取当前事件点*/floatx=event.getX();floaty=event.getY();/**如果当前事件点位于右下自滑区域*/if(xmAutoAreaRight&&ymAutoAreaButtom){//获取当前点为直线方程坐标之一floatstartX=x,startY=y;/**当x坐标小于控件宽度时*/while(xmViewWidth){//不断让x自加x++;//重置当前点的值mPointX=x;mPointY=startY+((x-startX)*(mViewHeight-startY))/(mViewWidth-startX);//重绘视图invalidate();}}break;OK,我们来看看效果:大家看到当手指弹起时如果触摸点在右下角的自滑区域内的话就会自动“滑动”到右下角去,可是大家细心的话会发现效果好像不太对啊!怎么一下子就到右下角了?说好的“滑动”呢?好像毫无滑动效果啊!!!!为什么会这样?其实如果你细心就会发现上一节我们在讲图片左右两侧自滑的时候也是一样的效果!根本就没有什么滑动!为什么?难道在我们的while循环中没有执行invalidate吗?大家可以尝试在View中重写invalidate()方法Log一些信息看看invalidate()是否没有没执行。这里鉴于篇幅我就直接简单地说一下了,具体的我们会在《自定义控件其实很简单》系列文章讲到View绘制流程的时候详细阐述。这里我先可以告诉大家的是invalidate()方法即便你调用了也不会马上执行,invalidate()的作用更准确地说是将我们的View标记为无效,当View被标记为无效后Android就会尝试去调用onDraw()对其重绘,如果大家曾翻阅过API文档就会看到在invalidate()方法中Google给出了这么一句话:我们知道UI的刷新需要在UIThread也就是主线程中进行,这里会涉及到一个叫做message和messagequeue的东西,message你可以见文知意地称其为消息而messagequeue则为消息队列,我们将一个message压入messagequeue后UIThread会处理它,而我们刷新UI也需要有message作为载体去告诉UIThread诶需要更新UI了哦,而当我们在UIThread中去做一个loop不断地往messagequeue中压入消息时,我们的UIThread是不会去处理这些message的,直到loop结束为止,这就是为什么我们在while中不断调用invalidate()的时候你只会看到最后的结果而不会得到中间过程的变化。这里我只阐述了一个很浅显能懂的原因,更深入的原因涉及到View中各种标识位的运算如上所说篇幅过长就不多说了。那么知道了原因该如何去处理呢?message和messagequeue如果大家对Handler有一定的了解一定不陌生,没错,这里我们也将使用Handler来实现我们的滑动,首先,在我们的View中创建一个内部类,该内部类是Handler的一个子类,我们将使用它来更新View实现滑动效果:[java]viewplaincopyprint?/***处理滑动的Handler*/@SuppressLint(HandlerLeak)privateclassSlideHandlerextendsHandler{@OverridepublicvoidhandleMessage(Messagemsg){//循环调用滑动计算FoldView.this.slide();//重绘视图FoldView.this.invalidate();}/***延迟向Handler发送消息实现时间间隔**@paramdelayMillis*间隔时间*/publicvoidsleep(longdelayMillis){this.removeMessages(0);sendMessageDelayed(obtainMessage(0),delayMillis);}}我们额外提供一个slide()方法来对参数值进行更新:[java]viewplaincopyprint?/***计算滑动参数变化*/privatevoidslide(){/**如果x坐标恒小于控件宽度*/if(isSlide&&mPointXmViewWidth){//