Android开发之使用贝塞尔曲线实现黏性水珠下拉效果
2017-10-22 22:31
537 查看
Android开发之使用贝塞尔曲线实现黏性水珠下拉效果
标签: 贝塞尔曲线简介
网上关于贝塞尔曲线的博客和教程很多,通常讲到的三点确定一条曲线:起点,终点,辅助点。常见的贝塞尔黏性效果
常见的各阶贝塞尔曲线
实现效果
本文所要讲的黏性下拉实现效果如下:效果计算分析
上图中,分别有四个点,
左边:开始点,
上边:控制点,
下边:结束点,
中间:圆心。
因此可看出,该贝塞尔曲线实际上就是一个二阶贝塞尔曲线(一个控制点)。各点的位置计算以及角度在稍后的代码中将做提供。
代码部分
PullView.java
import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.drawable.Drawable; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v4.view.animation.PathInterpolatorCompat; import android.util.AttributeSet; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; /** * Created by shenhua on 2017/10/21. * Email shenhuanet@126.com */ public class PullView extends View { private Paint mCirclePaint;//圆的画笔 private int mCircleRadius = 50;//圆的半径 private float mCirclePointX;//圆的xy坐标 private float mCirclePointY; private float mProgress;//进度 private int mDragHeight = 300;//可拖拽高度 private int mTargetWidth = 600;//目标宽度 private Path mPath = new Path();//贝塞尔曲线 private Paint mPathPaint; private int mTargetGravityHeight = 10;//重心点最终高度,决定控制点的Y坐标 private int mTangentAngle = 100;//角度变换 0-135 private Interpolator mProgressInterpolator = new DecelerateInterpolator();//进度插值器 private Interpolator mTangentAngleInterpolator;//切角路径插值器 private Drawable mContent = null;//圆圈内部的圈 private int mContentMargin = 10;//圈的边距 private ValueAnimator valueAnimator;//释放动画 public PullView(Context context) { this(context, null); } public PullView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public PullView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setAntiAlias(true);//抗锯齿 paint.setDither(true);//防抖动 paint.setStyle(Paint.Style.FILL);//填充方式 paint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent)); mCirclePaint = paint; paint = new Paint(Paint.ANTI_ALIAS_FLAG);//初始化路径部分画笔 paint.setAntiAlias(true); paint.setDither(true); paint.setStyle(Paint.Style.FILL); paint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent)); mPathPaint = paint; mTangentAngleInterpolator = PathInterpolatorCompat.create((mCircleRadius * 2.0f) / mDragHeight, 90.0f / mTangentAngle ); mContent = getResources().getDrawable(R.drawable.circle_drawable); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); updatePathLayout(); } private void updatePathLayout() { final float progress = mProgressInterpolator.getInterpolation(mProgress); //获取可绘制区域高度宽度 final float w = getValueByLine(getWidth(), mTargetWidth, mProgress); final float h = getValueByLine(0, mDragHeight, mProgress); //X对称轴的参数,圆的圆心坐标,半径等 final float cPointX = w / 2; final float cRadius = mCircleRadius; final float cPointY = h - cRadius; //控制点结束Y坐标 final float endControlY = mTargetGravityHeight; mCirclePointX = cPointX; mCirclePointY = cPointY; final Path path = mPath; //重置 path.reset(); path.moveTo(0, 0); //左边部分的结束点和控制点 float lEndPointX, lEndPointY; float lControlPointX, lControlPointY; //角度转弧度 float angle = mTangentAngle * mTangentAngleInterpolator.getInterpolation(progress); double radian = Math.toRadians(angle); float x = (float) (Math.sin(radian) * cRadius); float y = (float) (Math.cos(radian) * cRadius); lEndPointX = cPointX - x; lEndPointY = cPointY + y; //控制点y坐标变化 lControlPointY = getValueByLine(0, endControlY, progress); //控制点与结束定之前的高度 float tHeight = lEndPointY - lControlPointY; //控制点与x坐标的距离 float tWidth = (float) (tHeight / Math.tan(radian)); lControlPointX = lEndPointX - tWidth; //左边贝塞尔曲线 path.quadTo(lControlPointX, lControlPointY, lEndPointX, lEndPointY); //连接到右边 path.lineTo(cPointX + (cPointX - lEndPointX), lEndPointY); //右边贝塞尔曲线 path.quadTo(cPointX + cPointX - lControlPointX, lControlPointY, w, 0); //更新内容部分Drawable updateContentLayout(cPointX, cPointY, cRadius); } /** * 对内容部分进行测量并设置 * * @param cx cPointX * @param cy cPointY * @param radius cRadius */ private void updateContentLayout(float cx, float cy, float radius) { Drawable drawable = mContent; if (drawable != null) { int margin = mContentMargin; int l = (int) (cx - radius + margin); int r = (int) (cx + radius - margin); int t = (int) (cy - radius + margin); int b = (int) (cy + radius - margin); drawable.setBounds(l, t, r, b); } } /** * 获取当前值 * * @param start 起点 * @param end 终点 * @param progress 进度 * @return 某一个坐标差值的百分百,计算贝塞尔的关键 */ private float getValueByLine(float start, float end, float progress) { return start + (end - start) * progress; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int iHeight = (int) ((mDragHeight * mProgress) + getPaddingTop() + getPaddingBottom()); int iWidth = 2 * mCircleRadius + getPaddingLeft() + getPaddingRight(); int measureWidth, measureHeight; if (widthMode == MeasureSpec.EXACTLY) { measureWidth = width; } else if (widthMode == MeasureSpec.AT_MOST) { measureWidth = Math.min(iWidth, width); } else { measureWidth = iWidth; } if (heightMode == MeasureSpec.EXACTLY) { measureHeight = height; } else if (heightMode == MeasureSpec.AT_MOST) { measureHeight = Math.min(iHeight, height); } else { measureHeight = iHeight; } setMeasuredDimension(measureWidth, measureHeight); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int count = canvas.save(); float tranX = (getWidth() - getValueByLine(getWidth(), mTargetWidth, mProgress)) / 2; canvas.translate(tranX, 0); canvas.drawPath(mPath, mPathPaint); //画圆 canvas.drawCircle(mCirclePointX, mCirclePointY, mCircleRadius, mCirclePaint); Drawable drawable = mContent; if (drawable != null) { canvas.save(); //剪切矩形区域 canvas.clipRect(drawable.getBounds()); //绘制 drawable.draw(canvas); canvas.restore(); } canvas.restoreToCount(count); } /** * 设置进度 * * @param progress 进度 */ public void setProgress(float progress) { mProgress = progress; requestLayout(); } /** * 添加释放动作 */ public void release() { if (valueAnimator == null) { ValueAnimator animator = ValueAnimator.ofFloat(mProgress, 0f); animator.setInterpolator(new DecelerateInterpolator()); animator.setDuration(400); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Object val = animation.getAnimatedValue(); if (val instanceof Float) { setProgress((Float) val); } } }); valueAnimator = animator; } else { valueAnimator.cancel(); valueAnimator.setFloatValues(mProgress, 0f); } valueAnimator.start(); } }
circle_drawable.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#7FFFFFFF" /> </shape>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.shenhua.bezier_demo.PullView android:id="@+id/pullView" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout>
MainActivity.java
import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.MotionEvent; public class MainActivity extends AppCompatActivity { PullView pullView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pullView = (PullView) findViewById(R.id.pullView); } private float mTouchStartY; private static final float TOUCH_MOVE_MAX_Y = 300; private static final float SLIPPAGE_FACTOR = 0.5f;// 拖动阻力因子 0~1 @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: mTouchStartY = event.getY(); return true; case MotionEvent.ACTION_MOVE: float y = event.getY(); if (y >= mTouchStartY) { float moveSize = (y - mTouchStartY) * SLIPPAGE_FACTOR; float progress = moveSize >= TOUCH_MOVE_MAX_Y ? 1 : (moveSize / TOUCH_MOVE_MAX_Y); pullView.setProgress(progress); } return true; case MotionEvent.ACTION_UP: pullView.release(); return true; default: break; } return false; } }
总结
贝塞尔曲线在Android中用起来并不难,通常的使用到二阶贝塞尔曲线的创意组合就能实现很多酷炫的效果,曲线的变化就成了很重要的了,需要有很大创意,才能将贝塞尔曲线利用到最完美。相关文章推荐
- Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果
- Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果
- Android开发-使用自定义View实现loading效果
- Android开发之使用ViewPager实现图片左右滑动切换效果
- Android下拉列表(Spinner)效果(使用C#和Java分别实现)
- android使用PullToRefresh实现上拉加载和下拉刷新效果
- Android开发:使用ViewDragHelper实现抽屉拉伸效果
- Android开发——多线程使用Handler实现读诗效果
- 使用android自带的下拉刷新效果实现页面下拉刷新功能
- Android程序开发之使用Design包实现QQ动画侧滑效果和滑动菜单导航
- android游戏开发框架libgdx的使用(二十三)—使用Universal Tween Engine实现动画效果
- android游戏开发框架libgdx的使用(十八)—简单的AVG游戏效果实现
- 【Android进阶】使用Andbase快速开发框架实现常见侧滑栏和滑动标签页组合效果
- 【Android】Android开发实现进度条效果,SeekBar的简单使用。音量,音乐播放进度,视频播放进度等
- [置顶] 【Android】Android开发实现进度条效果,SeekBar的简单使用。音量,音乐播放进度,视频播放进度等
- 【Android进阶】使用Andbase快速开发框架实现常见侧滑栏和滑动标签页组合效果
- Android开发:使用ViewDragHelper实现抽屉拉伸效果
- android开发之滑动效果实现图片浏览_ViewFilpper的使用
- Android使用PullToRefresh实现上拉加载和下拉刷新效果的代码
- Android开发——使用RadioGroup及Fragment来实现底部Tab效果