首先看下效果:
可以看到每滑出或滑入一个view,都会执行相应的动画
闲话不说,直接上代码:
思路:
1.最外层是一个scrollview,我们要监听scrollview的滑动,根据滑动距离来计算view的可见高度,再根据view的本身高度计算一个比例值,再根据比例值来执行相应的动画
2.各个子View执行的动画都是不同的,这是就考虑到用自定义属性来控制view执行什么样的动画
3.怎么样去获取我们的自定义属性,并将属性值传给执行动画的控件
1.首先定义一个接口,用来操作view的动画
public interface DiscrollInterface {
/**
* 当滑动的时候调用该方法,用来控制里面的控件执行相应的动画
* @param ratio 动画执行的百分比(child view画出来的距离百分比)
*/
public void onDiscroll(float ratio);
/**
* 重置动画--让view所有的属性都恢复到原来的样子
*/
public void onResetDiscroll();
}
2 . 继承scrollview,实现对滑动的监听,来执行子view的动画
public class AnimatorScrollView extends ScrollView {
//scrollview里的包裹布局,下面会讲到,这个是最重要的部分,
//通过该布局来分发动画
private AnimatorLinerLayout mContent;
public AnimatorScrollView(Context context) {
super(context);
}
public AnimatorScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AnimatorScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View view = getChildAt(0);
if (view instanceof AnimatorLinerLayout) {
mContent = (AnimatorLinerLayout) view;
} else {
throw new RuntimeException("The child view must instanceof AnimatorLinerLayout");
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//为了效果,把第一个子view的高度设置满屏
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 mHeight = getHeight();
for (int i = 0; i < mContent.getChildCount(); i++) {
View view = mContent.getChildAt(i);
if (view instanceof DiscrollInterface) {
//子view都要实现DiscrollInterface 这个接口,执行动画
DiscrollInterface discrollInterface = (DiscrollInterface) view;
//view划出来的距离
int absolutY = view.getTop() - t;
//如果view当前已经可见
if (absolutY < mHeight) {
//计算可见的高度
int visibleGap = mHeight - absolutY;
//根据可见高度来算出动画执行的比例值
float radio = clamp(1f, 0f, visibleGap / (float) view.getHeight());
discrollInterface.onDiscroll(radio);
} else {
discrollInterface.onResetDiscroll();
}
} else {
continue;
}
}
}
//为了把比例控制在0~1之间
public float clamp(float max, float min, float value) {
return Math.min(Math.max(min, value), max);
}
}
3.写一些自定义动画属性
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<declare-styleable name="DiscrollView_LayoutParams">
<!-- 透明度-->
<attr name="discrollve_alpha" format="boolean"/>
<!--X方向的缩放-->
<attr name="discrollve_scaleX" format="boolean"/>
<!--Y方向的缩放-->
<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>
4.接下来继承LinearLayout,用来分发子view的动画
/**
* scrollview外层布局控件,总控内部
*/
public class AnimatorLinerLayout extends LinearLayout {
public AnimatorLinerLayout(Context context) {
this(context, null);
}
public AnimatorLinerLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AnimatorLinerLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
}
//通过查看源码,viewgroup读取控件属性是在addview创建LayoutParams时读取的,而viewGroup设置的layoutParams是通过generateLayoutParams来返回的,所以这里我们重写generateLayoutParams返回我们自己的layoutParams(见下面的注释)
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new AnimatorLayoutParams(getContext(),attrs);
}
//1.系统控件不识别自定义属性,所以要考虑给控件包一层布局
//2.这里采取用父容器(AnimatorFramelayout)组件给子容器包裹一层的方式,而真正执行动画的就是AnimatorFramelayout (步骤5)
//3.系统是通过布局文件,然后调用VIEW的addView来进行加载的
//4.此时进行layoutparams的转换
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
AnimatorFramelayout framelayout = new AnimatorFramelayout(child.getContext());
AnimatorLayoutParams layoutParams = (AnimatorLayoutParams) params;
if (!isAnimatorFramelayout(layoutParams)) {
super.addView(child, params);
} else {
framelayout.addView(child);
framelayout.setmDiscrollveScaleX(layoutParams.mDiscrollveScaleX); framelayout.setmDiscrollveScaleY(layoutParams.mDiscrollveScaleY);
framelayout.setmDiscrollveAlpha(layoutParams.mDiscrollveAlpha);
framelayout.setmDiscrollveFromBgColor(layoutParams.mDiscrollveFromBgColor);
framelayout.setmDiscrollveToBgColor(layoutParams.mDiscrollveToBgColor);
framelayout.setmDisCrollveTranslation(layoutParams.mDisCrollveTranslation);
super.addView(framelayout, params);
}
}
private boolean isAnimatorFramelayout(AnimatorLayoutParams params) {
return params.mDiscrollveAlpha || params.mDiscrollveScaleX
|| params.mDiscrollveScaleY || params.mDisCrollveTranslation != -1
|| (params.mDiscrollveFromBgColor != -1 && params.mDiscrollveToBgColor != -1);
}
//在这里继承LinearLayout.LayoutParams,初始化时读取自定义的属性
private class AnimatorLayoutParams extends LinearLayout.LayoutParams {
public boolean mDiscrollveAlpha;
public boolean mDiscrollveScaleX;
public boolean mDiscrollveScaleY;
public int mDisCrollveTranslation;
public int mDiscrollveFromBgColor;
public int mDiscrollveToBgColor;
public AnimatorLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
init(attrs);
}
//在这里读取自定义的属性
private void init( AttributeSet attrs) {
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.DiscrollView_LayoutParams);
mDiscrollveAlpha = array.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_alpha, false);
mDiscrollveScaleX = array.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleX, false);
mDiscrollveScaleY = array.getBoolean(R.styleable.DiscrollView_LayoutParams_discrollve_scaleY, false);
mDiscrollveFromBgColor = array.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_fromBgColor, -1);
mDiscrollveToBgColor = array.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_toBgColor, -1);
mDisCrollveTranslation = array.getInt(R.styleable.DiscrollView_LayoutParams_discrollve_translation, -1);
Log.i("swl","mDisCrollveTranslation=="+mDisCrollveTranslation);
array.recycle();
}
}
}
5.最后一步,写一个真正执行动画的布局AnimatorFramelayout ,用来包裹子控件
public class AnimatorFramelayout extends FrameLayout implements DiscrollInterface {
public AnimatorFramelayout(@NonNull Context context) {
this(context, null);
}
public AnimatorFramelayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AnimatorFramelayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* <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>
* 0000000001
* 0000000010
* 0000000100
* 0000001000
* top|left
* 0000000001 top
* 0000000100 left 或运算 |
* 0000000101
* 反过来就使用& 与运算
*/
//保存自定义属性
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;
}
@Override
public void onDiscroll(float ratio) {
if (mDiscrollveAlpha) {
setAlpha(ratio);
}
if (mDiscrollveScaleX) {
setScaleX(ratio);
}
if (mDiscrollveScaleY) {
setScaleY(ratio);
}
if (-1 != mDiscrollveFromBgColor && -1 != mDiscrollveToBgColor) {
setBackgroundColor((int) sArgbEvaluator.evaluate(ratio, mDiscrollveFromBgColor, mDiscrollveToBgColor));
}
if (isTranslation(TRANSLATION_FROM_TOP)) {
// setTranslationY();
setTranslationY(-(1 - ratio) * mHeight);
}
if (isTranslation(TRANSLATION_FROM_BOTTOM)) {
setTranslationY((1 - ratio) * mHeight);
}
if (isTranslation(TRANSLATION_FROM_LEFT)) {
setTranslationX(-(1 - ratio) * mWidth);
}
if (isTranslation(TRANSLATION_FROM_RIGHT)) {
setTranslationX((1 - ratio) * mWidth);
}
}
@Override
public void onResetDiscroll() {
if (mDiscrollveAlpha) {
setAlpha(0);
}
if (mDiscrollveScaleX) {
setScaleX(0);
}
if (mDiscrollveScaleY) {
setScaleY(0);
}
//平移动画 int值:left,right,top,bottom left|bottom
if (isTranslation(TRANSLATION_FROM_BOTTOM)) {
setTranslationY(mHeight);//height--->0(0代表恢复到原来的位置)
}
if (isTranslation(TRANSLATION_FROM_TOP)) {
setTranslationY(-mHeight);//-height--->0(0代表恢复到原来的位置)
}
if (isTranslation(TRANSLATION_FROM_LEFT)) {
setTranslationX(-mWidth);//mWidth--->0(0代表恢复到原来的位置)
}
if (isTranslation(TRANSLATION_FROM_RIGHT)) {
setTranslationX(mWidth);//-mWidth--->0(0代表恢复到原来的位置)
}
}
//是否有平移动画属性
private boolean isTranslation(int value) {
if (mDisCrollveTranslation == -1) {
return false;
}
return (mDisCrollveTranslation & value) == value;
}
}
最后附上布局文件:
<com.swl.animfreamwork.AnimatorScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:discrollve="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.swl.animfreamwork.AnimatorLinerLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#007788"
android:textColor="@android:color/black"
android:textSize="25sp"
android:padding="25dp"
android:gravity="center"
android:fontFamily="serif"
android:text="带上您的行李箱,准备shopping!"
discrollve:discrollve_alpha="true"
/>
<ImageView
android:layout_width="200dp"
android:layout_height="120dp"
android:layout_gravity="top|right"
discrollve:discrollve_translation="fromLeft|fromBottom"
discrollve:discrollve_alpha="true"
android:src="@mipmap/baggage" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:textColor="@android:color/black"
android:textSize="25sp"
android:padding="25dp"
android:gravity="center"
android:fontFamily="serif"
android:text="准备好相机,这里有你想象不到的惊喜!"
discrollve:discrollve_fromBgColor="#ffff00"
discrollve:discrollve_toBgColor="#88EE66"
/>
<ImageView
android:layout_width="220dp"
android:layout_height="110dp"
android:layout_gravity="right"
android:src="@mipmap/camera"
discrollve:discrollve_translation="fromRight" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:fontFamily="serif"
android:gravity="center"
android:background="#D97C1F"
android:text="这次淘宝造物节真的来了,我们都在造,你造吗?\n
7月22日-7月24日\n
上海世博展览馆\n
在现场,我们造什么?"
android:textSize="23sp"
discrollve:discrollve_alpha="true"
discrollve:discrollve_translation="fromBottom" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/sweet"
discrollve:discrollve_scaleX="true"
discrollve:discrollve_scaleY="true" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/shoes"
discrollve:discrollve_alpha="true"
discrollve:discrollve_scaleX="true"
discrollve:discrollve_scaleY="true"
discrollve:discrollve_translation="fromLeft|fromBottom" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/shoes"
discrollve:discrollve_alpha="true"
discrollve:discrollve_scaleY="true"
discrollve:discrollve_translation="fromRight|fromTop" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/sweet"
discrollve:discrollve_alpha="true"
discrollve:discrollve_scaleY="true"
discrollve:discrollve_translation="fromLeft" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@mipmap/camera"
discrollve:discrollve_scaleY="true"
discrollve:discrollve_translation="fromLeft" />
</com.swl.animfreamwork.AnimatorLinerLayout>
</com.swl.animfreamwork.AnimatorScrollView>
至此,整个动画框架的代码就完成了。
注释:ViewGroup的源码查看分析
/**
* <p>Adds a child view. If no layout parameters are already set on the child, the
* default parameters for this ViewGroup are set on the child.</p>
*
* <p><strong>Note:</strong> do not invoke this method from
* {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
* {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
*
* @param child the child view to add
*
* @see #generateDefaultLayoutParams()
*/
public void addView(View child) {
addView(child, -1);
}
接着 addView(child, -1)
/**
* Adds a child view. If no layout parameters are already set on the child, the
* default parameters for this ViewGroup are set on the child.
*
* <p><strong>Note:</strong> do not invoke this method from
* {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
* {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
*
* @param child the child view to add
* @param index the position at which to add the child
*
* @see #generateDefaultLayoutParams()
*/
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
再接着 addView(child, index, params)
/**
* Adds a child view with the specified layout parameters.
*
* <p><strong>Note:</strong> do not invoke this method from
* {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
* {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
*
* @param child the child view to add
* @param index the position at which to add the child or -1 to add last
* @param params the layout parameters to set on the child
*/
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
再查看addViewInner(child, index, params, false)
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
if (mTransition != null) {
// Don't prevent other add transitions from completing, but cancel remove
// transitions to let them complete the process before we add to the container
mTransition.cancel(LayoutTransition.DISAPPEARING);
}
if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
}
if (mTransition != null) {
mTransition.addChild(this, child);
}
if (!checkLayoutParams(params)) {
/*----------------------------------------------
关键代码,这里就是layoutParams真正创建的地方
---------------------------------------------------*/
params = generateLayoutParams(params);
}
if (preventRequestLayout) {
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
final boolean childHasFocus = child.hasFocus();
if (childHasFocus) {
requestChildFocus(child, child.findFocus());
}
AttachInfo ai = mAttachInfo;
if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
boolean lastKeepOn = ai.mKeepScreenOn;
ai.mKeepScreenOn = false;
child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
if (ai.mKeepScreenOn) {
needGlobalAttributesUpdate(true);
}
ai.mKeepScreenOn = lastKeepOn;
}
if (child.isLayoutDirectionInherited()) {
child.resetRtlProperties();
}
dispatchViewAdded(child);
if ((child.mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE) {
mGroupFlags |= FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE;
}
if (child.hasTransientState()) {
childHasTransientStateChanged(child, true);
}
if (child.getVisibility() != View.GONE) {
notifySubtreeAccessibilityStateChangedIfNeeded();
}
if (mTransientIndices != null) {
final int transientCount = mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
final int oldIndex = mTransientIndices.get(i);
if (index <= oldIndex) {
mTransientIndices.set(i, oldIndex + 1);
}
}
}
if (mCurrentDragStartEvent != null && child.getVisibility() == VISIBLE) {
notifyChildOfDragStart(child);
}
if (child.hasDefaultFocus()) {
// When adding a child that contains default focus, either during inflation or while
// manually assembling the hierarchy, update the ancestor default-focus chain.
setDefaultFocus(child);
}
}
再接着我们去查看ViewGroup.LayoutParams的源码,我们可以找到下面这个构造方法,
通过看注释就是在这里映射 XML属性(The XML attributes mapped to this set of layout parameters are)
/**
* Creates a new set of layout parameters. The values are extracted from
* the supplied attributes set and context. The XML attributes mapped
* to this set of layout parameters are:
*
* <ul>
* <li><code>layout_width</code>: the width, either an exact value,
* {@link #WRAP_CONTENT}, or {@link #FILL_PARENT} (replaced by
* {@link #MATCH_PARENT} in API Level 8)</li>
* <li><code>layout_height</code>: the height, either an exact value,
* {@link #WRAP_CONTENT}, or {@link #FILL_PARENT} (replaced by
* {@link #MATCH_PARENT} in API Level 8)</li>
* </ul>
*
* @param c the application environment
* @param attrs the set of attributes from which to extract the layout
* parameters' values
*/
public LayoutParams(Context c, AttributeSet attrs) {
//真正读取属性的地方就是在这里,所以我们就可以依葫芦画瓢,替换成我们自己的LayoutParams来获取我们的自定义属性
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
总结:
学着去查看分析android的源码,理解其中原理后,很多时候我们一些的需求就会迅速找到思路,阅读源码非常重要,非常重要,非常重要!