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实现的动画,看上出应该是本身是一个圆环加一个放大镜,当我们点击之后,然后执行动画,最终形成一个白色横线的输入框,当我们输入文字之后,点击搜索就应该可以进行搜索了。
这里我提供一个比较简单的实现思路:就是我们自己实现一个布局将EditText和这个JJSearchView叠加放置,这里要注意EditText的宽度要比JJSearchView要短,最好是一个放大镜的宽度。首先隐藏EditText,点击JJSearchView执行动画,然后显示EditText再点击搜索按钮时,这时候我们通过JJSearchView的状态来判断是否需要搜索.这样就简单的完成了一个带动画的SearchView实现了.下面我们大致贴出具体的实现,大家也可以到这里看看:地址
1.首先是布局文件
<?xml version="1.0" encoding="utf-8"?><merge xmlns:android="http://schemas.android.com/apk/res/android"> <com.cjj.sva.JJSearchView android:id="@+id/search_view" android:layout_width="200dp" android:layout_height="60dp" android:layout_centerInParent="true"/> <EditText android:id="@+id/edit_text" android:layout_width="150dp" android:layout_height="50dp" android:layout_alignParentLeft="true" android:background="@null" android:layout_centerInParent="true" android:textCursorDrawable="@null" android:textColor="@android:color/white" android:textSize="14sp" android:singleLine="true" android:visibility="invisible" android:layout_marginLeft="12dp"/></merge>
2.CircleSearchView的具体实现
public class CircleSearchView extends RelativeLayout { private Context mContext; private JJSearchView mSearchView; private EditText mEditText; public CircleSearchView(Context context) { this(context, null);
} public CircleSearchView(Context context, AttributeSet attrs) { this(context, attrs, 0);
} public CircleSearchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);
initLayout(context);
} private void initLayout(Context context) { this.mContext = context;
LayoutInflater.from(mContext).inflate(R.layout.view_circle_search, this);
mSearchView = (JJSearchView) findViewById(R.id.search_view);
mSearchView.setController(new JJCircleToLineAlphaController());
mEditText = (EditText) findViewById(R.id.edit_text);
mSearchView.setOnClickListener(new OnClickListener() { @Override
public void onClick(View v) { if (mSearchView.getState() == JJBaseController.STATE_ANIM_NONE) {
mSearchView.startAnim();
mEditText.setVisibility(View.VISIBLE);
mEditText.bringToFront();
} else if (mSearchView.getState() == JJBaseController.STATE_ANIM_START) {
Toast.makeText(mContext, "正在搜索", Toast.LENGTH_LONG).show();
}
}
});
}
}主要看onClick(View v);方法中的实现。
2.JJCircleToLineAlphaController的修改
要想很好的实现平移的距离以及最终横线的长度,需要修改一些JJCircleToLineAlphaController中的方法
[代码]java代码:
package com.cjj.sva.anim.controller;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import com.cjj.sva.anim.JJBaseController;
/**
* 这是一个神奇的类,searchview释放外面的圈圈,呼吸新鲜空气!
*
* Created by androidcjj on 2016/4/2.
*/
public class JJCircleToLineAlphaController extends JJBaseController {
private String mColor = "#673AB7";
private int cx, cy, cr;
private RectF mRectF, mRectFLarge;
private float tran = 200;
private static int mGap = 3;
public JJCircleToLineAlphaController() {
mRectF = new RectF();
mRectFLarge = 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.2) {
paint.setAlpha((int) (mPro * 255));
drawNormalView(paint, canvas);
}
canvas.restore();
}
private void drawStartAnimView(Paint paint, Canvas canvas) {
canvas.save();
canvas.rotate(45, mRectF.centerX(), mRectF.centerY());
canvas.drawLine(mRectF.centerX() + cr, mRectF.centerY(), mRectF.centerX() + (cr * 2), mRectF.centerY(), paint);
canvas.restore();
canvas.drawArc(mRectF, 0, 360, false, paint);
canvas.drawArc(mRectFLarge, 90, -360 * (1 - mPro), false, paint);
if (mPro >= 0.4f && mPro < 1.0f) {
canvas.drawLine(mRectFLarge.left * (1 - mPro) + mRectF.width(), mRectFLarge.bottom,
(mRectFLarge.right - mGap * cr), mRectFLarge.bottom, paint);
}
if (mPro == 1.0f) {
canvas.drawLine(mRectFLarge.left * (1 - mPro) + mRectF.width(), mRectFLarge.bottom,
mRectF.left, mRectFLarge.bottom, paint);
}
mRectF.left = cx - cr + tran * mPro;
mRectF.right = cx + cr + tran * mPro;
mRectFLarge.left = cx - mGap * cr + tran * mPro;
mRectFLarge.right = cx + mGap * cr + tran * mPro;
}
private void drawNormalView(Paint paint, Canvas canvas) {
cr = getWidth() / 30;
cx = getWidth() / 2;
cy = getHeight() / 2;
mRectF.left = cx - cr;
mRectF.right = cx + cr;
mRectF.top = cy - cr;
mRectF.bottom = cy + cr;
mRectFLarge.left = cx - mGap * cr;
mRectFLarge.right = cx + mGap * cr;
mRectFLarge.top = cy - mGap * cr;
mRectFLarge.bottom = cy + mGap * 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(mRectFLarge, 0, 360, false, paint);
canvas.restore();
}
@Override
public void startAnim() {
if (mState == STATE_ANIM_START) return;
mState = STATE_ANIM_START;
startSearchViewAnim();
tran = getWidth() / 2 - mRectF.width() * 2;
}
@Override
public void recetAnim() {
if (mState == STATE_ANIM_STOP) return;
mState = STATE_ANIM_STOP;
startSearchViewAnim();
}
}
6.个人评价
JJSearchViewAnim实现了多种酷炫的搜索动画,我们不仅能从项目里学到大量的动画相关的用法,更能学到如何去分解和思考一个动画的实现。是一个非常值得我们学习的项目。不过项目中可能有一些数字或者变量没有做注释,有可能会影响代码的阅读,不过CJJ同学已经着手开始优化了,很快就会更新。
另外关于实现的这些效果并没有办法直接在项目中使用的问题,CJJ同学的初衷是想让大家从项目中学习动画实现的思路与技巧,修改之后放到自己的项目中去而不是做一个伸手党总想直接使用最好。在修改的过程中更能提升自己的编程能力。这点我很赞同CJJ(其实我们就是懒=。=)。