JJSearchViewAnim是CJJ同学这周刚放出来的一个实现了各种搜索交互动画的动画库,一共实现了8种不同的搜索交互动画,短短4天github上的star就已经900+。可见此项目的受欢迎程度。我也第一时间把代码clone下来看了一遍,并和CJJ交流了一些心得,这篇文章我们就来分析JJSearchViewAnim到底是如何实现的,以及该怎么更好的运用的项目中去呢?
2.使用方法
JJSearchViewAnim实现的效果部分如下,更详细的请参照这里:
JJDotGoPathController
JJAroundCircleBornTailController
JJCircleToLineAlphaController
JJSearchViewAnim使用方法相当简单:
1.先在布局文件xml中声明
<RelativeLayout 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" tools:context="com.cjj.jjsearchviewanim.MainActivity"> <com.cjj.sva.JJSearchView android:id="@+id/jjsv" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout>
2.再在Java代码中设置你需要显示的动画类型
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
JJSearchView mJJSearchView = (JJSearchView) findViewById(R.id.jjsv);
mJJSearchView.setController(new JJChangeArrowController());
}3.设置动画开启及恢复
mJJSearchView.startAnim(); mJJSearchView.resetAnim();
3.类关系图
JJSearchViewAnim.png
从上面的类图中可以清晰的看到JJSearchViewAnim的项目结构:
JJSearchView是继承自View的,其内部持有一个JJBaseController的对象,JJBaseController有8个子类的实现,应该就是对应这8个动画的具体实现了。通过给JJSearchView设置不同的Controller就能实现对应的动画效果了。
是不是觉得跟我们以前分析过的HTextView很相似?因为这两个项目都使用了策略模式来设定不同的Controller从而实现不同的动画效果,如果你以后也想开发这种类型的项目,那么这种架构是相当适合你的。
4.源码分析
在分析源码之前,我们要知道其实所有的动画无非是:
在规定的动画持续时间内,在特定时间绘制出当前需要展示的画面,并随时间变化而改变绘制的画面从而形成动画。
我们在开发中使用属性动画时,我们只需要传递需要变换的参数和时间等等,Android已经为我们封装好了绘制过程。但是如果我们需要开发例如上图中的几个效果的时候,属性动画已经不能满足我们,这时候我们就要负责整个动画每一帧的绘制了。这就要运用到自定义View,Canvas,Paint,Path和PathMeasure等等相关知识了。那到底该如何实现呢?下面我们就先从整体上分析JJSearchView的整体结构,然后再具体分析两个动画的具体实现,相信你看完之后就会明白。
1.JJSearchView的实现
JJSearchView的部分代码如下:
public class JJSearchView extends View { private JJBaseController mController = new JJChangeArrowController(); public JJSearchView(Context context) { this(context, null);
} public JJSearchView(Context context, AttributeSet attrs) { this(context, attrs, 0);
} public void setController(JJBaseController controller) { this.mController = controller;
mController.setSearchView(this);
invalidate();
} @Override
protected void onDraw(Canvas canvas) { super.onDraw(canvas);
mController.draw(canvas, mPaint);
} public void startAnim() { if (mController != null)
mController.startAnim();
} public void resetAnim() { if (mController != null)
mController.resetAnim();
}
}可以看出在onDraw()方法中直接调用了JJBaseController的draw()方法说明具体的绘制都是交给JJBaseController的实现类去做的,同时又提供了startAnim()和resetAnim()也都是调用mController了对应方法。代码很简单就不再多说了,我们继续来看看JJBaseController。
2.JJBaseController的实现
JJBaseController的部分代码如下:
public abstract class JJBaseController { public abstract void draw(Canvas canvas, Paint paint); //开启搜索动画
public void startAnim() {
} //重置搜索动画
public void resetAnim() {
} public ValueAnimator startSearchViewAnim() {
ValueAnimator valueAnimator = startSearchViewAnim(0, 1, 500); return valueAnimator;
} public ValueAnimator startSearchViewAnim(float startF, float endF, long time) {
ValueAnimator valueAnimator =startSearchViewAnim(startF, endF, time, null); return valueAnimator;
} public ValueAnimator startSearchViewAnim(float startF, float endF, long time, final PathMeasure pathMeasure) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(startF, endF);
valueAnimator.setDuration(time);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPro = (float) valueAnimator.getAnimatedValue(); if (null != pathMeasure)
pathMeasure.getPosTan(mPro, mPos, null);
getSearchView().invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() { @Override
public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation);
}
}); if (!valueAnimator.isRunning()) {
valueAnimator.start();
}
mPro = 0; return valueAnimator;
}
}JJBaseController是一个抽象类,draw(Canvas canvas, Paint paint);方法是一个抽象方法,所以实现类必须实现draw()方法来完成具体动画的绘制,此外startAnim()和resetAnim()也是空方法子类可以重写实现具体的功能。
值得注意的是这里实现的一个ValueAnimator,从代码上看上去是一个在500毫秒中从0-1的一个不断变化的值,最后在onAnimationUpdate()回调方法中赋值给了mPro并调用了invalidate();方法。从这里应该可以看出,在JJSearchViewAnim中所有具体的Controller应该都是根据不断变化的mPro的值来绘制对应的图像,最终形成动画。
此外JJBaseController还定义了三种状态:
public static final int STATE_ANIM_NONE = 0; public static final int STATE_ANIM_START = 1; public static final int STATE_ANIM_STOP = 2; @IntDef({STATE_ANIM_NONE,STATE_ANIM_START, STATE_ANIM_STOP}) @Retention(RetentionPolicy.SOURCE) public @interface State {
}分别对应当前View无动画,动画开始和动画结束的状态。下面我们就具体分析两个Controller的具体实现,来看看具体怎么实现的,分别是JJAroundCircleBornTailController与JJCircleToLineAlphaController。
3.JJAroundCircleBornTailController的实现
在看具体的实现之前,我们先看一下这个动画的设计图:
JJAroundCircleBornTailController
我们可以分解成两种状态:
正常状态:就是一个搜索放大镜。
动画状态:圆弧形进度不断围绕圆环旋转,并且进度完成之后放大镜的把手不断变长最终变成和正常状态一样。
再来看看具体的代码实现:
public class JJAroundCircleBornTailController extends JJBaseController { private int mAngle = 10; private RectF mRectF; private int cx, cy, cr; @Override
public void draw(Canvas canvas, Paint paint) { //先设置一个背景
canvas.drawColor(Color.parseColor(mColor)); //根据当前状态的不同调用不同的绘制方法
switch (mState) { case STATE_ANIM_NONE:
drawNormalView(paint, canvas); break; case STATE_ANIM_START:
drawStartAnimView(paint, canvas); break; case STATE_ANIM_STOP:
drawStopAnimView(paint, canvas); break;
}
} private void drawStartAnimView(Paint paint, Canvas canvas) { //设置paint的状态
paint.setAntiAlias(true);
paint.setColor(Color.parseColor(mColorTran));
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.STROKE);
canvas.rotate(45, cx, cy); //绘制旋转时的外环
canvas.drawCircle(cx, cy, cr, paint); //给mRectF赋值为圆形成的矩形的值
mRectF.left = cx - cr;
mRectF.right = cx + cr;
mRectF.top = cy - cr;
mRectF.bottom = cy + cr; //当mPro小于0.2时,绘制一个不断变短的直线以及一个弧形
if (mPro <= 0.2) {
canvas.drawLine(cx + cr, cy, cx + cr + cr * (.2f - mPro),
cy, paint);
canvas.save();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
canvas.drawArc(mRectF, 6, -14, false, paint);
canvas.restore();
} else if (mPro > 0.2 && mPro < 4.5) {
canvas.save();
paint.setColor(Color.WHITE); //不断增加mAngle的值
mAngle += 20; //不断的旋转画布再绘制弧形,就可以形成旋转进度
canvas.rotate(mAngle, getWidth() / 2, getHeight() / 2);
canvas.drawArc(mRectF, 0, mAngle / 4, false, paint);
canvas.restore();
} else { //当mPro的值大于4.5时
canvas.save();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(14);
paint.setStyle(Paint.Style.STROKE); //绘制出放大镜的把手,这里通过mPro来时把手的长度不断增加
canvas.drawLine(cx + cr, cy, cx + cr + cr * ((mPro - 4.5f) * 2), cy, paint);
canvas.drawCircle(cx, cy, cr, paint);
canvas.restore();
}
} private void drawNormalView(Paint paint, Canvas canvas) { //cr 表示圆环半径
cr = getWidth() / 15; //cx 表示圆心的x坐标
cx = getWidth() / 2; //cy 表示圆心得y坐标
cy = getHeight() / 2;
paint.reset();
paint.setAntiAlias(true); // 保存当前canvas的状态
canvas.save();
paint.setColor(Color.WHITE);
paint.setStrokeWidth(14);
paint.setStyle(Paint.Style.STROKE); //将canvas旋转45度
canvas.rotate(45, cx, cy); //画斜线
canvas.drawLine(cx + cr, cy, cx + cr * 2, cy, paint); //画圆形
canvas.drawCircle(cx, cy, cr, paint); //恢复canvas的状态到上次save()方法调用的状态
canvas.restore();
} @Override
public void startAnim() { if (mState == STATE_ANIM_START) return; //设置状态
mState = STATE_ANIM_START; //开启ValueAnimator
startSearchViewAnim(0, 5, 2000);
} @Override
public void resetAnim() { if (mState == STATE_ANIM_STOP) return;
mState = STATE_ANIM_STOP;
mAngle = 0;
startSearchViewAnim();
}
}以上就是大部分JJAroundCircleBornTailController的代码,由于这个动画的初始状态和完成动画后的状态是一样的所以drawStopAnimView(paint, canvas);和drawStartAnimView(paint, canvas);方法是相同的实现,这里就省略了。
从上面的代码注释中可以看出,当我们调用startAnim()方法时会通过startSearchViewAnim(0, 5, 2000);开启ValueAnimator,这里是在2000毫秒中将mPro的值从0-5匀速变换,然后再回调方法中又回不断的调用invalidate()方法从而不断调用JJAroundCircleBornTailController的draw()方法,进而就可以通过判断mPro的值来绘制不同状态的图像。从而就达到了动画效果。相当清晰的实现,下面让我们来看看JJCircleToLineAlphaController是不是也是类似的实现方法呢?
4.JJCircleToLineAlphaController的实现
再看一下这次的动画设计图:
JJCircleToLineAlphaController
同样可以分解成两种状态:
正常状态: 一个放大镜以及外面有一个圆环
动画状态: 整体不断向右平移,并且圆环会不断减少最后变为输入框的横线。
下面我们来看看具体实现:
public class JJCircleToLineAlphaController extends JJBaseController { private String mColor = "#673AB7"; private int cx, cy, cr; private RectF mRectF, mRectF2; private float sign = 0.707f; private float tran = 120; public JJCircleToLineAlphaController() {
mRectF = new RectF();
mRectF2 = new RectF();
} @Override
public void draw(Canvas canvas, Paint paint) {
canvas.drawColor(Color.parseColor(mColor)); switch (mState) { case STATE_ANIM_NONE:
drawNormalView(paint, canvas); break; case STATE_ANIM_START:
drawStartAnimView(paint, canvas); break; case STATE_ANIM_STOP:
drawStopAnimView(paint, canvas); break;
}
} private void drawStopAnimView(Paint paint, Canvas canvas) {
canvas.save(); if (mPro > 0.7) {
paint.setAlpha((int) (mPro * 255));
drawNormalView(paint, canvas);
}
canvas.restore();
} private void drawStartAnimView(Paint paint, Canvas canvas) {
...
} private void drawNormalView(Paint paint, Canvas canvas) {
...
} @Override
public void startAnim() { if (mState == STATE_ANIM_START) return;
mState = STATE_ANIM_START;
startSearchViewAnim();
} @Override
public void resetAnim() { if (mState == STATE_ANIM_STOP) return;
mState = STATE_ANIM_STOP;
startSearchViewAnim();
}
}好像和第一个动画是一个套路是吗?是的,其实这些动画经过我们的分析,无非是两种或三种状态,再根据动画期间不断变换的mPro的值再做具体的动画就可以了,所以我们在具体看看这里的drawNormalView(Paint paint, Canvas canvas);方法和drawStartAnimView(Paint paint, Canvas canvas);方法的实现:
private void drawNormalView(Paint paint, Canvas canvas) { //圆的半径
cr = getWidth() / 50; //圆心x坐标
cx = getWidth() / 2; //圆心y坐标
cy = getHeight() / 2; //内圆所占的矩形区域
mRectF.left = cx - cr;
mRectF.right = cx + cr;
mRectF.top = cy - cr;
mRectF.bottom = cy + cr; //外圆所占的矩形局域
mRectF2.left = cx - 3 * cr;
mRectF2.right = cx + 3 * cr;
mRectF2.top = cy - 3 * cr;
mRectF2.bottom = cy + 3 * cr;
canvas.save();
paint.reset();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(4);
paint.setStyle(Paint.Style.STROKE);
canvas.rotate(45, cx, cy); //绘制放大镜把手
canvas.drawLine(cx + cr, cy, cx + cr * 2, cy, paint); //绘制内圆,也就是组成放大镜的圆
canvas.drawArc(mRectF, 0, 360, false, paint); //绘制外圆
canvas.drawArc(mRectF2, 0, 360, false, paint);
canvas.restore();
} private void drawStartAnimView(Paint paint, Canvas canvas) {
canvas.save(); //根据当前的mRectF来绘制放大镜的把手
canvas.drawLine(mRectF.left + cr + (cr * sign), mRectF.top + cr + (cr * sign),
mRectF.left + cr + (2 * cr * sign), mRectF.top + cr + (2 * cr * sign), paint); //绘制放大镜的圆
canvas.drawArc(mRectF, 0, 360, false, paint); //绘制外圆,由于mPro是从0-1不断增加,这里的绘制的角度就会不断变小,
//从而形成动画
canvas.drawArc(mRectF2, 90, -360 * (1 - mPro), false, paint); //当mPro 大于 0.7时开始绘制横线,会不断变长
if (mPro >= 0.7f) {
canvas.drawLine((1 - mPro + 0.7f) * (mRectF2.right - 3 * cr), mRectF2.bottom,
(mRectF2.right - 3 * cr), mRectF2.bottom, paint);
}
canvas.restore(); //tran表示平移的距离,同样会不断变化然后再给两个RectF赋值
mRectF.left = cx - cr + tran * mPro;
mRectF.right = cx + cr + tran * mPro;
mRectF2.left = cx - 3 * cr + tran * mPro;
mRectF2.right = cx + 3 * cr + tran * mPro;
}注释相当清晰,这里就不再解释了。看到了这里大家应该都已经明白了JJSearchViewAnim具体是如何实现的了,而且也已经掌握了该如何开发此类动画效果。但是我们学习这些动画最终是想要应用到项目中去的,那么拿刚刚这种动画来说,目前在项目中应该是无法使用的,那么我们怎么才能又快又简单的应用到项目中去呢?接下来我们就讲如何简单封装JJCircleToLineAlphaController并实际应用到项目中去。
5.项目应用
再拿出这张设计图。。。:
JJCircleToLineAlphaController
JJCircleToLineAlphaController实现的动画,看上出应该是本身是一个圆环加一个放大镜,当我们点击之后,然后执行动画,最终形成一个白色横线的输入框,当我们输入文字之后,点击搜索就应该可以进行搜索了。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章



