效果圖:

代碼地址AnimationDemo
實現(xiàn)的思路是通過監(jiān)聽ScrollView 的滑動距離,對View的屬性進行更改,從而達到動畫效果。
當然了,可以獲取控件,監(jiān)聽ScrollView的滑動直接對其屬性進行操作,偽代碼:
View view1 = findViewById(R.id.id1);
view1.setAlpha(0.5f);
view1.setTranslationX(15);
View view2 = findViewById(R.id.id2);
view2.setScaleX(0.5f);
view2.setScaleY(15);
但是這種方式代碼量大,并且耦合程度高,當需要修改某一個view 的動畫時,就需要直接修改源文件,本著復(fù)用性和易用性的原則,這里考慮直接通過 xml布局文件 配置的方式來完成,例如:
<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" />
通過配置自定義 view 屬性,discrollve_alpha 、discrollve_scaleY、discrollve_translation等屬性來自動完成動畫的操作。
自定義屬性
<resources>
<declare-styleable name="AnimatorFrame">
<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>
設(shè)計定義自定義屬性
discrollve_alpha 透明度
discrollve_scaleX discrollve_scaleY 縮放
discrollve_fromBgColor discrollve_toBgColor 顏色的漸變
discrollve_translation 平移
通過xml 文件配置動畫實現(xiàn)有兩個難點:
- 如何獲取自定義屬性
- 如何根據(jù)自定義屬性進行動畫
接下來圍繞這兩個難點來進行分析
如何獲取自定義屬性
自定義屬性不被系統(tǒng)控件所接收,我們需要手動去解析獲取自定義屬性并保存,通常我們自定義view 獲取自定義屬性是在構(gòu)造函數(shù)中 完成
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.MyView);
.....
a.recycle();
}
但是這里我們需要獲取自定義View 的子View xml中的屬性,那么系統(tǒng)控件是怎么解析這些屬性的呢,我們來分析一下源碼
UI 的繪制流程
PhoneWindow 的setContentView 方法

重點關(guān)心代碼418行, 調(diào)用的是 LayoutInflater 的inflate方法,接下來看LayoutInflater的 inflate方法

421行: 根據(jù) layoutResource 獲取 xml文件解析
422行:調(diào)用 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法進行繪制
轉(zhuǎn)到 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法,這個方法稍微有點長,就截取重點部分

492 行 根據(jù) xml 的tag 創(chuàng)建view
496-508 根據(jù) 父view 創(chuàng)建 layoutparams
515行 繪制子View,我們重點關(guān)心這個方法
rInflateChildren方法會調(diào)用LayoutInflater的 rInflate方法

863- 根據(jù)tag 創(chuàng)建子view
865行- 通過 viewGroup的 generateLayoutParams(attrs)方法來創(chuàng)建LayoutParams
866行- 將剛剛創(chuàng)建的子View添加到viewGroup中
在 863行viewGroup的 generateLayoutParams(attrs)方法看到了一個參數(shù) attrs,而我們上面自定義view 獲取自定義屬性正好需要 attars。
而LinearLayout也是這么操作的,查看 LinearLayout.LayoutParams的源碼
/**
* {@inheritDoc}
*/
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =
c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
a.recycle();
}
通過分析源碼,可以通過 重寫 viewGroup 的generateLayoutParams(attrs)的方法創(chuàng)建自定義的LayoutParams,并且在LayoutParam中保存我們自定義屬性
代碼實現(xiàn)
自定義LinearLayout
public class AnimatorLinearLayout extends LinearLayout {
public AnimatorLinearLayout(Context context) {
super(context);
}
public AnimatorLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public AnimatorLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
//創(chuàng)建自定義的LayoutParams ,解析并保存自定義屬性
return new AnimatorLayoutParam(getContext(), attrs);
}
}
自定義的LayoutParams
public class AnimatorLayoutParam extends LinearLayout.LayoutParams {
public boolean mDiscrollveAlpha; //透明度
public boolean mDiscrollveScaleX; //X縮放
public boolean mDiscrollveScaleY; //Y縮放
public int mDisCrollveTranslation; //平移
public int mDiscrollveFromBgColor; //漸變起始色
public int mDiscrollveToBgColor; //漸變結(jié)束色
public AnimatorLayoutParam(Context c, AttributeSet attrs) {
super(c, attrs);
//獲取自定義屬性并且保存
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AnimatorFrame);
//沒有傳屬性過來,給默認值FALSE
mDiscrollveAlpha = a.getBoolean(R.styleable.AnimatorFrame_discrollve_alpha, false);
mDiscrollveScaleX = a.getBoolean(R.styleable.AnimatorFrame_discrollve_scaleX, false);
mDiscrollveScaleY = a.getBoolean(R.styleable.AnimatorFrame_discrollve_scaleY, false);
mDisCrollveTranslation = a.getInt(R.styleable.AnimatorFrame_discrollve_translation, -1);
mDiscrollveFromBgColor = a.getColor(R.styleable.AnimatorFrame_discrollve_fromBgColor, -1);
mDiscrollveToBgColor = a.getColor(R.styleable.AnimatorFrame_discrollve_toBgColor, -1);
a.recycle();
}
}
如何根據(jù)自定義屬性進行動畫
- 監(jiān)聽scrollView的滑動
- 計算比例
- 修改view屬性
監(jiān)聽滑動
繼承 ScrollView 重寫onScrollChanged 方法
public class AnimatorScrollView extends ScrollView {
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);
}
AnimatorLinearLayout mLinearLayout;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
try {
mLinearLayout = (AnimatorLinearLayout) getChildAt(0);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//注意這里的 t 值為 scrollView 最頂部 內(nèi)容的坐標
//例如 scroll向上滑動了 50px 那么 t 的值就為 50
int scrollViewHeight = getHeight();
}
}
注意 方法的參數(shù)t 這里的參數(shù)t 表示內(nèi)容滑動的距離,
黑色邊框表示屏幕,紫色邊框表示scrollView內(nèi)容,t 實際為 黑色箭頭到紅色標線之間的距離

計算比例
需要計算 控件滑出的比例,即 控件顯示在屏幕上的高度

通過 child.getTop能夠獲取到 當前view在scrollView中的坐標
int disTop = childAtTop - t 計算子視圖 頂部距離scrollView頂部 距離
screenHeight - disTop 既可以計算出 view在屏幕中顯示的高度
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//注意這里的 t 值為 scrollView 最頂部 內(nèi)容的坐標
//例如 scroll向上滑動了 50px 那么 t 的值就為 50
int scrollViewHeight = getHeight();
for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
View childAt = mLinearLayout.getChildAt(i);
int childAtTop = childAt.getTop();
int disTop = childAtTop - t; //子視圖 頂部距離 界面頂部 距離
if (disTop < 0) {
//視圖已經(jīng)被向上劃出屏幕
} else if (disTop < scrollViewHeight ) {
//說明視圖 處于可見位置
int visiableGap = scrollViewHeight - disTop;
if(visiableGap<=childAt.getHeight()) {
//計算滑動的比例
float ratio = visiableGap * 1.0f / childAt.getHeight();
}else {
}
}
}
}
執(zhí)行動畫
1、可以直接在 onScrollChanged監(jiān)聽中執(zhí)行每個view的動畫,但是為了代碼的復(fù)用性和可維護性,采用接口抽取的形式
public interface AnimatorProvider{
//執(zhí)行動畫
abstract void onAnim(float ratio);
//重置動畫
abstract void resetAnim();
}
接口的實現(xiàn)
@Override
void onAnim(float ratio) {
//執(zhí)行動畫ratio:0~1
if(mDiscrollveAlpha){
mView.setAlpha(ratio);
}
if(mDiscrollveScaleX){
Log.e("TAG", "scaleX:"+ratio);
mView.setScaleX(ratio);
}
if(mDiscrollveScaleY){
Log.e("TAG", "scaleY:"+ratio);
mView.setScaleY(ratio);
}
//平移動畫 int值:left,right,top,bottom left|bottom
if(isTranslationFrom(TRANSLATION_FROM_BOTTOM)){//是否包含bottom
mView. setTranslationY(mHeight*(1-ratio));//height--->0(0代表恢復(fù)到原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_TOP)){//是否包含bottom
mView.setTranslationY(-mHeight*(1-ratio));//-height--->0(0代表恢復(fù)到原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_LEFT)){
mView. setTranslationX(-mWidth*(1-ratio));//mWidth--->0(0代表恢復(fù)到本來原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_RIGHT)){
mView.setTranslationX(mWidth*(1-ratio));//-mWidth--->0(0代表恢復(fù)到本來原來的位置)
}
//判斷從什么顏色到什么顏色
if(mDiscrollveFromBgColor!=-1&&mDiscrollveToBgColor!=-1){
mView.setBackgroundColor((int) sArgbEvaluator.evaluate(ratio, mDiscrollveFromBgColor, mDiscrollveToBgColor));
}
}
/**
* <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 boolean isTranslationFrom(int translationMask){
if(mDisCrollveTranslation ==-1){
return false;
}
//fromLeft|fromeBottom & fromBottom = fromBottom
return (mDisCrollveTranslation & translationMask) == translationMask;
}
isTranslationFrom方法 根據(jù)位運算結(jié)果特性來判斷移動的方向
2、考慮性能的問題,對于不需要執(zhí)行動畫的 view 不執(zhí)行比例的計算,使用容器保存需要動畫的view。
在自定義的LinearLayout中初始化容器并保存需要動畫的view,對該類改造如下
public class AnimatorLinearLayout extends LinearLayout {
private Map<View, AnimatorProvider> mAnimatorProviderMap = new HashMap<>();
public Map<View, AnimatorProvider> getAnimatorProviderMap() {
return mAnimatorProviderMap;
}
public AnimatorLinearLayout(Context context) {
super(context);
}
public AnimatorLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public AnimatorLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new AnimatorLayoutParam(getContext(), attrs);
}
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
AnimatorLayoutParam layoutParam = (AnimatorLayoutParam) params;
if (isDiscrollvable(layoutParam)) {
mAnimatorProviderMap.put(child, new AnimatorProviderImpl(child,layoutParam));
}
super.addView(child, params);
}
private boolean isDiscrollvable(AnimatorLayoutParam layoutParams) {
return layoutParams.mDiscrollveAlpha ||
layoutParams.mDiscrollveScaleX ||
layoutParams.mDiscrollveScaleY ||
layoutParams.mDisCrollveTranslation != -1 ||
(layoutParams.mDiscrollveFromBgColor != -1 &&
layoutParams.mDiscrollveToBgColor != -1);
}
}
scrollView中執(zhí)行動畫代碼改造
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//注意這里的 t 值為 scrollView 最頂部 內(nèi)容的坐標
//例如 scroll向上滑動了 50px 那么 t 的值就為 50
int scrollViewHeight = getHeight();
for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
View childAt = mLinearLayout.getChildAt(i);
if (mAnimatorProviderMap.containsKey(childAt)) {
//需要執(zhí)行動畫
AnimatorProvider provider = mAnimatorProviderMap.get(childAt);
int childAtTop = childAt.getTop();
int disTop = childAtTop - t; //子視圖 頂部距離 界面頂部 距離
if (disTop < 0) {
} else if (disTop < scrollViewHeight ) {
//說明視圖 處于可見位置
int visiableGap = scrollViewHeight - disTop;
if(visiableGap<=childAt.getHeight()) {
float ratio = visiableGap * 1.0f / childAt.getHeight();
provider.onAnim(ratio);
}else {
provider.onAnim(1.0f);
}
} else {
// //說明視圖 在下方 不可見
provider.resetAnim();
}
} else {
continue;
}
}
}