项目地址:Github
动画效果:
-
清单文件
<manifest package="cn.lry.animation" xmlns:android="http://schemas.android.com/apk/res/android"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".guide.MainActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".guide.GuideActivity"/> </application> </manifest>
主要分为连个页面,一个是墨迹天气的引导页面,就是GIF中涨势的效果,另外一个也是差不多的动画效果的只不过是左右滑动的引导页面
-
主页面布局MainActivity,主页面主要有一个viewpager构成,滑动方式为vertical
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <cn.lry.animation.viewpager.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" app:orientation="vertical"/> </LinearLayout>
-
ViewPager的初始化,通过xml文件来初始化每个页面的视图,然后通过pagers.add(view1)添加到ViewPager中,总共有四个
View view1 = LayoutInflater.from(this).inflate(R.layout.layout_tutorial_1, null); t1_icon1= (ImageView) view1.findViewById(R.id.t1_icon1); t1_icon2 = (ImageView) view1.findViewById(R.id.t1_icon2); t1_fixed = (ImageView) view1.findViewById(R.id.t1_fixed); t1_next = (ImageView) view1.findViewById(R.id.t1_next); pagers.add(view1); .........四个类似的页面
-
我们看其中一个页面的布局R.layout.layout_tutorial_1,也就是将页面上的几个图片分别用ImageView来进行布局分布。每个图片都是一个png的图片
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@android:color/white" android:gravity="center" android:orientation="vertical" > <ImageView android:id="@+id/t1_fixed" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="15.0dip" android:src="@mipmap/tutorial1_fixed" /> <RelativeLayout android:id="@+id/center_layout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" > <ImageView android:id="@+id/t1_icon1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="180.0dip" android:layout_marginTop="53.0dip" android:background="@drawable/t1_frame_animation" /> <ImageView android:id="@+id/t1_icon2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/t1_icon1" android:layout_centerHorizontal="true" android:layout_marginTop="30.0dip" android:src="@mipmap/tutorial1_icon3" android:visibility="visible" /> <ImageView android:id="@+id/t1_static" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:src="@mipmap/tutorial1_static" android:visibility="visible" /> <ImageView android:id="@+id/t1_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/t1_static" android:layout_centerHorizontal="true" android:layout_marginTop="10.0dip" android:src="@mipmap/tutorial1_text" android:visibility="visible" /> </RelativeLayout> <ImageView android:id="@+id/t1_next" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20.0dip" android:src="@mipmap/tutorial_next" /> </LinearLayout>
-
动画的实现主要靠private void animal(int position) 方法,在页面切换的时候,去启动或者禁用视图控件的动画
-
极低耗电 这几个字的放大缩小动画
极低耗电为一个图片 t1_fixed = (ImageView) view1.findViewById(R.id.t1_fixed);
<ImageView android:id="@+id/t1_fixed" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="15.0dip" android:src="@mipmap/tutorial1_fixed" />
顶部动画加载
animationTop = AnimationUtils.loadAnimation(MainActivity.this, R.anim.tutorail_scalate_top);
启动动画
t1_fixed.startAnimation(animationTop);
主要分析这个动画的描述R.anim.tutorail_scalate_top,主要定义了两个动画效果scale,放大缩小,动画时长1000ms,x,y从1.0变为1.5倍大,重复模式reverse为倒序回复,如果设置为restart的话,表示从头回放,repeatcount负一表示无限循环;另外一个是alpha,时间也是1000,这样就是一边放大缩小,一边透明度渐变,透明度从1变为0.5
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <scale android:duration="1000" android:fromXScale="1.0" android:fromYScale="1.0" android:interpolator="@android:anim/decelerate_interpolator" android:pivotX="50%" android:pivotY="50%" android:toXScale="1.5" android:toYScale="1.5" android:repeatMode="reverse" android:repeatCount="-1"/> <alpha android:fromAlpha="1.0" android:toAlpha="0.5" android:duration="1000" android:repeatMode="reverse" android:repeatCount="-1"/> </set> <!-- 尺寸伸缩动画效果 scale 属性:interpolator 指定一个动画的插入器 常见动画插入器: accelerate_decelerate_interpolator 加速-减速 动画插入器 accelerate_interpolator 加速-动画插入器 decelerate_interpolator 减速- 动画插入器 anticipate_interpolator 先回退一小步然后加速前进 anticipate_overshoot_interpolator 在上一个基础上超出终点一小步再回到终点 bounce_interpolator 最后阶段弹球效果 cycle_interpolator 周期运动 linear_interpolator 匀速 overshoot_interpolator 快速到达终点并超出一小步最后回到终点 浮点型值: fromXScale 属性为动画起始时 X坐标上的伸缩尺寸 toXScale 属性为动画结束时 X坐标上的伸缩尺寸 fromYScale 属性为动画起始时Y坐标上的伸缩尺寸 toYScale 属性为动画结束时Y坐标上的伸缩尺寸 说明: 以上四种属性值 0.0表示收缩到没有 1.0表示正常无伸缩 值小于1.0表示收缩 值大于1.0表示放大 pivotX 属性为动画相对于物件的X坐标的开始位置 pivotY 属性为动画相对于物件的Y坐标的开始位置 说明: 以上两个属性值 从0%-100%中取值 50%为物件的X或Y方向坐标上的中点位置 长整型值: duration 属性为动画持续时间 说明: 时间以毫秒为单位 布尔型值: fillAfter 属性 当设置为true ,该动画转化在动画结束后被应用 -->
-
中间电池图标的旋转动画
电池图标
<ImageView android:id="@+id/t1_icon2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/t1_icon1" android:layout_centerHorizontal="true" android:layout_marginTop="30.0dip" android:src="@mipmap/tutorial1_icon3" android:visibility="visible" />
加载并开始动画
Animation animation1 = AnimationUtils.loadAnimation(MainActivity.this, R.anim.tutorail_rotate); LinearInterpolator lin = new LinearInterpolator(); animation1.setInterpolator(lin); t1_icon2.setVisibility(View.VISIBLE); t1_icon2.startAnimation(animation1);
关于旋转动画的描述,刚开始的时候,电池图标是从无到有的一个从小到大的scale,android:fromXScale="0.0"到1.2,然后就结束了,同时会无限的旋转,rotate,从0度到359度旋转,时间为3000ms,pivotX,Y 50%表示从以中心点旋转
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" > <scale android:duration="800" android:fillAfter="false" android:fromXScale="0.0" android:fromYScale="0.0" android:interpolator="@android:anim/accelerate_decelerate_interpolator" android:pivotX="50%" android:pivotY="50%" android:toXScale="1.2" android:toYScale="1.2" /> <rotate android:duration="3000" android:fromDegrees="0" android:pivotX="50%" android:pivotY="50%" android:repeatCount="-1" android:toDegrees="359.0" /> <!-- rotate 旋转动画效果 属性:interpolator 指定一个动画的插入器 浮点数型值: fromDegrees 属性为动画起始时物件的角度 toDegrees 属性为动画结束时物件旋转的角度 可以大于360度 说明: 当角度为负数——表示逆时针旋转 当角度为正数——表示顺时针旋转 (负数from——to正数:顺时针旋转) (负数from——to负数:逆时针旋转) (正数from——to正数:顺时针旋转) (正数from——to负数:逆时针旋转) pivotX 属性为动画相对于物件的X坐标的开始位置 pivotY 属性为动画相对于物件的Y坐标的开始位置 说明: 以上两个属性值 从0%-100%中取值 50%为物件的X或Y方向坐标上的中点位置 长整型值: duration 属性为动画持续时间 说明: 时间以毫秒为单位 --> </set>
-
电池图片数字的闪烁变换
闪烁的图片
<ImageView android:id="@+id/t1_icon1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="180.0dip" android:layout_marginTop="53.0dip" android:background="@drawable/t1_frame_animation" />
图片定义t1_frame_animation.xml,animation-list实现的是逐帧动画,oneshot表示不停的循环播放,每个帧的图片和时长定义
<?xml version="1.0" encoding="utf-8"?> <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false" > <!-- 根标签为animation-list,其中oneshot代表着是否只展示一遍,设置为false会不停的循环播放动画 根标签下,通过item标签对动画中的每一个图片进行声明 android:duration 表示展示所用的该图片的时间长度 --> <item android:drawable="@mipmap/tutorial1_icon1" android:duration="200"/> <item android:drawable="@mipmap/tutorial1_icon2" android:duration="200"/> </animation-list>
开始启动动画
t1_icon1.setImageResource(R.drawable.t1_frame_animation); t1_icon1_animationDrawable = (AnimationDrawable) t1_icon1.getDrawable(); t1_icon1_animationDrawable.start();
-
最下角箭头下移和透明度变换
底部箭头
<ImageView android:id="@+id/t1_next" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20.0dip" android:src="@mipmap/tutorial_next" />
加载动画
animationBottom = AnimationUtils.loadAnimation(MainActivity.this, R.anim.tutorail_bottom);
启动
t1_next.startAnimation(animationBottom);
动画描述文件tutorail_bottom.xml,转移动画和透明度的组合
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:repeatMode="reverse" > <translate android:duration="1000" android:fromXDelta="0" android:fromYDelta="-15" android:repeatCount="infinite" android:toXDelta="0" android:toYDelta="20" /> <!-- translate 平移动画效果 整型值: fromXDelta 属性为动画起始时 X坐标上的位置 toXDelta 属性为动画结束时 X坐标上的位置 fromYDelta 属性为动画起始时 Y坐标上的位置 toYDelta 属性为动画结束时 Y坐标上的位置 注意: 没有指定fromXType toXType fromYType toYType 时候, 默认是以自己为相对参照物 ,默认参考物最重要 长整型值: duration 属性为动画持续时间 说明: 时间以毫秒为单位 --> <alpha android:duration="1000" android:fromAlpha="1.0" android:repeatCount="infinite" android:toAlpha="0.3" /> <!-- 透明度控制动画效果 alpha 浮点型值: fromAlpha 属性为动画起始时透明度 toAlpha 属性为动画结束时透明度 说明: 0.0表示完全透明 1.0表示完全不透明 以上值取0.0-1.0之间的float数据类型的数字 长整型值: duration 属性为动画持续时间 说明: 时间以毫秒为单位 整型值: repeatCount:重复次数 说明: infinite:循环执行, 具体正整数表示循环次数 repeatMode:重复模式, 说明: restart:重新从头开始执行 reverse:反方向执行 --> </set>
其他页面的也都差不多原理实现
可以看一下火箭飞后面云朵的动画
每个云朵都是一个image view
<ImageView
android:id="@+id/t3_icon3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="120.0dip"
android:layout_marginTop="45.0dip"
android:src="@mipmap/t3_could_left2"
android:visibility="invisible"/>
启动动画
t3_icon2.startAnimation(transAnimation2);
动画定义
final TranslateAnimation transAnimation2 = new TranslateAnimation(fx1, tx1, fy1, ty1);
transAnimation2.setDuration(800);
transAnimation2.setRepeatCount(Animation.INFINITE);
transAnimation2.setRepeatMode(Animation.RESTART);
lin = new LinearInterpolator();
transAnimation2.setInterpolator(lin);
final TranslateAnimation transAnimation3 = new TranslateAnimation(fx2, tx2, fy2, ty2);
transAnimation3.setDuration(1200);
transAnimation3.setRepeatCount(Animation.INFINITE);
transAnimation3.setRepeatMode(Animation.RESTART);
transAnimation3.setInterpolator(lin);
每个云朵的移动速度不一样,其实位置fx1,fy1到目标位置tx1,ty1也不一样
挂牌左右摇晃动画
挂牌
<ImageView
android:id="@+id/t4_icon1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/t4_image"
android:visibility="visible" />
左右摇晃动画
int pivot = Animation.RELATIVE_TO_SELF;
CycleInterpolator interpolator = new CycleInterpolator(3.0f);//执行多少次
RotateAnimation animation = new RotateAnimation(0, 10, pivot, 0.47f, pivot, 0.05f);
animation.setStartOffset(500);
animation.setDuration(3000);
animation.setRepeatCount(1);// Animation.INFINITE
animation.setInterpolator(interpolator);
t4_icon1.startAnimation(animation);
其他基本都是类似的实现:
以上动画使用了Android的基本动画类型alpha,rotate,scale,translate,另外引入插值器完成动画
下面火箭的上升有一个自定义属性动画,我们现认识一下,后续在细细学习分析
火箭view
<ImageView
android:id="@+id/page_one_rocket"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@mipmap/image_one_rocket"
android:layout_below="@+id/page_one_center_bg"
android:layout_toLeftOf="@+id/center"
android:layout_marginRight="30dp"/>
加载动画
animationRocket = ArcAnimator.createArcAnimator(page_one_rocket, centerX + r - x*3/2 + 20, centerY - r + 3*x/4 - 20 , 90f, Side.RIGHT);
animationRocket.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
page_one_center_tj.startAnimation(animationCenterTJ);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animationRocket.setDuration(1500);
animationRocket.start();
其中 ArcAnimator是自定义的属性动画
package cn.lry.animation.arcanimator;
import android.view.View;
import android.view.animation.Interpolator;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;
import java.lang.ref.WeakReference;
public class ArcAnimator extends Animator {
public static ArcAnimator createArcAnimator(View clipView, View nestView, float degree, Side side){
return createArcAnimator(clipView, Utils.centerX(nestView), Utils.centerY(nestView), degree, side);
}
public static ArcAnimator createArcAnimator(View clipView, float endX, float endY, float degree, Side side){
ArcMetric arcMetric = ArcMetric.evaluate(Utils.centerX(clipView), Utils.centerY(clipView), endX, endY, degree, side);
return new ArcAnimator(arcMetric, clipView);
}
ArcMetric mArcMetric;
WeakReference<View> mTarget;
WeakReference<ObjectAnimator> mAnimator;
float mValue;
private ArcAnimator(ArcMetric arcmetric, View target) {
mArcMetric = arcmetric;
mTarget = new WeakReference<>(target);
mAnimator = new WeakReference<>(
ObjectAnimator.ofFloat(
ArcAnimator.this, // target
"degree", // property
arcmetric.getStartDegree(),
arcmetric.getEndDegree())
);//其实主要就是一个ObjectAnimator,关注的是degree的值
}
void setDegree(float degree){
mValue = degree;
View clipView = mTarget.get();
float x = mArcMetric.getAxisPoint().x + mArcMetric.mRadius * Utils.cos(degree);
float y = mArcMetric.getAxisPoint().y - mArcMetric.mRadius * Utils.sin(degree);
ViewHelper.setX(clipView,x - clipView.getWidth() / 2);
ViewHelper.setY(clipView,y - clipView.getHeight() / 2);
}
float getDegree(){
return mValue;
}
@Override
public long getStartDelay() {
Animator a = mAnimator.get();
return a == null ? 0 : a.getDuration();
}
@Override
public void setStartDelay(long startDelay) {
Animator a = mAnimator.get();
if(a != null) a.setStartDelay(startDelay);
}
@Override
public ArcAnimator setDuration(long duration) {
Animator a = mAnimator.get();
if(a != null) a.setDuration(duration);
return this;
}
@Override
public long getDuration() {
Animator a = mAnimator.get();
return a == null ? 0 : a.getDuration();
}
@Override
public void setInterpolator(Interpolator value) {
Animator a = mAnimator.get();
if(a != null) a.setInterpolator(value);
}
@Override
public void start() {
super.start();
Animator a = mAnimator.get();
if(a != null) a.start();
}
@Override
public void end() {
super.end();
Animator a = mAnimator.get();
if(a != null) a.end();
}
@Override
public void cancel() {
super.cancel();
Animator a = mAnimator.get();
if(a != null) a.cancel();
}
@Override
public void addListener(AnimatorListener listener) {
Animator a = mAnimator.get();
if(a != null) a.addListener(listener);
}
@Override
public void setupEndValues() {
super.setupEndValues();
Animator a = mAnimator.get();
if(a != null) a.setupEndValues();
}
@Override
public void setupStartValues() {
super.setupStartValues();
Animator a = mAnimator.get();
if(a != null) a.setupStartValues();
}
@Override
public boolean isRunning() {
Animator a = mAnimator.get();
return a != null && a.isRunning();
}
@Override
public String toString() {
return mArcMetric.toString();
}
}
关注一下
ArcAnimator.createArcAnimator(page_one_rocket, centerX + r - x*3/2 + 20, centerY - r + 3*x/4 - 20 , 90f, Side.RIGHT);
centerX + r - x*3/2 + 20
和centerY - r + 3*x/4 - 20
是计算出来火箭图标的最终目标位置的x,y坐标
看几条路线轨迹
01-10 04:57:03.822 11717-11717/cn.lry.animation I/GuideActivity: r=270.0中间背景的长宽
01-10 04:57:03.822 11717-11717/cn.lry.animation I/GuideActivity: x=120.0火箭图标的长宽
这两个只是用来算目标位置的坐标
01-10 04:57:03.832 11717-11717/cn.lry.animation I/ArcAnimator: createArcAnimator endx=349.0 endy=217.0degree90.0side =RIGHT
计算出目标位置的坐标为349,217
01-10 04:57:03.845 11717-11717/cn.lry.animation I/ArcAnimator: createArcAnimator starx=74.0 starty=381.0
当前位置的坐标,最后还进行了中心计算
01-10 04:57:03.884 11717-11717/cn.lry.animation I/ArcAnimator: setDegree=0.0
01-10 04:57:03.885 11717-11717/cn.lry.animation I/ArcAnimator: x=134.00002===ArcM.x + ArcM.r * cos(-88.82553): 129.5 + 219.54613 * 0.020496916
01-10 04:57:03.886 11717-11717/cn.lry.animation I/ArcAnimator: y=441.0===ArcM.y + ArcM.r * sin(-88.82553): 221.5 + 219.54613 * -0.9997899
01-10 04:57:03.887 11717-11717/cn.lry.animation I/ArcAnimator: setviewX=74.000015
01-10 04:57:03.887 11717-11717/cn.lry.animation I/ArcAnimator: setviewy=381.0
第一次偏移,当前
01-10 04:57:03.917 11717-11717/cn.lry.animation I/ArcAnimator: setDegree=-88.82553
01-10 04:57:03.919 11717-11717/cn.lry.animation I/ArcAnimator: x=134.00002===ArcM.x + ArcM.r * cos(-88.82553): 129.5 + 219.54613 * 0.020496916
01-10 04:57:03.920 11717-11717/cn.lry.animation I/ArcAnimator: y=441.0===ArcM.y + ArcM.r * sin(-88.82553): 221.5 + 219.54613 * -0.9997899
01-10 04:57:03.920 11717-11717/cn.lry.animation I/ArcAnimator: setviewX=74.000015
01-10 04:57:03.920 11717-11717/cn.lry.animation I/ArcAnimator: setviewy=381.0
01-10 04:57:03.965 11717-11717/cn.lry.animation I/ArcAnimator: setDegree=-88.82553
01-10 04:57:03.966 11717-11717/cn.lry.animation I/ArcAnimator: x=134.83453===ArcM.x + ArcM.r * cos(-88.60769): 129.5 + 219.54613 * 0.024298022
01-10 04:57:03.967 11717-11717/cn.lry.animation I/ArcAnimator: y=440.98132===ArcM.y + ArcM.r * sin(-88.60769): 221.5 + 219.54613 * -0.9997048
轨迹的示意图大概如下,90度是一个四分之一圆弧,180度就是一个半圆弧