本文目录
一、属性动画和MaterialDesign动画
属性动画和MaterialDesign动画相关的内容,已经在前面写过,具体查看https://www.jianshu.com/p/15d25638c001(属性动画)https://www.jianshu.com/p/a8f1f36c3fe0(MaterialDesign动画)
二、Android自定义动画框架
前面的部分,是动画的理论部分,这一部分,是动画的应用部分。
无论是封装一个自己的动画框架,还是封装一个自定义View,都需要遵从封装的原则:便于使用和复用,便于配置。
今天来做这样一个效果:
闪屏动画
略卡,是因为模拟器不好滑动的原因,在真机上的效果是非常流畅的,有点丑,个人能提供的设计有限,但跟那些绚丽的动画原理是一样的。
要封装成一个可以复用,便于配置的动画框架,如果只在xml文件中配置每个控件在X轴和Y轴上移动的加速度,就非常的方便。那么问题来了,我们在系统的控件上添加自定义属性,系统控件是无法识别的,但是如果在系统控件外包裹一层自定义的容器,让这个自定义的容器去识别这些自定义属性,这个问题就解决了。
那么我们现在就开始一步步的实现这样的效果。
1.分析页面,发现页面其实就是一个ViewPager,这里定义了一个ParallaxContainer继承自FrameLayout,在其中使用addView添加了一个ViewPager。其实也可以直接在Activity中写一个ViewPager,效果是一样的。
ParallaxContainer的代码如下
/**
* Created by kimliu on 2018/12/25
* 引导页最外层布局,里面有一个ViewPager
*/
public class ParallaxContainer extends FrameLayout implements ViewPager.OnPageChangeListener{
private ArrayList<ParallaxFragment> fragments;
private ParallaxPagerAdapter adapter;
private float containerWidth;
private ImageView iv_main;
public ViewPagerScrollListener mListener;
private ViewPagerScrollListener listener;
private void setmListener(ViewPagerScrollListener mListener){
this.mListener = mListener;
}
public ParallaxContainer(@NonNull Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 初始化设置
* @param childIds
*/
public void setUp(int...childIds){
fragments = new ArrayList<>();
for (int i = 0 ; i < childIds.length; i++){
ParallaxFragment fragment = new ParallaxFragment();
Bundle bundle = new Bundle();
bundle.putInt("index",i);
bundle.putInt("layoutId",childIds[I]);
fragment.setArguments(bundle);
fragments.add(fragment);
}
SplashActivity splashActivity = (SplashActivity) getContext();
adapter = new ParallaxPagerAdapter(splashActivity.getSupportFragmentManager(),
fragments);
ViewPager viewPager = new ViewPager(getContext());
viewPager.setId(R.id.parallax_pager);
viewPager.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
viewPager.setAdapter(adapter);
addView(viewPager);
viewPager.addOnPageChangeListener(this);
}
/**
*
* @param position 位置
* @param positionOffset 移动偏移量
* @param positionOffsetPixels 移动偏移量像素值
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
containerWidth = getWidth();
//进入的页面
ParallaxFragment inFragment = null;
try {
inFragment = fragments.get(position - 1);
}catch (Exception e){
//如果报错,那么inFragment为null,下面的ifelse就不会走
}
ParallaxFragment outFragment = null;
try {
outFragment = fragments.get(position);
}catch (Exception e){
//如果报错,那么outFragment为空,下面的ifelse就不会走
}
try {
if(position != adapter.getCount() - 1) {
//这里需要控制一下,因为最后一页是不需要设置动画,没有使用ViewContent包裹。
if (inFragment != null) {
//获取到的是什么呢?获得的是Fragment中的最外层View
View view = inFragment.getView();
ViewContent viewContent = (ViewContent) view;
for (int i = 0; i < viewContent.getChildCount(); i++) {
//拿到其中的View们,对其进行动画的控制
View child = viewContent.getChildAt(i);
if (!(child instanceof ViewPagerScrollListener)) {
continue;
}
listener = (ViewPagerScrollListener) child;
listener.onViewScrollIn(positionOffsetPixels, containerWidth);
}
}
if (outFragment != null) {
//拿到其中的View们 进行动画的控制
View view = outFragment.getView();
ViewContent viewContent = (ViewContent) view;
for (int i = 0; i < viewContent.getChildCount(); i++) {
View child = viewContent.getChildAt(i);
if (!(child instanceof ViewPagerScrollListener)) {
continue;
}
//AnimationView继承了ViewPagerScrollListener
listener = (ViewPagerScrollListener) child;
listener.onViewScrollOut(positionOffsetPixels);
}
}
}
}catch (Exception e){}
}
@Override
public void onPageSelected(int position) {
if(position == adapter.getCount() - 1){
iv_main.setVisibility(INVISIBLE);
}else{
iv_main.setVisibility(VISIBLE);
}
}
@Override
public void onPageScrollStateChanged(int state) {
//在滚动的状态下 动画开始 滚动停止 动画停止
AnimationDrawable animationDrawable = (AnimationDrawable) iv_main.getBackground();
switch (state){
case ViewPager.SCROLL_STATE_DRAGGING:
animationDrawable.start();
break;
case ViewPager.SCROLL_STATE_IDLE:
animationDrawable.stop();
break;
}
}
/**
* 拿到外层的ImageView,在这里需要根据ViewPager的滑动进行ImageView的动画控制,
* 也可以使用接口回调的方式进行。但这里只是进行一个动画的开始和停止,不需要那么复杂
* @param iv_main 外层ImageView:走路的小姑娘
*/
public void setIv_main(ImageView iv_main) {
this.iv_main = iv_main;
}
}
亮点:
在获取ViewPager中滑进去的页面和滑出来的页面时,使用了trycatch,如果出错,那么获取到的页面为空,就不会进行下面的操作。
2. ViewPager中的六个页面,前五个是要给其中的View设置动画的,需要设置动画的页面,我们需要使用一个自定义的容器ViewContent包裹,这个ViewContent的作用就是,遍历其中的View,如果用户传了自定义属性,那么就在这个View的外面包裹一层自定义容器,让这个自定义容器去识别用户传的自定义属性。
ViewContent的代码如下:
/**
* Created by kimliu on 2018/12/27
* 给View的外层包裹一个自定义的RelativeLayout
* 遍历其中的View,如果添加了自定义属性,就在这个View外层包裹一个自定义容器
*/
public class ViewContent extends RelativeLayout{
private static final String TAG = ViewContent.class.getSimpleName();
public ViewContent(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 查看源码发现,我们自定义View的时候,创建LayoutParams是调用的这个方法,那么我们重写这个方法,就可以偷梁换柱把LayoutParams换成我们自己LayoutParams
* @param attrs
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParams(getContext(),attrs);
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
MyLayoutParams p = (MyLayoutParams) params;
if(!isContent(p)){
//如果没有传自定义属性,直接把系统的child添加到Relativelayout中
super.addView(child,index,params);
}else{
//如果没有传自定义属性,在外层包裹一个View
AnimationView view = new AnimationView(getContext());
view.setAlphaIn(p.alphaIn);
view.setAlphaOut(p.alphaOut);
view.setxIn(p.xIn);
view.setxOut(p.xOut);
view.setyIn(p.yIn);
view.setyOut(p.yOut);
// Log.d(TAG, "addView: " + p.alphaIn +","+ p.alphaOut +","
// +","+p.xIn +","+p.xOut +","+p.yIn +","+p.yOut);
//把系统控件添加到AnimationView中
view.addView(child);
// 把包裹后的view添加到Relativelayout中
super.addView(view, index, params);
}
}
/**
* 是否有自定义属性
* @param params
* @return
*/
private boolean isContent(MyLayoutParams params){
return params.alphaIn != 0 ||
params.alphaOut != 0||
params.xIn != 0||
params.xOut != 0||
params.yIn != 0||
params.yOut != 0;
}
/**
* 自定义LayoutParams,在其中获取自定义属性
*/
public static class MyLayoutParams extends RelativeLayout.LayoutParams{
public int index;
public float xIn;
public float xOut;
public float yIn;
public float yOut;
public float alphaIn;
public float alphaOut;
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ParallaxAnimation);
alphaIn = a.getFloat(R.styleable.ParallaxAnimation_a_in,0f);//进入动画的透明度
alphaOut = a.getFloat(R.styleable.ParallaxAnimation_a_out,0f);//出去动画的透明度
xIn = a.getFloat(R.styleable.ParallaxAnimation_x_in,0f);//X轴上进入动画的加速度
xOut = a.getFloat(R.styleable.ParallaxAnimation_x_out,0f);//X轴上出去动画的加速度
yIn = a.getFloat(R.styleable.ParallaxAnimation_y_in,0f);//Y轴上进入动画的加速度
yOut = a.getFloat(R.styleable.ParallaxAnimation_y_out,0f);//Y轴上出去动画的加速度
a.recycle();
}
}
}
亮点:
- 查看源码可以发现,系统的View创建LayoutParams时,调用的是generateLayoutParams这个方法,如果我们重写这个方法,改变它的返回值,就可以偷梁换柱,把系统的LayoutParams换成我们自己写的LayoutParams。
2.在我们自己的LayoutParams类MyLayoutParams中,获取用户输入的自定义属性,在addView中判断,如果用户添加了自定义属性,我们就在该View的外层包裹一个自定义容器,再添加到ViewContent中,如果用户没有添加自定义属性,我们就直接把系统控件添加到ViewContent中,又是一个偷梁换柱。
3.AnimationView,这个自定义容器是包裹在系统View外面的ViewGroup,用来根据用户输入的自定义属性来控制View的动画。在这里,我们使用了接口回调,用来传递ViewPager滑动的像素值。
/**
* Created by kimliu on 2018/12/27
* 在定义了自定义属性的View外层包裹一个ViewGroup 这个ViewGroup用来识别自定义属性
* 并且根据这些自定义属性去做相应的操作
*
*/
public class AnimationView extends FrameLayout implements ViewPagerScrollListener{
public float xIn;
public float xOut;
public float yIn;
public float yOut;
public float alphaIn;
public float alphaOut;
public AnimationView(@NonNull Context context) {
super(context);
}
public AnimationView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public void setAlphaIn(float alphaIn) {
this.alphaIn = alphaIn;
}
public void setAlphaOut(float alphaOut) {
this.alphaOut = alphaOut;
}
public void setxIn(float xIn) {
this.xIn = xIn;
}
public void setxOut(float xOut) {
this.xOut = xOut;
}
public void setyIn(float yIn) {
this.yIn = yIn;
}
public void setyOut(float yOut) {
this.yOut = yOut;
}
@Override
public void onViewScrollIn(float positionOffsetPixels, float containerWidth) {
//实现属性动画
ViewHelper.setTranslationX(this,(containerWidth - positionOffsetPixels) * xIn);
ViewHelper.setTranslationY(this,(containerWidth - positionOffsetPixels) * yIn);
}
@Override
public void onViewScrollOut(float positionOffsetPixels) {
//实现属性动画
ViewHelper.setTranslationX(this,0 - positionOffsetPixels * xOut);
ViewHelper.setTranslationY(this,0 - positionOffsetPixels * yOut);
}
@Override
public void resetView() {
ViewHelper.setTranslationX(this,0);
ViewHelper.setTranslationY(this,0);
}
}
4. ViewPagerScrollListener,在其中实现了三个方法,分别是在页面滑入时调用的方法onViewScrollIn;页面滑出时调用的方法onViewScrollOut;页面看不见时讲所有View重置的方法resetView。
/**
* Created by kimliu on 2018/12/27
*/
public interface ViewPagerScrollListener {
/**
* 页面进入时调用
* @param positionOffsetPixels ViewPager滑动的偏移量
* @param containerWidth 最外层container的宽度
*/
void onViewScrollIn(float positionOffsetPixels,float containerWidth);
/**
* 页面滑出时调用
* @param positionOffsetPixels ViewPager滑动的偏移量
*/
void onViewScrollOut(float positionOffsetPixels);
/**
* 重置View 当View滑出屏幕时,将View的属性重置
*/
void resetView();
}
5.Fragment的编写,这里要注意的是,在这里进行页面上所有View的重置。
public class ParallaxFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle bundle = getArguments();
int layoutId = bundle.getInt("layoutId");
int index = bundle.getInt("index");
return LayoutInflater.from(getActivity()).inflate(layoutId,null);
}
/**
* 页面看不见时,获取页面中所有的View,进行View的重置
* @param isVisibleToUser
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
View view = getView();
if(view instanceof ViewContent){
ViewContent viewContent = (ViewContent) view;
for (int i = 0; i < viewContent.getChildCount() ; i++){
View child = viewContent.getChildAt(i);
if(child instanceof AnimationView){
ViewPagerScrollListener listener = (ViewPagerScrollListener) child;
listener.resetView();
}
}
}
}
}
6.Activity的编写:设置透明状态栏,这里使用了第三方工具类:
implementation 'com.blankj:utilcode:1.21.2'
public class SplashActivity extends AppCompatActivity {
private ParallaxContainer parallax_container;
private ImageView iv_splash;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
initStatusBar();
parallax_container = findViewById(R.id.parallax_container);
parallax_container.setUp(new int[]{
R.layout.view_intro_1,
R.layout.view_intro_2,
R.layout.view_intro_3,
R.layout.view_intro_4,
R.layout.view_intro_5,
R.layout.view_login
});
iv_splash = findViewById(R.id.iv_splash);
iv_splash.setBackgroundResource(R.drawable.splash_run);
parallax_container.setIv_main(iv_splash);
}
/**
* 沉浸式状态栏
*/
private void initStatusBar() {
RelativeLayout splash_relativelayout = findViewById(R.id.splash_relativelayout);
splash_relativelayout.setPadding(splash_relativelayout.getPaddingLeft(),
splash_relativelayout.getPaddingTop()+ BarUtils.getStatusBarHeight(),
splash_relativelayout.getPaddingRight(),
splash_relativelayout.getPaddingBottom());
//设置为透明
BarUtils.setStatusBarAlpha(this,0);
BarUtils.setStatusBarLightMode(this,true);
}
}
7. 最后要说一句,在ViewPager的前五个页面最外层,都需要包裹一个ViewContent,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<com.kimliu.kimliucustomview.ui.view.customAnimation.ViewContent
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ImageView
android:id="@+id/iv_0"
android:layout_width="103dp"
android:layout_height="19dp"
android:layout_centerInParent="true"
android:src="@drawable/intro1_item_0"
app:x_in="1.2"
app:x_out="1.2"
/>
<ImageView
android:id="@+id/iv_1"
android:layout_width="181dp"
android:layout_height="84dp"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="13dp"
android:layout_marginTop="60dp"
android:src="@drawable/intro1_item_1"
app:x_in="0.8"
app:x_out="0.8" />
<ImageView
android:id="@+id/iv_2"
android:layout_width="143dp"
android:layout_height="58dp"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginTop="109dp"
android:src="@drawable/intro1_item_2"
app:x_in="1.1"
app:x_out="1.8" />
<ImageView
android:id="@+id/iv_3"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_marginRight="40dp"
android:layout_marginBottom="185dp"
android:src="@drawable/intro1_item_3"
app:x_in="0.8"
app:x_out="0.8"
app:a_in="0.8"
app:a_out="0.8" />
<ImageView
android:id="@+id/iv_4"
android:layout_width="fill_parent"
android:layout_height="128dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="29dp"
android:background="@drawable/intro1_item_4"
app:a_in="0.8"
app:a_out="0.8"
app:x_in="0.8"
app:x_out="0.8" />
<ImageView
android:id="@+id/iv_5"
android:layout_width="260dp"
android:layout_height="18dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_marginBottom="16dp"
android:layout_marginLeft="15dp"
android:src="@drawable/intro1_item_5"
app:a_in="0.9"
app:a_out="0.9"
app:x_in="0.9"
app:x_out="0.9" />
<ImageView
android:id="@+id/iv_6"
android:layout_width="24dp"
android:layout_height="116dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_marginBottom="35dp"
android:layout_marginLeft="46dp"
android:src="@drawable/intro1_item_6"
app:x_in="0.6"
app:x_out="0.6" />
<ImageView
android:id="@+id/iv_7"
android:layout_width="45dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_marginBottom="23dp"
android:layout_marginLeft="76dp"
android:src="@drawable/intro1_item_7"
app:a_in="0.3"
app:a_out="0.3"
app:x_in="0.5"
app:x_out="0.5" />
</com.kimliu.kimliucustomview.ui.view.customAnimation.ViewContent>