有的时候会需要做一些自定义的动画效果,在会反复用到的动画效果可以考虑做成动画框架,方便使用,做成框架的话就需要考虑很多的问题,最典型的问题就是属性和方法必须要是可配置的,这里就来聊一聊自定义动画框架的做法
重难点分析
在自定义动画框架里面,最难的一个问题就是怎么样获得属性,如果直接写自定义的属性,那么编译时候就会报错了,那么自然就想到了在外层包裹自定义的属性,通过处理自定义的属性来得到,这样便是android源代码一些控件的解决办法,其实就是一招偷梁换柱,在处理的时候,如果是自定义的属性,就自己处理,如果不是自定义的属性,就交由系统处理,然后在addView上再做手脚,另外从xml的解析入手,可以自定义LayoutInflater,然后重写onCreateView(),在里面就可以完成偷梁换柱,故这里有两种方法可以完成
在addView时候拦截做法
这里做一个滑动的自定义动画
在滑动的View里面,需要实现滑动时候和重置的工作,这里有一个接口,及实现接口的方法来做
public interface DiscrollvableInterface {
//当滑动的时候调用该方法,用来控制里面的控件执行相应的动画
void onDiscrollve(float ratio);
//重置view的属性----恢复view的原来属性
void onResetDiscrollve();
}
实现上面的接口
public class DiscrollvableView extends FrameLayout implements DiscrollvableInterface {
private static final int TRANSLATION_FROM_TOP = 0x01;
private static final int TRANSLATION_FROM_BOTTOM = 0x02;
private static final int TRANSLATION_FROM_LEFT = 0x04;
private static final int TRANSLATION_FROM_RIGHT = 0x08;
//颜色估值器
private static ArgbEvaluator sArgbEvaluator = new ArgbEvaluator();
//自定义属性的一些接收的变量
private int mDiscrollveFromBgColor;//背景颜色变化开始值
private int mDiscrollveToBgColor;//背景颜色变化结束值
private boolean mDiscrollveAlpha;//是否需要透明度动画
private int mDisCrollveTranslation;//平移值
private boolean mDiscrollveScaleX;//是否需要x轴方向缩放
private boolean mDiscrollveScaleY;//是否需要y轴方向缩放
private int mHeight;//本view的高度
private int mWidth;//宽度
public void setmDiscrollveFromBgColor(int mDiscrollveFromBgColor) {
this.mDiscrollveFromBgColor = mDiscrollveFromBgColor;
}
public void setmDiscrollveToBgColor(int mDiscrollveToBgColor) {
this.mDiscrollveToBgColor = mDiscrollveToBgColor;
}
public void setmDiscrollveAlpha(boolean mDiscrollveAlpha) {
this.mDiscrollveAlpha = mDiscrollveAlpha;
}
public void setmDisCrollveTranslation(int mDisCrollveTranslation) {
this.mDisCrollveTranslation = mDisCrollveTranslation;
}
public void setmDiscrollveScaleX(boolean mDiscrollveScaleX) {
this.mDiscrollveScaleX = mDiscrollveScaleX;
}
public void setmDiscrollveScaleY(boolean mDiscrollveScaleY) {
this.mDiscrollveScaleY = mDiscrollveScaleY;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
onResetDiscrollve();
}
public DiscrollvableView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DiscrollvableView(Context context) {
super(context);
}
@Override
public void onDiscrollve(float ratio) {
// ratio:0~1
//控制自身的动画属性
if (mDiscrollveAlpha) {
setAlpha(ratio);
}
if (mDiscrollveScaleX) {
setScaleX(ratio);
}
if (mDiscrollveScaleY) {
setScaleY(ratio);
}
//判断到底是哪一种值:fromTop,fromBottom,fromLeft,fromRight
if (isDiscrollTranslationFrom(TRANSLATION_FROM_BOTTOM)) {
setTranslationY(mHeight * (1 - ratio));//mHeight-->0(代表原来的位置)
}
if (isDiscrollTranslationFrom(TRANSLATION_FROM_TOP)) {
setTranslationY(-mHeight * (1 - ratio));//-mHeight-->0(代表原来的位置)
}
if (isDiscrollTranslationFrom(TRANSLATION_FROM_LEFT)) {
setTranslationX(-mWidth * (1 - ratio));//-width-->0(代表原来的位置)
}
if (isDiscrollTranslationFrom(TRANSLATION_FROM_RIGHT)) {
setTranslationX(mWidth * (1 - ratio));//width-->0(代表原来的位置)
}
//颜色渐变动画
if (mDiscrollveFromBgColor != -1 && mDiscrollveToBgColor != -1) {
//ratio=0.5 color=中间颜色
setBackgroundColor((Integer) sArgbEvaluator.evaluate(ratio, mDiscrollveFromBgColor, mDiscrollveToBgColor));
}
}
private boolean isDiscrollTranslationFrom(int translationMask) {
if (mDisCrollveTranslation == -1) {
return false;
}
//fromLeft|fromBottom & fromBottom = fromBottom
return (mDisCrollveTranslation & translationMask) == translationMask;
}
@Override
public void onResetDiscrollve() {
//控制自身的动画属性
if (mDiscrollveAlpha) {
setAlpha(0);
}
if (mDiscrollveScaleX) {
setScaleX(0);
}
if (mDiscrollveScaleY) {
setScaleY(0);
}
//判断到底是哪一种值:fromTop,fromBottom,fromLeft,fromRight
if (isDiscrollTranslationFrom(TRANSLATION_FROM_BOTTOM)) {
setTranslationY(mHeight);//mHeight-->0(代表原来的位置)
}
if (isDiscrollTranslationFrom(TRANSLATION_FROM_TOP)) {
setTranslationY(-mHeight);//-mHeight-->0(代表原来的位置)
}
if (isDiscrollTranslationFrom(TRANSLATION_FROM_LEFT)) {
setTranslationX(-mWidth);//-width-->0(代表原来的位置)
}
if (isDiscrollTranslationFrom(TRANSLATION_FROM_RIGHT)) {
setTranslationX(mWidth);//width-->0(代表原来的位置)
}
}
}
那么便完成了对于滑动效果的控制,为了便于控制,自定义View继承ScrollView
public class DiscrollView extends ScrollView {
private DiscrollViewContent mContent;
public DiscrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View content = getChildAt(0);
mContent = (DiscrollViewContent) content;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
View first = mContent.getChildAt(0);
first.getLayoutParams().height = getHeight();
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
int scrollViewHeight = getHeight();
//监听滑动----接口---->控制DiscrollViewContent的属性
//遍历MyLinearLayout里面所有子控件(MyViewGroup)
for (int i = 0; i < mContent.getChildCount(); i++) {
View child = mContent.getChildAt(i);
if (!(child instanceof DiscrollvableInterface)) {
continue;
}
//ratio:0~1
DiscrollvableInterface discrollvableInterface = (DiscrollvableInterface) child;
//1.child离scrollview顶部的高度
int discrollvableTop = child.getTop();
int discrollvableHeight = child.getHeight();
//2.得到scrollview滑出去的高度
//3.得到child离屏幕顶部的高度
int discrollvableAbsoluteTop = discrollvableTop - t;
//什么时候执行动画?当child滑进屏幕的时候
if (discrollvableAbsoluteTop <= scrollViewHeight) {
int visibleGap = scrollViewHeight - discrollvableAbsoluteTop;
//确保ratio是在0~1,超过了1 也设置为1
discrollvableInterface.onDiscrollve(clamp(visibleGap / (float) discrollvableHeight, 1f, 0f));
} else {//否则,就恢复到原来的位置
discrollvableInterface.onResetDiscrollve();
}
}
}
public static float clamp(float value, float max, float min) {
return Math.max(Math.min(value, max), min);
}
}
设置属性,偷梁换柱
public class DiscrollViewContent extends LinearLayout {
public DiscrollViewContent(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(VERTICAL);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
// 得到xml里面穿过来的参数
return new MyLayoutParams(getContext(), attrs);
}
@Override
public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {
//从child里面拿到我自定义的属性,传到discrollvableView里面
MyLayoutParams p = (MyLayoutParams) params;
if (!isDiscrollvable(p)) {//判断该view是否穿了自定义属性值,不是就不需要执行动画,不包一层FrameLayout
super.addView(child, index, params);
} else {
//在addView里面插一脚,往child外面包裹一层FrameLayout
DiscrollvableView discrollvableView = new DiscrollvableView(getContext());
discrollvableView.setmDiscrollveAlpha(p.mDiscrollveAlpha);
discrollvableView.setmDisCrollveTranslation(p.mDisCrollveTranslation);
discrollvableView.setmDiscrollveScaleX(p.mDiscrollveScaleX);
discrollvableView.setmDiscrollveScaleY(p.mDiscrollveScaleY);
discrollvableView.setmDiscrollveFromBgColor(p.mDiscrollveFromBgColor);
discrollvableView.setmDiscrollveToBgColor(p.mDiscrollveToBgColor);
discrollvableView.addView(child);
super.addView(discrollvableView, index, params);
}
}
private boolean isDiscrollvable(MyLayoutParams p) {
return p.mDiscrollveAlpha || p.mDiscrollveScaleX
|| p.mDiscrollveScaleY || p.mDisCrollveTranslation != -1
|| (p.mDiscrollveFromBgColor != -1 && p.mDiscrollveToBgColor != -1);
}
public static class MyLayoutParams extends LinearLayout.LayoutParams {
public int mDiscrollveFromBgColor;//背景颜色变化开始值
public int mDiscrollveToBgColor;//背景颜色变化结束值
public boolean mDiscrollveAlpha;//是否需要透明度动画
public int mDisCrollveTranslation;//平移值
public boolean mDiscrollveScaleX;//是否需要x轴方向缩放
public boolean mDiscrollveScaleY;//是否需要y轴方向缩放
public MyLayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
// 从child里面拿到我自定义的属性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscrollView_LayoutParams);
mDiscrollveAlpha = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_alpha, false);
mDiscrollveScaleX = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleX, false);
mDiscrollveScaleY = a.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleY, false);
mDisCrollveTranslation = a.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_translation, -1);
mDiscrollveFromBgColor = a.getColor(R.styleable.DiscrollView_LayoutParams_discrollve_fromBgColor, -1);
mDiscrollveToBgColor = a.getColor(R.styleable.DiscrollView_LayoutParams_discrollve_toBgColor, -1);
a.recycle();
}
}
}
最后是一些自定义属性
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<declare-styleable name="DiscrollView_LayoutParams">
<attr name="discrollve_alpha" format="boolean"/>
<attr name="discrollve_scaleX" format="boolean"/>
<attr name="discrollve_scaleY" format="boolean"/>
<attr name="discrollve_fromBgColor" format="color"/>
<attr name="discrollve_toBgColor" format="color"/>
<attr name="discrollve_translation"/>
</declare-styleable>
<attr name="discrollve_translation">
<flag name="fromTop" value="0x01" />
<flag name="fromBottom" value="0x02" />
<flag name="fromLeft" value="0x04" />
<flag name="fromRight" value="0x08" />
</attr>
</resources>
布局
<com.cj5785.customanimationaddview.DiscrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.cj5785.customanimationaddview.DiscrollViewContent
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="560dp"
android:background="@android:color/white"
android:gravity="center"
android:text="测试文字使用情况"
android:textSize="25sp" />
<View
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/darker_gray"
custom:discrollve_alpha="true" />
<ImageView
android:layout_width="89dp"
android:layout_height="80dp"
android:src="@drawable/duck"
custom:discrollve_alpha="true"
custom:discrollve_translation="fromLeft|fromBottom" />
<View
android:layout_width="match_parent"
android:layout_height="200dp"
custom:discrollve_fromBgColor="#ffff00"
custom:discrollve_toBgColor="#88EE66" />
<ImageView
android:layout_width="150dp"
android:layout_height="106dp"
android:layout_gravity="right"
android:src="@drawable/camera"
custom:discrollve_translation="fromRight" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="第二段文字使用测试"
android:textSize="28sp"
custom:discrollve_alpha="true"
custom:discrollve_translation="fromBottom" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="20dp"
android:src="@drawable/sun"
custom:discrollve_scaleX="true"
custom:discrollve_scaleY="true" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="20dp"
android:src="@drawable/balloon"
custom:discrollve_translation="fromLeft|fromBottom" />
</com.cj5785.customanimationaddview.DiscrollViewContent>
</com.cj5785.customanimationaddview.DiscrollView>
效果如下
自定义LayoutInflater
这里实现一个仿小红书的视差动画效果
首先定义一个根布局,然后实例化其自定义控件
<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"
android:background="@android:color/white">
<com.cj5785.customanimationlayoutinflater.ParallaxContainer
android:id="@+id/parallax_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/iv_man"
android:layout_width="66dp"
android:layout_height="202dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="10dp"
android:background="@drawable/intro_item_manrun_1"/>
</RelativeLayout>
实例化自定义控件,在最后一个登陆界面,由于与前面的不同,使用切换Fragment的方式,需要使用到适配器
适配器
public class ParallaxAdapter extends FragmentPagerAdapter {
private List<ParallaxFragment> fragmentList;
public ParallaxAdapter(FragmentManager fm, List<ParallaxFragment> fragmentList) {
super(fm);
this.fragmentList = fragmentList;
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
}
自定义Fragment,其主要的偷梁换柱工作就是在这里完成的,返回我们处理以后的View
public class ParallaxFragment extends Fragment {
private List<View> parallaxViews = new ArrayList<>();
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Bundle bundle = getArguments();
int layoutId = bundle.getInt("layoutId");
//正常情况下在这里获得view,然后返回,我们就在这里偷梁换柱
// View view = inflater.inflate(layoutId, container);
//使用自定义的渲染器渲染
ParallaxLayoutInflater layoutInflater = new ParallaxLayoutInflater(inflater, getActivity(), this);
return layoutInflater.inflate(layoutId, null);
}
public List<View> getParallaxViews() {
return parallaxViews;
}
}
动画引导层
//引导页的最外层布局
public class ParallaxContainer extends FrameLayout implements OnPageChangeListener {
private List<ParallaxFragment> fragmentList;
private ParallaxAdapter adapter;
private float containerWidth;
private ImageView iv_man;
public ParallaxContainer(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
//指定引导页的所有页面布局文件
public void setUp(int... ids) {
//初始化framentList,在调用处直接调用setUp()
fragmentList = new ArrayList<>();
for (int i = 0; i < ids.length; i++) {
ParallaxFragment fragment = new ParallaxFragment();
Bundle bundle = new Bundle();
//Fragment中需要加载的布局文件id
bundle.putInt("layoutId", ids[i]);
fragment.setArguments(bundle);
fragmentList.add(fragment);
}
//设置ViewPager
ViewPager viewPager = new ViewPager(getContext());
viewPager.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
viewPager.setId(R.id.parallax_pager);
adapter = new ParallaxAdapter(((MainActivity) getContext()).getSupportFragmentManager(), fragmentList);
viewPager.setAdapter(adapter);
addView(viewPager, 0);
//设置监听
viewPager.addOnPageChangeListener(this);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
containerWidth = getWidth();
//在翻页的过程中,不断根据视图的标签中对应的动画参数,改变视图的位置或者透明度
//获取到进入的页面
ParallaxFragment inFragment = null;
try {
inFragment = fragmentList.get(position - 1);
} catch (Exception e) {
}
//获取到退出的页面
ParallaxFragment outFragment = null;
try {
outFragment = fragmentList.get(position);
} catch (Exception e) {
}
if (inFragment != null) {
//获取Fragment上所有的视图,实现动画效果
List<View> inViews = inFragment.getParallaxViews();
if (inViews != null) {
for (View view : inViews) {
//获取标签,从标签上获取所有的动画参数
ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
if (tag == null) {
continue;
}
//translationY改变view的偏移位置,translationY=100,代表view在其原始位置向下移动100
//仔细观察进入的fragment中view从远处过来,不断向下移动,最终停在原始位置
view.setTranslationX((containerWidth - positionOffsetPixels) * tag.xIn);
view.setTranslationY((containerWidth - positionOffsetPixels) * tag.yIn);
}
}
}
if (outFragment != null) {
List<View> outViews = outFragment.getParallaxViews();
if (outViews != null) {
for (View view : outViews) {
ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
if (tag == null) {
continue;
}
//仔细观察退出的fragment中view从原始位置开始向上移动,translationY应为负数
view.setTranslationX((-positionOffsetPixels) * tag.xOut);
view.setTranslationY((-positionOffsetPixels) * tag.yOut);
}
}
}
}
@Override
public void onPageSelected(int position) {
if (position == adapter.getCount() - 1) {
iv_man.setVisibility(INVISIBLE);
} else {
iv_man.setVisibility(VISIBLE);
}
}
@Override
public void onPageScrollStateChanged(int state) {
AnimationDrawable animation = (AnimationDrawable) iv_man.getBackground();
switch (state) {
case ViewPager.SCROLL_STATE_DRAGGING:
animation.start();
break;
case ViewPager.SCROLL_STATE_IDLE:
animation.stop();
break;
default:
break;
}
}
public void setIv_man(ImageView iv_man) {
this.iv_man = iv_man;
}
}
拦截系统处理xml
public class ParallaxLayoutInflater extends LayoutInflater {
private static final String TAG = "ParallaxLayoutInflater";
private ParallaxFragment parallaxFragment;
protected ParallaxLayoutInflater(LayoutInflater original, Context newContext, ParallaxFragment parallaxFragment) {
super(original, newContext);
this.parallaxFragment = parallaxFragment;
//源代码中存在:
//View view;
//if (mFactory2 != null) {
// view = mFactory2.onCreateView(parent, name, context, attrs);
//} else if (mFactory != null) {
// view = mFactory.onCreateView(name, context, attrs);
//} else {
// view = null;
//}
//if (view == null && mPrivateFactory != null) {
// view = mPrivateFactory.onCreateView(parent, name, context, attrs);
//}
//也就是说只要实现Factory,那么就不会调用后面的方法
setFactory(new ParallaxFactory(this));
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
//实际用于创建LayoutInflater的方法
return new ParallaxLayoutInflater(this, newContext, parallaxFragment);
}
//这个框架最核心的地方就是在这里设置Factory,从而进行拦截
class ParallaxFactory implements LayoutInflater.Factory{
private LayoutInflater inflater;
//系统提供的视图分为android.widget.xxx和android.view.xxx
private final String[] prefixs = {
"android.widget.",
"android.view."
};
public ParallaxFactory(LayoutInflater inflater) {
this.inflater = inflater;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
//实例化view
View view = null;
if (view == null) {
view = createViewOrFailQuietly(name,context,attrs);
}
if (view != null) {
//获取自定义属性,并将自定义标签值绑定到view上面
getCustomAttrs(context, attrs, view);
parallaxFragment.getParallaxViews().add(view);
}
return view;
}
private void getCustomAttrs(Context context, AttributeSet attrs, View view) {
//所有自定义的属性
int[] attrIds = {
R.attr.a_in,
R.attr.a_out,
R.attr.x_in,
R.attr.x_out,
R.attr.y_in,
R.attr.y_out
};
//获取
TypedArray typedArray = context.obtainStyledAttributes(attrs, attrIds);
if (typedArray != null && typedArray.length() > 0) {
//获取自定义属性的值
Log.d(TAG, "getCustomAttrs");
ParallaxViewTag tag = new ParallaxViewTag();
tag.alphaIn = typedArray.getFloat(0, 0f);
tag.alphaOut = typedArray.getFloat(1, 0f);
tag.xIn = typedArray.getFloat(2, 0f);
tag.xOut = typedArray.getFloat(3, 0f);
tag.yIn = typedArray.getFloat(4, 0f);
tag.yOut = typedArray.getFloat(5, 0f);
view.setTag(R.id.parallax_view_tag, tag);
}
typedArray.recycle();
}
private View createViewOrFailQuietly(String name, String prefix, Context context, AttributeSet attrs) {
try {
//通过系统的inflater创建视图,读取系统的属性
return inflater.createView(name, prefix, attrs);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
private View createViewOrFailQuietly(String name, Context context, AttributeSet attrs) {
//通过系统inflater创建视图
//1.自定义控件标签名称带点,所以创建时不需要前缀
if (name.contains(".")) {
return createViewOrFailQuietly(name, null, context, attrs);
}
//2.系统视图需要加上前缀
for (String prefix : prefixs) {
View view = createViewOrFailQuietly(name, prefix, context, attrs);
if (view != null) {
return view;
}
}
return null;
}
}
}
参数控制
//视差动画播放时参数的控制
public class ParallaxViewTag {
protected int index;
protected float xIn;
protected float xOut;
protected float yIn;
protected float yOut;
protected float alphaIn;
protected float alphaOut;
@Override
public String toString() {
return "ParallaxViewTag [index=" + index + ", xIn=" + xIn + ", xOut=" + xOut + ", yIn=" + yIn
+ ", yOut=" + yOut + ", alphaIn=" + alphaIn + ", alphaOut=" + alphaOut + "]";
}
}
同时完成人行走的帧动画
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/intro_item_manrun_1"
android:duration="200" />
<item
android:drawable="@drawable/intro_item_manrun_2"
android:duration="200" />
</animation-list>
自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="a_in" format="float" />
<attr name="a_out" format="float" />
<attr name="x_in" format="float" />
<attr name="x_out" format="float" />
<attr name="y_in" format="float" />
<attr name="y_out" format="float" />
</resources>
ids
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="parallax_pager" type="id"/>
<item name="parallax_view_tag" type="id"/>
</resources>
实现效果如下图所示