该项目主要实现了canvas绘制及小黄鸡会看向我们的手指以及上下弹跳等比较有意思的效果 我的github项目地址https://github.com/Johncuiqiang/-.git先看一下效果
代码导读
pointsBean类中主要实现了两个方法
初始化基本绘制的坐标点,包括头,身体,眼睛,鼻子的中心点,半径等相关参数,关键为了解决android的屏幕适配问题,我们先要获取屏幕宽高,然后依据获取到的屏幕参数动态初始化我们的坐标参数
private void initPoint(){
mBodyWidth = mScreenWidth /3;
mBodyHeight = mScreenHeight /4;
mLeft = (int) (mScreenWidth /3);
mRight = (int) (mLeft + mBodyWidth);
mTop = (int) (3* mScreenHeight /8);
.......
初始化我们的限制参数,限制参数就是在我们手指移动的过程中眼睛的上下左右移动的最大值,到达这个阈值,会进行一个加速度动画回弹到原位,原理类似于:手指移动5,图像移动1
private void initOtherParams() {
upScrollEdge = (int) (mBodyHeight /4f);
downScrollEdge = (int) (mBodyHeight /2f);
upScrollEye = (int) (upScrollEdge + mCircleR /6f);
downScrollEye = (int) (downScrollEdge + mCircleR /5f);
leftScrollEdge = (int) (mCircleR /3f);
rightScrollEdge = (int) (mCircleR /3f);
mBodyRatio = mScreenHeight / mBodyHeight +1;
mEyeRatio = mBodyRatio * downScrollEdge / downScrollEye;//手指滑动总距离是一样的
}
BezierView类中,我们进行了对形象的绘制,动画效果的实现等个功能 我们做完了我们初始化的功能,接下在ondraw方法中进行绘制我们的body形象
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
mPaint.setColor(mBodyColor);
mPaint.setStrokeWidth(UIUtils.dp2px(5));
mPaint.setStyle(Paint.Style.FILL);
drawHead(canvas);
drawBody(canvas);
drawEye(canvas);
drawNose(canvas);
}
主要举两个例子,画身体,这个使用贝塞尔曲线画的
什么是贝塞尔曲线?
这里简单解释一下,比如一条线有三个点,线的两头为开始和结束点,中间是一个点,通过拖拽中间这个点,达到线的凹凸变化。
就像小时候美术课画大山一样,你想让山高点,就把中间的点多往上画点,反之亦然,怎么样这个解释够形象把
/**
*画body的身体* @param canvas canvas对象
*/
private void drawBody(Canvas canvas) {
//重置路径mPath.reset();
//起点mPath.moveTo(mStartPointLeft.x, mStartPointLeft.y);
mPath.quadTo(mAssistPointLeft.x, mAssistPointLeft.y, mEndPointLeft.x, mEndPointLeft.y);
mPath.lineTo(mEndPointRight.x, mEndPointRight.y);
mPath.quadTo(mAssistPointRight.x, mAssistPointLeft.y, mStartPointRight.x, mStartPointLeft.y);
mPath.lineTo(mStartPointLeft.x, mStartPointLeft.y);//画路径canvas.drawPath(mPath, mPaint); }
画眼睛,绘制眼睛相对来说较好实现,drawCircle是google提供的绘制圆行的api,只是较为麻烦,眼睛还有眼珠等参数较多,后面的绘制就不上代码了
/**
* 画眼睛
* @param canvas canvas对象
*/
private void drawEye(Canvas canvas) {
//绘制外眼眶mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(UIUtils.dp2px(3));
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(mLeftEyePoint.x, mLeftEyePoint.y, mCircleR /6, mPaint);
canvas.drawCircle(mRightEyePoint.x, mRightEyePoint.y, mCircleR /6, mPaint);
//绘制白眼球mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(mLeftEyePoint.x, mLeftEyePoint.y, mCircleR /6, mPaint);
canvas.drawCircle(mRightEyePoint.x, mRightEyePoint.y, mCircleR /6,
mPaint);
//绘制黑眼球
mPaint.setColor(Color.BLACK);mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(mLeftEyeBallPoint.x, mLeftEyeBallPoint.y, mCircleR /12, mPaint);
canvas.drawCircle(mRightEyeBallPoint.x, mLeftEyeBallPoint.y, mCircleR /12, mPaint);
//绘制瞳孔(白色)
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(mLeftEyeBallPoint.x, mLeftEyeBallPoint.y - mCircleR /24,
mCircleR /30, mPaint);
canvas.drawCircle(mRightEyeBallPoint.x, mRightEyeBallPoint.y - mCircleR /24, mCircleR /30, mPaint);
}
该方法根据之前初始话的左右限定边界,滑动限制参数,以及具体的手指滑动距离,计算出body身体各部分应该移动的距离
/**
* 得到限制距离
*
* @param allDistance 手指移动的距离
* @param ratio 滑动限制参数
* @param leftUpEdge 左限定边界
* @param rightDownEdge 右限定边界
* @return 计算后的移动距离
*/
private float getAllDiff(float allDistance, float ratio, int leftUpEdge, int rightDownEdge) {
//限定边界
if (allDistance < -ratio * leftUpEdge) {
allDistance = -ratio * leftUpEdge;
}
if (allDistance > ratio * rightDownEdge) {
allDistance = ratio * rightDownEdge;
}
//计算图像y轴更改值 allDiff
float fraction;
float allDiff;
if (allDistance >= 0) {
//下滑或右滑
fraction = 1f * allDistance / (ratio * rightDownEdge);
float interpolation = mInterpolator.getInterpolation(fraction);
allDiff = rightDownEdge * interpolation;
} else {
fraction = 1f * Math.abs(allDistance) / (ratio * leftUpEdge);
float interpolation = mInterpolator.getInterpolation(fraction);
allDiff = -leftUpEdge * interpolation;
}
return allDiff;
}
对原始参数和变化参数进行存储,方便之后做回位和回弹动画的效果
/**
* 因为参数比较多,所以把原始参数和变化后的参数,存起来传到执行动画中
*/
private PropertyValuesHolder[] getValuesHolder() {
PropertyValuesHolder[] holders = new PropertyValuesHolder[mResetList.size() + mResetList.size()];
for (int i = 0; i < mResetList.size(); i++) {
MyPoint point = mResetList.get(i);
PropertyValuesHolder holderY = PropertyValuesHolder.ofFloat(point.getPointName() + ".y", point.y, point.oy);
holders[i] = holderY;
PropertyValuesHolder holderX = PropertyValuesHolder.ofFloat(point.getPointName() + ".x", point.x, point.ox);
//假如size = 8,单存y方向的参数可存8个,最后一个索引为7,从索引8+i = 8,9,10...开始存x方向参数
holders[mResetList.size() + i] = holderX;
}
return holders;
}
滑动监听事件
- 在ACTION_DOWN中我们记录一下用户的初始值
- 在ACTION_MOVE我们执行随手动以及眼睛跟随动画,在手指移动的过程中我们不断的改变我们的body各个坐标的值并不断重新绘制,并处理一些我们遇到的问题,比如用户在短时间内进行了多次滑动,第二次滑动开始的时候,如果第一次滑动没有执行完,我们要把第一次的滑动停止掉,重置动画也要停止掉,通过中间变量的判断实现即可
- 在ACTION_UP中我们在移动距离置0,执行重置动画
@Override
public boolean onTouchEvent(MotionEvent event) {
int downY = (int) event.getRawY();
int downX = (int) event.getRawX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = downY;
lastEyeX = downX;
lastEyeY = downY;
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG," ACTION_MOVE");
if (!isResetFinish) {
//重置动画如果没有执行完,停止动画
Log.d(TAG," isResetFinish");
//当手指滑动了两次,停止第一次滑动的效果,否则会出现划不动的情况
isAbortAnim = true;
//判断动画是否执行完,让isAbortAnim为false或true
isResetFinish = true;
}
//判断是否是点击还是移动
isPointerMove = true;
moveBody(event);
moveFace(event);
//重新绘制方法
invalidate();
break;
case MotionEvent.ACTION_UP:
allDy = 0;
allEyeDx = 0;
allEyeDy = 0;
Log.d(TAG," ACTION_UP");
//解开动画禁制
isAbortAnim = false;
if(isPointerMove) {
reset(getValuesHolder());
}
break;
default:
break;
}
return true;
}
移动身体逻辑的实现
/**
* 移动body的身体
* @param event 手指滑动事件
*/
private void moveBody(MotionEvent event) {
moveY = (int) event.getRawY();
int dy = moveY - lastY;
allDy = allDy + dy;
float allDiff = getAllDiff(allDy, mBodyRatio, UP_SCROLL_BODY_DEGE, DOWN_SCROLL_BODY_DEGE);
//更改各点y坐标
mStartPointLeft.y = (int) (mStartPointLeft.oy + allDiff);
mAssistPointLeft.y = (int) (mAssistPointLeft.oy + allDiff / 2);
mCirclePoint.y = (int) (mCirclePoint.oy + allDiff);
lastY = moveY;
}
重置回弹动画的执行,ValueAnimator这个类可以监听到属性动画改变值,一旦发现value的变化值与我们之前point存储的原有值相等,则停止执行回置动画,为了让动画效果更好,还增加了动画插值器OvershootInterpolator向前甩一定值后再回到原来位置,让动画具有弹性
/**
* 重置回弹动画
* @param holders 之前记录的原始参数
*/
private void reset(PropertyValuesHolder[] holders) {
final ValueAnimator animator;// 动画器
animator = ValueAnimator.ofPropertyValuesHolder(holders);
// 动画更新的监听
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator arg0) {
for (int i = 0; i < mResetList.size(); i++) {
MyPoint point = mResetList.get(i);
float valueY = (Float) arg0.getAnimatedValue(point.getPointName() + ".y");
float valueX = (Float) arg0.getAnimatedValue(point.getPointName() + ".x");
if (!isAbortAnim) {
//执行动画的核心方法
point.x = (int) valueX;
if (i > 2) {//前3个参数为body参数,不需要x方向移动
invalidate();
}
//动画执行完成
isResetFinish = point.x == point.ox;
Log.d(TAG," onAnimationUpdate");
isPointerMove = false;
}
// 根据最新value,更新布局
if (!isAbortAnim) {
point.y = (int) valueY;
invalidate();
//动画执行完成
isResetFinish = point.y == point.oy;
isPointerMove = false;
}
}
}
});
animator.setDuration(500);// 动画时间
animator.setInterpolator(new OvershootInterpolator());
animator.start();// 开启动画
}