(二十四)自定義動畫框架
一、效果
這邊程式碼很簡單,主要學習這個動畫框架開發過程的思想,開發出來的動畫框架便於使用,利於擴充套件。
二、分析
實現滾動是使用了 ScrollView,如果說要使用 ListView 的話,理論上也是可以的,但是 Item 型別比較多的時候,估計會比較複雜。
ScrollView 下,裡面的每個 Item 可以有一些動畫效果,支援的動畫有四種:
1.透明度變化
2.X 或 Y 方向縮放
3.顏色漸變
4.平移進場
通過監聽 ScrollView 的滑動,呼叫對應 Item 的設定屬性方法。Item 執行動畫的程度跟這個 Item 從底部滑出來的高度有關,需要先計算 Item 滑出來多少。
每個 Item 執行的動畫不一致,這邊把 Item 要執行什麼動畫作為自定義屬性配置在各自的 Item 上面,這些 Item 是系統自帶的 View,如 TextView、ImageView 等。動畫由 MyFrameLayout 去控制。
<LinearLayout>
<MyFrameLayout
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="@drawable/camera"
/>
</MyFrameLayout>
</LinearLayout >
三、包裹 Item
1.MyFrameLayout
先定義個 MyFrameLayout 包裹類,這個類包裹每一個 Item,並控制動畫的執行。
public class MyFrameLayout extends FrameLayout {
public MyFrameLayout(@NonNull Context context) {
super(context);
}
}
2.MyLinearLayout
考慮到再 XML 佈局檔案中, LinearLayout 下每一個 Item 都需要在外新增一個 MyFrameLayout 才可以,這樣使用起來較為麻煩,所以 自定義 MyLinearLayout 擴充套件自 LinearLayout,預設為子 View 新增一個 MyFrameLayout。
public class MyLinearLayout extends LinearLayout {
public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//設定排版為豎著
setOrientation(VERTICAL);
}
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
MyFrameLayout mf = new MyFrameLayout(getContext());
mf.addView(child);
super.addView(mf, params);
}
}
這時候佈局檔案樣式為:
<MyLinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_gravity="center"
android:src="@drawable/camera"
discrollve:discrollve_scaleY="true"
discrollve:discrollve_translation="fromLeft"
/>
</MyLinearLayout>
四、自定義屬性
自定義屬性 attrs.xml
<?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>
上面為了方便使用,擴充套件了 LinearLayout,讓他自動為子 View 新增一個 MyFrameLayout,這時候自定義屬性只能寫在各個 Item 上,但是 Item 是系統的 View,自身無法識別到這些屬性,所以是讓 MyLinearLayout 去識別子 View 身上的屬性。即重寫 generateLayoutParams 方法。
1.自定義 LayoutParams
public 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 c, AttributeSet attrs) {
super(c, attrs);
//解析attrs得到自定義的屬性,儲存
TypedArray a = getContext().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();
}
}
同時,重寫 MyLinearLayout 的 generateLayoutParams 方法。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParams(getContext(),attrs);
}
2.MyFrameLayout 儲存自定義屬性
為 MyFrameLayout 新增自定義屬性,生成 set 方法。同時,重寫 onSizeChanged 方法,記錄寬高。
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) {
// TODO Auto-generated method stub
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
}
在 MyLinearLayout 的 addView 方法中,把自定義屬性儲存到 MyFrameLayout 中。添加了一個判斷是否有要執行的自定義動畫,沒有的話則不進行包裹。效能上的一點優化。
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
MyLayoutParams p = (MyLayoutParams) params;
if(!isDiscrollvable(p)){//判斷是否有自定義屬性,沒有則不包裹一層容器
super.addView(child,params);
}else {
//偷天換日
MyFrameLayout mf = new MyFrameLayout(getContext());
mf.addView(child);
mf.setmDiscrollveAlpha(p.mDiscrollveAlpha);
mf.setmDiscrollveFromBgColor(p.mDiscrollveFromBgColor);
mf.setmDiscrollveToBgColor(p.mDiscrollveToBgColor);
mf.setmDiscrollveScaleX(p.mDiscrollveScaleX);
mf.setmDisCrollveTranslation(p.mDisCrollveTranslation);
super.addView(mf, params);
}
}
/**
* 是否要執行自定義動畫
* @param p
* @return
*/
private boolean isDiscrollvable(MyLayoutParams p){
return p.mDiscrollveAlpha||
p.mDiscrollveScaleX||
p.mDiscrollveScaleY||
p.mDisCrollveTranslation!=-1||
(p.mDiscrollveFromBgColor!=-1&&
p.mDiscrollveToBgColor!=-1);
}
五、動畫
1.封裝動畫方法
在上面已經把要執行的動畫屬性傳給了 MyFrameLayout,為 MyFrameLayout 實現兩個方法,設定屬性值和初始化。
為了便於擴充套件,這邊採用介面。
介面 DiscrollInterface:
public interface DiscrollInterface {
/**
* 當滑動的時候呼叫該方法,用來控制裡面的控制元件執行相應的動畫
* @param ratio 動畫執行的百分比(child view畫出來的距離百分比)
*/
void onDiscroll(float ratio);
/**
* 重置動畫--讓view所有的屬性都恢復到原來的樣子
*/
void onResetDiscroll();
}
MyFrameLayout 實現 DiscrollInterface:
public class MyFrameLayout extends FrameLayout implements DiscrollInterface{
...
@Override
public void onDiscroll(float ratio) {
//執行動畫ratio:0~1
if(mDiscrollveAlpha){
setAlpha(ratio);
}
if(mDiscrollveScaleX){
setScaleX(ratio);
}
if(mDiscrollveScaleY){
setScaleY(ratio);
}
//平移動畫 int值:left,right,top,bottom left|bottom
if(isTranslationFrom(TRANSLATION_FROM_BOTTOM)){//是否包含bottom
setTranslationY(mHeight*(1-ratio));//height--->0(0代表恢復到原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_TOP)){//是否包含bottom
setTranslationY(-mHeight*(1-ratio));//-height--->0(0代表恢復到原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_LEFT)){
setTranslationX(-mWidth*(1-ratio));//mWidth--->0(0代表恢復到本來原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_RIGHT)){
setTranslationX(mWidth*(1-ratio));//-mWidth--->0(0代表恢復到本來原來的位置)
}
//判斷從什麼顏色到什麼顏色
if(mDiscrollveFromBgColor!=-1&&mDiscrollveToBgColor!=-1){
setBackgroundColor((int) sArgbEvaluator.evaluate(ratio, mDiscrollveFromBgColor, mDiscrollveToBgColor));
}
}
private boolean isTranslationFrom(int translationMask){
if(mDisCrollveTranslation ==-1){
return false;
}
//fromLeft|fromeBottom & fromBottom = fromBottom
return (mDisCrollveTranslation & translationMask) == translationMask;
}
@Override
public void onResetDiscroll() {
if(mDiscrollveAlpha){
setAlpha(1);
}
if(mDiscrollveScaleX){
setScaleX(1);
}
if(mDiscrollveScaleY){
setScaleY(1);
}
//平移動畫 int值:left,right,top,bottom left|bottom
if(isTranslationFrom(TRANSLATION_FROM_BOTTOM)){//是否包含bottom
setTranslationY(0);//height--->0(0代表恢復到原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_TOP)){//是否包含bottom
setTranslationY(0);//-height--->0(0代表恢復到原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_LEFT)){
setTranslationX(0);//mWidth--->0(0代表恢復到本來原來的位置)
}
if(isTranslationFrom(TRANSLATION_FROM_RIGHT)){
setTranslationX(0);//-mWidth--->0(0代表恢復到本來原來的位置)
}
}
}
六、ScrollView 監聽
到這裡,就缺一個滑動時候對執行動畫的監聽,為了方便使用,擴充套件 ScrollView ,實現滑動監聽執行動畫效果。
這裡比較複雜的就是計算最後一個 Item 滑出來的距離,從而確認執行動畫的百分比 ratio。ratio = child 浮現的高度/ child 的高度,浮現的高度沒有辦法直接獲取,只能用 ScrollView 的高度減去 Child 離 ScrollView 的頂部距離(紅色箭頭距離)再減去 ScrollView 滑出去的距離(綠色部分)。
public class MyScrollView extends ScrollView {
private MyLinearLayout mContent;
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = (MyLinearLayout) getChildAt(0);
}
@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 scrollViewHeight = getHeight();
for (int i=0;i<mContent.getChildCount();i++){
View child = mContent.getChildAt(i);
int childHeight = child.getHeight();
if(!(child instanceof DiscrollInterface)){
continue;
}
DiscrollInterface discrollInterface = (DiscrollInterface) child;
//child離parent頂部的高度
int childTop = child.getTop();
//滑出去的這一截高度:t (t 為負的)
//child離螢幕頂部的高度
int absoluteTop = childTop - t;
if(absoluteTop <= scrollViewHeight) {
//child浮現的高度 = ScrollView 的高度 - child 離螢幕頂部的高度
int visibleGap = scrollViewHeight - absoluteTop;
//float ratio = child浮現的高度/child的高度
float ratio = visibleGap / (float) childHeight;
//確保ratio是在0~1的範圍
discrollInterface.onDiscroll(clamp(ratio, 1f, 0f));
}else{
discrollInterface.onResetDiscroll();
}
}
}
/**
* 求三個數的中間大小的一個數
* @param value 輸入的值
* @param max 最大值限制
* @param min 最小值限制
*/
public static float clamp(float value, float max, float min){
return Math.max(Math.min(value, max), min);
}
}
注:在 onScrollChanged 中迴圈遍歷對每一個子 View 都進行操作,實際上只需要對最後一個 Item 進行動畫屬性的設定,這邊對所有的 Item 都進行了設定,在效能上實際是由一點影響的,正常情況下,這裡的 Item 不會很多,如果說 Item 比較多的話,可以考慮像 ListView 記錄最後一個 Item,在滑動的時候根據計算進行重新獲取,每次繪製的時候只需要呼叫這個 Item 執行動畫即可。