Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果
来源:互联网 发布:如何利用大数据赚钱 编辑:程序博客网 时间:2024/04/23 18:37
前言
接着上一期 Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件 的博客开始,同样,在开始前我们先来看一下目标效果:
下面上一下本章需要实现的效果图:
大家看到这个效果肯定不会觉得陌生,QQ已经把粘滞效果做的满大街都是,相信不少读者或多或少对于贝塞尔曲线有所了解,不了解的朋友们也没有关系,在这里我会带领读者领略一下贝塞尔的魅力!
一、关于贝塞尔曲线
我们知道,任何一条线段是由起始点和终止点的连线组成,两点组成一条直线,这就是最简单的一阶公式(就是线段):
一阶贝塞尔曲线表达公式(图略):
B(t) = P0 + ( P1 - P0 ) t = ( 1 - t ) P0 + t P1 , t∈[0,1]
很显然,一阶的贝塞尔只是用于一条线段,其中t的变化率代表着线性插值大小.所以我们的效果用于一阶贝塞尔曲线公式肯定不行,下面我们来着重介绍一下二阶(次)贝塞尔曲线变化率和公式:
(图片来自于网络)
公式:
B(t) = ( 1 - t )² P0 + 2 t ( 1 - t ) P1 + t² P2 , t∈[0,1]
其实公式对于我们的开发者来说并没有太大的意义,因为主要的算法我们的API都已经包含,不过我们需要了解的是,我们的辅助点的查找.首先,我们需要了解曲线是如何画出来的?从图中我们可以看出我们的辅助点是p1点,由p0和p1组成的线段加上p1和p2组成的线段一共是有两条线段,我们需要一个变化率t,t从p0走到p1和从p1走到p2的时间是一样的,这样我们连接两点,就产生了第三条直线(图中绿色的线),这条直线其实就是我们的贝塞尔曲线的切线,只要有了这条直线,我们就可以确定我们的贝塞尔曲线轨迹(这一点至关重要).
当然,有一阶二阶,肯定也会有三阶、四阶等等.因为辅助点的增加,曲线也会发生各种变化,在这里,博主就不介绍了,想了解更深入的读者,可以在很多关于贝塞尔的博客中去了解.
介绍完了贝塞尔曲线,接下来我们就要开始着手打造QQ的粘滞效果了.在开始编写代码前我们先分析一下,我们要实现这个效果所需要的准备工作:
- 自定义View先绘制两个同样大小并重叠的圆形
- 按照小圆的大小我们设置圆形上刷新图标
- 重写触摸事件,绘制我们的贝塞尔曲线
- 动画收回
二、自定义View绘制圆形
/** * 圆的画笔 */ private Paint circlePaint; /** * 画笔的路径 */ private Path circlePath; /** * 可拖动的最远距离 */ private int maxHeight; /** * 刷新图标 */ private Bitmap bt; private float topCircleRadius;//默认上面圆形半径 private float topCircleX;//默认上面圆形x private float topCircleY;//默认上面圆形y private float bottomCircleRadius;//默认上面圆形半径 private float bottomCircleX;//默认下面圆形x private float bottomCircleY;//默认下面圆形y private float defaultRadius;//默认上面圆形半径 float offset=1.0f; float lastY; OnAnimResetListener listener; ObjectAnimator anim;变量比较多,但是非常好理解,该写的注释也已经标注了,下面我们来看构造函数以及初始化:
public YPXBezierView(Context context) { this(context, null); } public YPXBezierView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public YPXBezierView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } protected void init() { maxHeight=dp(60); topCircleX=ScreenUtils.getScreenWidth(getContext())/2; topCircleY=dp(100); topCircleRadius=dp(15); bottomCircleX=topCircleX; bottomCircleY=topCircleY; bottomCircleRadius=topCircleRadius; defaultRadius=topCircleRadius; circlePath = new Path(); circlePaint = new Paint(); circlePaint.setAntiAlias(true); circlePaint.setStyle(Paint.Style.FILL_AND_STROKE); circlePaint.setStrokeWidth(1); circlePaint.setColor(Color.parseColor("#999999")); }代码很简单,我们首先定义好我们的一些参数值和初始化画笔,其中maxHeight代表可以拉伸的高度,可以由用户自己去设置,然后就是定位我们的圆形在屏幕上方且居中,最后把底部圆形和顶部圆形重叠.
@Override protected void onDraw(Canvas canvas) { drawPath(); float left=topCircleX-topCircleRadius; float top=topCircleY-topCircleRadius; canvas.drawPath(circlePath, circlePaint); canvas.drawCircle(bottomCircleX, bottomCircleY, bottomCircleRadius, circlePaint); canvas.drawCircle(topCircleX, topCircleY, topCircleRadius, circlePaint); int btWidth=(int) topCircleRadius* 2-dp(6); if ((btWidth) > 0) { bt = BitmapFactory.decodeResource(getResources(), R.mipmap.refresh); bt = Bitmap.createScaledBitmap(bt,btWidth, btWidth, true); canvas.drawBitmap(bt, left+dp(3), top+dp(2) , null); bt.recycle(); } super.onDraw(canvas); }
这个方法了,画圆形的代码不用多说,直接drawCircle就好,关于刷新图标,我们需要说一下,因为我们的刷新图标是需要跟随大圆的大小变化而变化的,所以它自身的大小一定是可变的,我查阅了关于修改bitmap大小的方法,发现只有在创建的时候使用createScaledBitmap方法,该方法支持bitmap的缩放,但是美中不足的是,它的效果是叠加的,如果把bitmap只创建一次并且不去释放,那么每次刷新的时候会发现我们的刷新图标越来越模糊,目前博主没有什么好的解决方案,只能在绘制的时候重新生成bitmap,如果有了解更优化的方案的话,欢迎大神联系交流~我们的边距是3dp,所以我们的位置需要减去6dp,这样看起来效果更好一点!
三、绘制贝塞尔曲线
图中有六个重要的点,p1、p2、p3、p4、anchor1、anchor2,因为我们的粘滞小球尽量需要平滑一点,所以博主选择了最简单的四个交叉点(p1~p4),这四个点不涉及到三角函数的处理,所以坐标很容易的就可以得到:
private void drawPath() { float p1X = topCircleX - topCircleRadius ; float p1Y = topCircleY ; float p2X = topCircleX + topCircleRadius; float p2Y = topCircleY ; float p3X = bottomCircleX - bottomCircleRadius ; float p3Y = bottomCircleY ; float p4X = bottomCircleX + bottomCircleRadius ; float p4Y = bottomCircleY ; float anchorX = (p1X+ p4X) / 2-topCircleRadius*offset; float anchorY = (p1Y + p4Y) / 2; float anchorX2 = (p2X +p3X) / 2+topCircleRadius*offset; float anchorY2 = (p2Y + p3Y) / 2; /* 画粘连体 */ circlePath.reset(); circlePath.moveTo(p1X, p1Y); circlePath.quadTo(anchorX, anchorY, p3X, p3Y); circlePath.lineTo(p4X, p4Y); circlePath.quadTo(anchorX2, anchorY2, p2X, p2Y); circlePath.lineTo(p1X, p1Y); }可能细心的朋友发现,我们的两个辅助点的x坐标动态的加减了 topCircleRadius*offset ,其实这是博主的一个小小的优化,因为按照效果图上的六个点,已经可以画出贝塞尔的粘滞效果,但是我们会发现,描边并不是很圆润,因为我们的曲线是穿过两个圆,所以看起来就和QQ未读消息数的那个气泡效果一样,很显然,和我们的预期刷新效果有一点点不同.在这里我之所以加上这个距离,是想让贝塞尔的起点相对往外切于圆的边上,这样描边出来的效果才更像"鼻涕",为什么要*offset,这个就要涉及到了我们的触摸事件监听了.
三、触摸事件监听以及收回
@Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastY = event.getRawY(); break; case MotionEvent.ACTION_MOVE: float delayY=event.getRawY() - lastY;//滑动高度的偏移量 if(delayY<0){ return true; } offset=1-delayY/maxHeight;//滑动的偏移量offset 范围 offset∈(1,0) //如果偏移量大于等于0.2的时候我们就让它开始重绘, // 这样可以给下面的圆留下一点可见半径,要不然offset为0的时候下面的圆就成了点 if(offset>=0.2){ bottomCircleRadius = defaultRadius * offset; bottomCircleX = topCircleX; bottomCircleY = topCircleY + delayY; topCircleRadius = (float) (defaultRadius * (Math.pow(offset, 1 / 3.0))); postInvalidate(); } break; case MotionEvent.ACTION_UP: animToReset(false); break; case MotionEvent.ACTION_CANCEL: animToReset(false); break; } return true; }主要代码在Move中处理,我们先得到手指滑动的高度,然后判断当前滑动的方向,过滤掉向上的滑动,因为我们的粘滞效果自上而下,所以不需要处理向上的操作(在这里说明一下,如果用户的需求是可以任意方向,就好比QQ的未读消息气泡,那么我们的触摸事件就需要针对手势进行判断,然后在绘制贝塞尔曲线时也要进行方向判断).有了滑动的距离,有了最大滑动距离,那么我们就可以得到滑动的偏移量:
public void animToReset(boolean lock){ if(!lock) { Log.e("onAnimationEnd", "动画开始"); anim= ObjectAnimator.ofFloat(offset, "ypx", 0.0F, 1.0F).setDuration(200); //使用反弹算法插值器,貌似没有什么太大的效果 - -! anim.setInterpolator(new BounceInterpolator()); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float cVal = (Float) animation.getAnimatedValue(); offset = cVal; bottomCircleX=bottomCircleX+(topCircleX-bottomCircleX)*offset; bottomCircleY=bottomCircleY+(topCircleY-bottomCircleY)*offset; bottomCircleRadius=bottomCircleRadius+(topCircleRadius-bottomCircleRadius)*offset; topCircleRadius=topCircleRadius+(defaultRadius-topCircleRadius)*offset; postInvalidate(); } }); anim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { Log.e("onAnimationEnd", "动画结束"); if (listener != null) { listener.onReset(); } } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); anim.start(); } }忽视掉lock参数,这个参数是为了后面QQ刷新准备的,在此不多介绍,我们直接看onAnimationUpdate动画回调,在这里我们根据返回的每一帧率,动态设置回我们的初始状态并且添加了动画结束的回调,到此,我们的贝塞尔控件全部完成
四、使用和总结
感谢大家的支持,谢谢!
作者:yangpeixing
QQ:313930500
下载地址:http://download.csdn.net/detail/qq_16674697/9741375
转载请注明出处~谢谢~
- Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果
- Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件
- Android仿苹果版QQ下拉刷新实现(三)
- Android贝塞尔曲线粘性下拉刷新仿QQ粘性拉动效果
- Android 仿IOS版QQ实现下拉刷新水滴的效果
- js+canvas绘制QQ下拉刷新贝塞尔曲线效果
- Android 轻松实现仿QQ消息下拉刷新
- Android 轻松实现仿QQ空间下拉刷新
- Android高仿QQ下拉刷新
- Android自定义ListView实现下拉刷新,效果仿SwipeRefreshLayout
- 仿IOS版QQ的下拉刷新头实现原理
- android仿QQ下拉回弹效果
- android仿QQ下拉回弹效果
- android 仿朋友圈下拉刷新效果
- Android下拉刷新效果实现
- iOS版QQ的黏性下拉刷新效果简易实现
- 高仿QQ聊天消息列表的下拉刷新效果
- 仿QQ的下拉效果
- Android studio导入项目时的问题(Re-download dependencies and sync project (requires network))
- Git总结
- 对于Java程序猿学习的建议
- INIT_DELAYED_WORK和INIT_WORK定时器中断函数
- [leetcode] 482. License Key Formatting
- Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果
- 【SSH网上商城项目实战12】添加和更新商品功能的实现
- Android开发艺术探索<Android的线程池>
- 计算Java对象在内存中占用空间
- CentOS6.5PXE自动部署
- 奇异值分解(SVD)
- 矩阵第一章总结笔记
- vs2012使用向导创建和使用dll
- C链栈实现