您的位置:首页 > 移动开发 > Android开发

Android 补间动画Animation的实用应用

2015-11-17 00:57 615 查看
转载请注明:

http://blog.csdn.net/sinat_30276961/article/details/49868409

上一篇,我总结了补间动画的各种特性,并写了个小的应用来使用这些特性。本篇,将在上篇的基础上,更进一步的使用Android的Animation,让你的应用不断炫起来~~

ok,闲话少说,开始正题~

先看一下效果:

第一个实例:



第二个实例:



先大致讲一下这两个实例用到了Animation的哪些特性。

第一个实例,我想大部分朋友都看到过了,最初是一个老外写的卫星菜单,然后有很多人尝试去实现,我也是其中一个。

这个实例中需要用到的特性是:

Animation中的透明度的渐变,平移和旋转。外加一个动画保持fillAfter。

第二个实例用到了Animation的渐变的最核心的特性,那就是值从0到1变化,并可以通过插值器进行过程控制。

接下来,我一个个来讲解下。

卫星菜单实现

要实现卫星菜单,有两种方案,一个是通过Android平台里的动画效果来实现,另一个是单纯的用画来实现。

这两个方案中,第二个方案是不可取的,虽然实现也不是很复杂,但是相比较第一种,还是挺复杂的。

第一个方案又能有两种实现方式:用补间动画和属性动画。

因为本篇是讲补间动画的应用,所以这里就用补间动画来实现。

这里,需要考虑的是补间动画虽然可以实现动画效果,但是view本身没有做相应的属性改变,所以,如果仅仅创建一组可动画的菜单view,然后动画到了最外围,去点击时是无效的。那怎么解决呢?

我们可以创建两组菜单view,一组负责动画,一组负责响应点击。这样一来,这两组view的位置就可以确定了:负责动画的view都放在最初始的地方;负责响应的view放在动画结束的地方。然后只要控制下view的可见不可见就可以了。

接着是动画效果的选择。

每个动画view在展开时,需要哪些动画呢?一个是自身旋转;一个是平移。然后插值器要选择OvershootInterpolator,达到一个稍微过头再回来的效果。动画view收回时,就是上述的反动画效果。注意,这里需要设置fillAfter=true,原因就不用我多说了吧。

然后是菜单点击选择时,动画view需要哪些动画呢?被点击的菜单view需要两个动画效果:一个是透明度渐变,因为最后要消失;一个是放大。没被选择的菜单view也需要两个:一个是透明度渐变,还有一个是缩小。注意,这里fillAfter=false,因为动画完之后,希望它们回到最初始的地方。

最后是控制打开关闭的菜单view需要的动画,很简单,旋转。

需要注意的就这些。

接下去开始实现:

先定义一些属性:

<resources>
<attr name="num" format="integer"/>
<attr name="radius" format="dimension"/>

<declare-styleable name="UseTweenAnimationView">
<attr name="num"/>
<attr name="radius"/>
</declare-styleable>
</resources>


这里,我定义了两个属性:菜单的数量;菜单的半径。当然你可以多定义一些,比方说动画时间等等。

然后是初始化:

public class UseTweenAnimationView extends ViewGroup {
/**
* mRadius = mMenuRadius *  RADIUS_TIME;
*/
private static final int RADIUS_TIME = 8;
/**
* 每个菜单的默认半径(dp)
*/
private static final int DEF_MENU_RADIUS = 25;
/**
* 动画时间
*/
private static final int DURATION = 1000;

private int mWidth;
private int mHeight;

/**
* 每个菜单的半径
*/
private int mMenuRadius;
/**
* 每个菜单相对于界面所在的半径
*/
private int mRadius;
/**
* 菜单数量
*/
private int mMenuNum;

/**
* 标记菜单打开关闭情况
*/
private boolean mMenuClosed = true;
/**
* 标记是否在动画中
*/
private boolean mIsAnimation = false;

public UseTweenAnimationView(Context context) {
this(context, null);
}

public UseTweenAnimationView(Context context, AttributeSet attrs) {
super(context, attrs);

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.UseTweenAnimationView);
mMenuNum = ta.getInteger(R.styleable.UseTweenAnimationView_num, 5);
mMenuRadius = ta.getDimensionPixelSize(R.styleable.UseTweenAnimationView_radius,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()));
mRadius = mMenuRadius * RADIUS_TIME;
ta.recycle();

// 控件的宽高留个菜单直径的余量
mWidth = mRadius + mMenuRadius * 4;
mHeight = mWidth;

// 根据数量,创建动画菜单,这里偷懒直接用textview
for (int i = 0; i < mMenuNum; i++) {
TextView menuView = new TextView(context);
menuView.setBackgroundResource(R.drawable.round_bg);
menuView.setText(String.valueOf(i + 1));
menuView.setTextColor(Color.WHITE);
menuView.setGravity(Gravity.CENTER);

LayoutParams menuViewLP = new LayoutParams(
mMenuRadius * 2,
mMenuRadius * 2
);
menuView.setLayoutParams(menuViewLP);
addView(menuView);
}
// 根据数量,创建用于点击响应菜
for (int i = 0; i < mMenuNum; i++) {
TextView menuView = new TextView(context);
menuView.setBackgroundResource(R.drawable.round_bg);
menuView.setText(String.valueOf(i + 1));
menuView.setTextColor(Color.WHITE);
menuView.setGravity(Gravity.CENTER);

LayoutParams menuViewLP = new LayoutParams(
mMenuRadius * 2,
mMenuRadius * 2
);
menuView.setLayoutParams(menuViewLP);
menuView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
clickMenuItem(v);
}
});
addView(menuView);
}

// 创建开关菜单
ImageView controlView = new ImageView(context);
controlView.setImageResource(R.drawable.add);
LayoutParams lp = new LayoutParams(
mMenuRadius * 2,
mMenuRadius * 2
);
controlView.setLayoutParams(lp);
controlView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (!mIsAnimation) {
mIsAnimation = true;
animControlView(v);
toggleMenu();
}
}
});
addView(controlView);
}


代码可能有点长,不过不复杂。继承自ViewGroup这无可厚非,因为要包含很多view。然后,定义了一些变量值,这里的mIsAnimation这个是为了防止用户在动画还没结束时就点击。

控件的宽和高是一样长的,都为mRadius加上两个菜单view的直径。多了一个菜单view直径的预留是为了给overshoot插值器和放大动画效果预留空间。

然后是先添加动画菜单,再添加响应菜单,最后添加控制开关菜单。这里可以看到都设置了LayoutParams。早加晚加都要加,干脆就放这里了。

菜单view点击响应的clickMenuItem和控制菜单的点击响应animControlView和toggleMenu后面会再贴出来。

ok,接下去是测量尺寸和放置控件

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
setMeasuredDimension(mWidth, mHeight);
}

/**
* 这里偷懒了,明确大小
*
* @param spec
* @param childDimension
* @return
*/
private int getChildMeasureSpec(int spec, int childDimension) {
return MeasureSpec.makeMeasureSpec(childDimension, MeasureSpec.EXACTLY);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
// 先放动画菜单
for (int i = 0; i < mMenuNum; i++) {
View child = getChildAt(i);
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
child.setVisibility(View.INVISIBLE);
}
// 再放用于响应的菜单
// 计算一个角度
final double angle = Math.PI / 2 / (mMenuNum - 1);
for (int i = 0; i < mMenuNum; i++) {
View child = getChildAt(i + mMenuNum);
int cl = 0;
int ct = 0;
if (i == 0) {
cl = 0;
ct = mRadius;
} else if (i == mMenuNum - 1) {
cl = mRadius;
ct = 0;
} else {
cl = (int) (mRadius * Math.sin(angle * i));
ct = (int) (mRadius * Math.cos(angle * i));
}
child.layout(cl, ct, cl + mMenuRadius * 2, ct + mMenuRadius * 2);
child.setVisibility(View.INVISIBLE);
}

// 最后放开关菜单
View controlView = getChildAt(mMenuNum * 2);
controlView.layout(0, 0, controlView.getMeasuredWidth(), controlView.getMeasuredHeight());
}
}


这里,测量尺寸时,我偷懒了,每个view长宽直接用固定值,也就是最初读入进来的半径*2。然后整个控件的宽高在上面有讲过,就是mRadius+view的直径*2,预留一个view的直径。

然后是放置view。负责动画的view的位置很简单,就是0,0点;负责响应的view的位置稍微麻烦点。需要通过sin,cos去计算一下它们的x和y点。

为了方便讲解,我贴张图:



view1和view5的坐标很好计算,它们的x点分别是0和mRadius,y点分别是mRadius和0。然后是view2,view3,view4,它们的x和y需要通过弧度去计算,先算出90度被分为4个一样大小的小角度a,然后view2的x就是sina*mRadius,view3的x是sin(2a)*mRadius,以此类推,应该不难理解。

ok,尺寸量好,位置放好,接着就是设置响应了。

在上面已经有贴出调用点,就是初始化那里。这里就贴出详细定义的方法:

private void animControlView(View v) {
RotateAnimation rotateAnimation = new RotateAnimation(0, 270,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(DURATION);
rotateAnimation.setFillAfter(true);
v.startAnimation(rotateAnimation);
}


上面就是控制菜单点击的动画效果。

接着是toggleMenu

private void toggleMenu() {
final double angle = Math.PI / 2 / (mMenuNum - 1);
for (int i = 0; i < mMenuNum; i++) {
final View animChild = getChildAt(i);
final View showChild = getChildAt(mMenuNum+i);
int cl = 0;
int ct = 0;
if (i == 0) {
cl = 0;
ct = mRadius;
} else if (i == mMenuNum - 1) {
cl = mRadius;
ct = 0;
} else {
cl = (int) (mRadius * Math.sin(angle * i));
ct = (int) (mRadius * Math.cos(angle * i));
}
animChild.setVisibility(View.VISIBLE);
if (!mMenuClosed) {
showChild.setVisibility(View.INVISIBLE);
}
AnimationSet animationSet = new AnimationSet(true);
animationSet.setInterpolator(new OvershootInterpolator(2f));

TranslateAnimation translateAnimation = null;
RotateAnimation rotateAnimation = null;

if (mMenuClosed) {
translateAnimation = new TranslateAnimation(0, cl, 0, ct);
rotateAnimation = new RotateAnimation(0, 1440,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
} else {
translateAnimation = new TranslateAnimation(cl, 0, ct, 0);
rotateAnimation = new RotateAnimation(1440, 0,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
}
translateAnimation.setFillAfter(true);
rotateAnimation.setFillAfter(true);
// 设置一些延迟时间
translateAnimation.setStartOffset((i * 100) / mMenuNum);
rotateAnimation.setStartOffset((i * 100) / mMenuNum);

animationSet.addAnimation(rotateAnimation);
animationSet.addAnimation(translateAnimation);
animationSet.setDuration(DURATION);

final int position = i;
animationSet.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {

}
@Override
public void onAnimationEnd(Animation animation) {
if (mMenuClosed) {
animChild.setVisibility(View.INVISIBLE);
showChild.setVisibility(View.VISIBLE);
if (position == mMenuNum - 1) {
mIsAnimation = false;
mMenuClosed = false;
}
} else {
animChild.setVisibility(View.INVISIBLE);
showChild.setVisibility(View.INVISIBLE);
if (position == mMenuNum - 1) {
mIsAnimation = false;
mMenuClosed = true;
}
}
}
@Override
public void onAnimationRepeat(Animation animation) {

}
});
animChild.startAnimation(animationSet);
}
}


这里,要先计算出动画到哪个位置。当然,你也可以在onLayout那里把坐标保存下来,然后这里直接使用。确定好动画的路径,接着就是创建组合动画效果:旋转加平移。这里添加的顺序很重要:先添加旋转,再添加平移,至于为什么,你可以试试。

然后可以设置一些延迟,让菜单响应有个先后的感觉。然后监听一下动画,获取到动画结束回调,然后做处理。这里可能有朋友有疑问,为啥每个动画都做监听,给最后一个设置监听不就行了,因为最后一个结束了,也就意味着全部结束了。没错,这样思路是对的,不过,这样会有一个不好的效果,为啥呢?因为我在前面加了延时动画,如果给最后一个设置监听,然后在动画结束得到回调时再去遍历每个view,去隐藏,就会看起来不自然,所以只能给每个做动画监听,每次结束就马上把当前的动画view隐藏起来。至于为啥要隐藏,应该不用我说吧。

ok,最后就是点击菜单时的动画效果了:

private void clickMenuItem(final View v) {
AnimationSet animationSet = new AnimationSet(true);
animationSet.setInterpolator(new AccelerateInterpolator(1));
animationSet.setDuration(DURATION/2);

ScaleAnimation scaleAnimation = new ScaleAnimation(1, 2, 1, 2,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(alphaAnimation);
animationSet.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
v.setVisibility(View.INVISIBLE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
v.startAnimation(animationSet);

for (int i = 0; i < mMenuNum; i++) {
final View showChild = getChildAt(i+mMenuNum);
if (showChild != v) {
AnimationSet as = new AnimationSet(true);
as.setInterpolator(new AccelerateInterpolator(3));
as.setDuration(DURATION/2);

ScaleAnimation sa = new ScaleAnimation(1, 0, 1, 0,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
AlphaAnimation aa = new AlphaAnimation(1, 0);
as.addAnimation(sa);
as.addAnimation(aa);
as.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
showChild.setVisibility(View.INVISIBLE);
mMenuClosed = true;
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
showChild.startAnimation(as);
}
}
}


照例,使用组合动画,点击的view添加放大和透明的动画;其余view添加缩小和透明的动画。

至此,一个鲜活的卫星菜单就完成了,这样一路下来不难吧。

闪亮的文字

接着,我来讲讲闪亮的文字怎么实现。

这个的实现需要用到的核心技术是LinearGradient。什么是LinearGradient,就是线性渐变,通俗点讲,就是可以设置颜色的线性变化。比方说,我起点设置白色,终点设置黑色,然后设置到一个空图上,就会出现从白色渐变到黑色的颜色。

那为啥这个会和animation扯到一起呢?其实,这只是我的心血来潮。当然,你也可以不用animation。

既然扯到animation,那就讲一下怎么把Animation使用进去。大家都知道,Animation它的值变化过程是从0到1或者从1到0,然后插值器控制从0到1的变化过程。既然如此,那么我们就可以利用这0到1的自动产生值的特性,用比例去放大它,就能控制很多方式了。

不懂?我举个例子,我现在要实现月亮绕地球旋转的效果。我有很多种方案去实现月亮绕行的速度,有一个办法就是用animation。首先,我要知道月亮绕地球的轨迹的长度,然后通过animation从0到1变化的特性,把这个值再乘以月亮绕行轨迹长度,不就可以不用计算速度了吗。因为我给animation安个线性插值器,它的比例自然是线性增长,也就是匀速了。最妙的是,我觉得匀速缺乏美感,那安个OverShoot..啥的,你想咋地就咋地,不用你自己去设定这个变化过程,animation本身会提供给你值,多妙哉!

ok,闲话就说到这,我们来实现。

public class ShiningTextView extends TextView{
/**
* 可以加入插值器,简单实现更多特效
*/
private Animation mAnimation;
/**
* 线性颜色变化控制类
*/
private LinearGradient mLinearGradient;
private Matrix mGradientMatrix;

private int mWidth = 0;
private int mHeight = 0;

private float mOffset = 0;

public ShiningTextView(Context context) {
this(context, null);
}

public ShiningTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mWidth == 0) {
mWidth = getMeasuredWidth();
if (mWidth > 0) {
final Paint paint = getPaint();
mGradientMatrix = new Matrix();
mLinearGradient = new LinearGradient(-mWidth, 0, 0, 0,
new int[] {0x33ffffff, 0xffffffff, 0x33ffffff}, new float[] {0, 0.5f, 1}, Shader.TileMode.CLAMP);
paint.setShader(mLinearGradient);
}
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mGradientMatrix.setTranslate(mOffset, 0);
mLinearGradient.setLocalMatrix(mGradientMatrix);
}

public void startShining(Interpolator interpolator, int duration, int repeatCount) {
mAnimation = new Animation() {

@Override
protected void applyTransformation(float interpolatedTime,
Transformation t) {
super.applyTransformation(interpolatedTime, t);
mOffset = interpolatedTime*mWidth+mWidth;
invalidate();
}
};
mAnimation.setAnimationListener(new AnimationListener() {

@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
getPaint().setShader(null);
}
});
mAnimation.setDuration(duration);
mAnimation.setInterpolator(interpolator);
mAnimation.setRepeatCount(repeatCount);
mAnimation.setRepeatMode(Animation.RESTART);
startAnimation(mAnimation);
}
}


代码不多,我就直接贴出来了。

重点就是applyTransformation的应用。它传进来的interpolatedTime是从0到1变化的,这个变化在你设置的duration时间里,变化过程由interpolator插值器决定,很简单明了吧。

至此,这两个实例就讲到这。

感兴趣的朋友可以下载源码(用Android studio创建的):代码入口
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息