Android魔法(第四弹)—— 一步步实现百叶窗效果
目錄
1、效果展示
2、實現(xiàn)AnimationViewInterface接口
3、解析動畫組成
4、翻轉(zhuǎn)單元——RotateView
1)前景背景圖
2)實現(xiàn)翻轉(zhuǎn)
3)實現(xiàn)翻轉(zhuǎn)動畫
5、百葉窗——BlindsView
1)初始化圖片矩陣
2)手動翻轉(zhuǎn)百葉窗
3)自動翻轉(zhuǎn)百葉窗
6、總結(jié)一下
源碼:
本篇是基于AnimationListView框架的,這個框架在上一篇中詳細(xì)的講解了,建議閱讀本篇前先熟悉Android魔法(第三彈)—— 一步步實現(xiàn)對折頁面。
1、效果展示
在上一章中我們實現(xiàn)對折的效果同時實現(xiàn)了一個AnimationListView的框架,在這個框架下我們可以實現(xiàn)很多效果。 本篇文章我們就在這個框架下實現(xiàn)一個百葉窗的效果,效果如下:2、實現(xiàn)AnimationViewInterface接口
如果想在AnimationListView中應(yīng)用一種效果,那么就需要實現(xiàn)AnimationViewInterface接口,如下 public?class?BlindsView?extends?LinearLayout?implements?AnimationViewInterface{ BlindsView的具體實現(xiàn)我們稍后在講解,先看看BlindsView繼承LinearLayout,為什么呢?3、解析動畫組成
我們來看其中一幀的畫面,如下 可以看到整個百葉窗效果其實是由一個個小的方形組成的,這些方塊做水平翻轉(zhuǎn)的動作,并且在不同列有一個效果的時差,就形成了百葉窗的效果。 所以我們BlindsView實際上包含許多這樣的子view,真正的動畫是這些子view翻轉(zhuǎn)產(chǎn)生的,所以BlindsView要繼承LinearLayout來實現(xiàn)這種宮格布局。4、翻轉(zhuǎn)單元——RotateView
上面提到的子view,我們定義為RotateView,繼承ImageView以便來裝載圖片。如下: public?class?RotateView?extends?ImageView?{1)前景背景圖
觀察下圖中指示位置的方塊,并對比上一張同一位置的方塊。 可以發(fā)現(xiàn)當(dāng)翻轉(zhuǎn)過180度的時候,該方塊顯示了另外一張圖片,實際上是下一頁該位置的部分。所以每個RotateView需要前景和背景兩張圖片,代碼如下: public?void?setBitmap(Bitmap?frontBitmap,?Bitmap?backBitmap){if(frontBitmap?==?null){return;}mFrontBitmap?=?frontBitmap;mBackBitmap?=?backBitmap;mRotatedBackBitmap?=?null;setImageBitmap(frontBitmap);setScaleType(ScaleType.FIT_XY);//初始化翻轉(zhuǎn)角度setRotationX(0);setRotationY(0); }代碼比較簡單,默認(rèn)顯示前景圖。
其中有個mRotateBackBitmap,我們后面會講。
2)實現(xiàn)翻轉(zhuǎn)
代碼如下: public?void?setRotation(float?value,?boolean?isRotateX){//設(shè)置翻轉(zhuǎn)角度if(isRotateX){setRotationX(value);}else?{setRotationY(value);}//將角度轉(zhuǎn)換為0-360之間,以便后面判斷float?rotate?=?value?%?360;if?(rotate?<?0)?{rotate?+=?360;}/***?設(shè)置縮放:當(dāng)向垂直翻轉(zhuǎn)時縮小,反之恢復(fù)*?縮放的主要原因是在翻轉(zhuǎn)時,圖像會變形為梯形,這時圖片中心軸保持原來的寬度,*?則向上翻轉(zhuǎn)那邊會變大,部分圖像會超出無法顯示。所以這里用縮放處理一下,*?至于縮放大小,根據(jù)實際需求改變。*/float?scale?=?rotate?>?180???Math.abs(rotate?-?270)?:?Math.abs(rotate?-?90);scale?=?scale?/?90?*?(1?-?mScaleMin)?+?mScaleMin;if(isRotateX){setScaleX(scale);}else{setScaleY(scale);}//根據(jù)翻轉(zhuǎn)的位置,設(shè)置前景/背景圖片if(mBackBitmap?!=?null)?{if(mRotatedBackBitmap?==?null?||?this.isRotateX?!=?isRotateX)?{/***?首先會根據(jù)翻轉(zhuǎn)的方向,對背景圖片進(jìn)行一次翻轉(zhuǎn)*?這樣當(dāng)翻轉(zhuǎn)時背景圖片不會左右上下顛倒*/Matrix?matrix?=?new?Matrix();if?(isRotateX)?{matrix.postScale(1,?-1);}?else?{matrix.postScale(-1,?1);}mRotatedBackBitmap?=?Bitmap.createBitmap(mBackBitmap,?0,?0,mBackBitmap.getWidth(),?mBackBitmap.getHeight(),?matrix,?true);}/***?當(dāng)翻轉(zhuǎn)在2、3象限顯示背景圖,在1、4象限顯示前景圖*/if?(rotate?>?90?&&?rotate?<?270)?{setImageBitmap(mRotatedBackBitmap);}?else?{setImageBitmap(mFrontBitmap);}}this.isRotateX?=?isRotateX; }兩個參數(shù),第一個參數(shù)是翻轉(zhuǎn)的角度,第二個參數(shù)是翻轉(zhuǎn)的方向(水平還是垂直)。
翻轉(zhuǎn)很簡單,調(diào)用setRotationX或setRotationY函數(shù)即可,主要是前景圖和背景圖的切換。
注意第二部分代碼,這里做了縮放的處理,是因為翻轉(zhuǎn)時由于實現(xiàn)了近大遠(yuǎn)小的效果,所以翻轉(zhuǎn)時處于外側(cè)的一邊會增大并超出區(qū)域,這樣視覺上效果不好,所以做了縮放處理,保證整個翻轉(zhuǎn)過程可以完整的呈現(xiàn)在區(qū)域內(nèi)。大家可以試著將這部分代碼去掉對比一下效果,這里就不展示了。
最后一步代碼則是根據(jù)反轉(zhuǎn)的角度不同設(shè)置不同的圖片。重點關(guān)注背景圖,由于背景圖實際上應(yīng)該是水平鏡像的,所以使用要提前水平翻轉(zhuǎn)一下,翻轉(zhuǎn)后的就是mRotateBackBitmap。為了防止每次都做一次翻轉(zhuǎn)操作,判斷如果已經(jīng)有mRotateBackBitmap并且翻轉(zhuǎn)方向未變則不必再執(zhí)行。所以如果改變了背景圖,要重置mRotateBackBitmap為null,就是上面setBitmap函數(shù)提到的。
這樣當(dāng)我們調(diào)用setRotate方法設(shè)置不同的角度就能得到不同的翻轉(zhuǎn)效果。
3)實現(xiàn)翻轉(zhuǎn)動畫
對于RotateView其實只需要setRotate函數(shù),動畫部分在BlindsView中處理并調(diào)用setRotate即可。但是我們也希望這個類可以單獨使用,所以我加入了它自身的動畫處理,如下: public?void?rotateXAnimation(float?fromRotate,?float?toRotate,?long?duration){rotateAnimation(fromRotate,?toRotate,?duration,?0,?true); }/***?翻轉(zhuǎn)動畫*?@param?fromRotate?開始角度*?@param?toRotate? ?結(jié)束角度*?@param?duration*?@param?delay? ? ?動畫延時*?@param?isRotateX?是否以X為軸*/ private?void?rotateAnimation(float?fromRotate,?float?toRotate,?long?duration,?long?delay,?boolean?isRotateX){if(mAnimator?!=?null){mAnimator.cancel();}mAnimator?=?ValueAnimator.ofFloat(fromRotate,?toRotate);mAnimator.setStartDelay(delay);mAnimator.setDuration(duration);mAnimator.start();mAnimator.addUpdateListener(new?RotateListener(isRotateX)); }class?RotateListener?implements?ValueAnimator.AnimatorUpdateListener{private?boolean?isRotateX;public?RotateListener(boolean?isRotateX){this.isRotateX?=?isRotateX;}@Overridepublic?void?onAnimationUpdate(ValueAnimator?animation)?{float?value?=?(Float)(animation.getAnimatedValue());setRotation(value,?isRotateX);} }其實也很簡單,用屬性動畫來實現(xiàn)即可。這里直接用ValueAnimator,這樣動畫的值會從fromRotate逐漸改變至toRotate。為動畫設(shè)置一個監(jiān)聽器,并調(diào)用setRotate函數(shù)就實現(xiàn)了翻轉(zhuǎn)的動畫。
5、百葉窗——BlindsView
上面我們完成了翻轉(zhuǎn)單元——RotateView,下面講解如何用這些單元來組成百葉窗的效果。1)初始化圖片矩陣
將整個的前景和背景圖片切割成小圖片設(shè)置給RotateView,并將這些RotateView以矩陣形式布局到BlindsView中,代碼如下: public?void?setBitmap(Bitmap?frontBitmap,?Bitmap?backBitmap){//處理圖片List<Bitmap>?subFrontBitmaps?=?getSubBitmaps(mRowCount,?mColumnCount,?frontBitmap);List<Bitmap>?subBackBitmaps?=?getSubBitmaps(mRowCount,?mColumnCount,?backBitmap);setBitmaps(mRowCount,?mColumnCount,?subFrontBitmaps,?subBackBitmaps); }/***?獲取圖片陣列*?將大圖片分割為rowCount*columnCount陣列的小圖片*?@param?rowCount*?@param?columnCount*?@param?bitmap*?@return*/ private?List<Bitmap>?getSubBitmaps(int?rowCount,?int?columnCount,?Bitmap?bitmap){List<Bitmap>?subBitmaps?=?new?ArrayList<Bitmap>();int?subWidth?=?bitmap.getWidth()?/?columnCount;int?subHeight?=?bitmap.getHeight()?/?rowCount;for(int?i?=?0;?i?<?rowCount;?i++){for(int?j?=?0;?j?<?columnCount;?j++){/***?這里計算每個葉面圖片的大小*?由于有余數(shù),所以最后一張圖片大小單獨計算*/int?height?=?i?==?rowCount?-?1???bitmap.getHeight()?-?subHeight?*?i?:?subHeight;int?width?=?j?==?columnCount?-?1???bitmap.getWidth()?-?subWidth?*?j?:?subWidth;Bitmap?subBitmap?=?Bitmap.createBitmap(bitmap,?subWidth?*?j,?subHeight?*?i,?width,?height);subBitmaps.add(subBitmap);}}return?subBitmaps; }/***?設(shè)置圖片陣列*?將前景和背景圖片的陣列放入每個rotateview中*?@param?rowCount*?@param?columnCount*?@param?mFrontBitmaps*?@param?mBackBitmaps*/ private?void?setBitmaps(int?rowCount,?int?columnCount,?List<Bitmap>?mFrontBitmaps,?List<Bitmap>?mBackBitmaps){/***?為了復(fù)用,需要做些處理*?首先判斷現(xiàn)有行/列是否多余,多余直接remove,不足補充*///最大行數(shù),是取現(xiàn)有行數(shù)和目標(biāo)行數(shù)的最大值。int?maxRow?=?Math.max(getChildCount()?,?rowCount);LinearLayout.LayoutParams?params?=?new?LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,?1);params.weight?=?1;for(int?i?=?0;?i?<?maxRow;?i++){LinearLayout?subView?=?null;if(i?>=?getChildCount()?&&?i?<?rowCount){//如果現(xiàn)有行數(shù)不足,則補充。每一行都是水平的linearlayoutsubView?=?new?LinearLayout(getContext());subView.setOrientation(HORIZONTAL);addView(subView,?params);}else?if(i?<?getChildCount()?&&?i?>=?rowCount){//如果現(xiàn)有行數(shù)過多,則移除removeViewAt(i);i--;maxRow--;}else{subView?=?(LinearLayout)getChildAt(i);}//開始處理每一行中的每項if(subView?!=?null){//最大列數(shù),是取現(xiàn)有列數(shù)和目標(biāo)列數(shù)的最大值。int?maxColumn?=?Math.max(subView.getChildCount()?,?columnCount);LinearLayout.LayoutParams?subParams?=?new?LinearLayout.LayoutParams(1,?LinearLayout.LayoutParams.MATCH_PARENT);subParams.weight?=?1;for(int?j?=?0;?j?<?maxColumn;?j++){RotateView?rotateView?=?null;if(j?>=?columnCount?&&?j?<?subView.getChildCount()){//如果現(xiàn)有列過多,則移除subView.removeViewAt(j);j--;maxColumn--;}else?if(j?<?columnCount?&&?j?>=?subView.getChildCount()){//如果現(xiàn)有列不足,則補充。每個葉面是RotateViewrotateView?=?new?RotateView(getContext());subView.addView(rotateView,?subParams);}else{rotateView?=?(RotateView)subView.getChildAt(j);}//為重新整理好的矩陣填充圖片if(rotateView?!=?null){int?index?=?i?*?columnCount?+?j;rotateView.setBitmap(mFrontBitmaps.get(index),?mBackBitmaps.get(index));rotateView.setScaleMin(0.5f);}}}} }可以看到先調(diào)用getSubBitmaps函數(shù)分別將前景和背景圖切割并返回一個list。
然后調(diào)用setBitmaps函數(shù),根據(jù)指定的行和列循環(huán)新建RotateView,傳入對應(yīng)的圖片并添加到布局中。
注意,這里復(fù)用之前已經(jīng)存在的RotateView,如果不足則補充,多余的remove掉。
這部分雖然代碼較多,但是實際上就是每行add一個水平的LinearLayout(BlindsView本身是垂直的),然后逐行一個個add或復(fù)用RotateView并為其setBitmap。
2)手動翻轉(zhuǎn)百葉窗
與上一篇對折效果一樣,整個百葉窗效果的移動包括手動和自動兩個部分。當(dāng)用戶touch屏幕并移動時,百葉窗跟隨touch的move事件去移動;當(dāng)用戶touch up或end時,會通過一個animation自動完成剩余的部分。 手動移動階段的需要實現(xiàn)AnimationViewInterface的setAnimationPrecent方法,如下: @Override public?void?setAnimationPercent(float?percent,?MotionEvent?event,?boolean?isVertical){mAnimationPercent?=?percent;//獲取總的轉(zhuǎn)動的角度float?value?=?mAnimationPercent?*?getTotalVaule(isVertical);/***?遍歷每一個小葉面設(shè)置當(dāng)前的角度*?根據(jù)轉(zhuǎn)動的方向不同,從不同的位置開始翻轉(zhuǎn)*/for(int?i?=?0;?i?<?mRowCount;?i++){LinearLayout?parent?=?(LinearLayout)getChildAt(i);for(int?j?=?0;?j?<?mColumnCount;?j++){RotateView?view?=?(RotateView)parent.getChildAt(j);float?subValue;if(value?>?0){if(isVertical){//向下滑動。從第一行開始轉(zhuǎn)動,每行轉(zhuǎn)動角度依次遞減subValue?=?value?-?mSpace?*?i;}else{//向右滑動。從第一列開始轉(zhuǎn)動,每列轉(zhuǎn)動角度依次遞減subValue?=?value?-?mSpace?*?j;}//保證轉(zhuǎn)動角度在0到180度內(nèi)if(subValue?<?0){subValue?=?0;}else?if(subValue?>?180){subValue?=?180;}}else{if(isVertical){//向下滑動。從最后一行開始轉(zhuǎn)動,每行轉(zhuǎn)動角度依次遞減(注意由于value是負(fù)數(shù),所以數(shù)值上是遞增)subValue?=?value?+?mSpace?*?(mRowCount?-?i?-?1);}else{//向左滑動。從最后一列開始轉(zhuǎn)動,每列轉(zhuǎn)動角度依次遞減(注意由于value是負(fù)數(shù),所以數(shù)值上是遞增)subValue?=?value?+?mSpace?*?(mColumnCount?-?j?-?1);}//保證轉(zhuǎn)動角度在0到-180度內(nèi)if(subValue?<?-180){subValue?=?-180;}else?if(subValue?>?0){subValue?=?0;}}//注意,如果是上下翻動,角度需要轉(zhuǎn)為負(fù)值,否則轉(zhuǎn)動的方向有誤view.setRotation(isVertical???-subValue?:?subValue,?isVertical);}} }可以看到,一開始我們就通過getTotalValue計算出一個總的轉(zhuǎn)動角度,這個函數(shù)代碼如下:
private?float?getTotalVaule(boolean?isVertical){if(isVertical)?{return?mSpace?*?(mRowCount?-?1)?+?180;}else{return?mSpace?*?(mColumnCount?-?1)?+?180;} }這塊需要解釋一下。從上面的圖片可以看到,每一列旋轉(zhuǎn)的角度時不同的,相鄰列會差一個角度,就是mSpace。
那么getTotalValue函數(shù)計算的是一個什么值?
在一個完整翻轉(zhuǎn)過程中,當(dāng)?shù)谝涣蟹D(zhuǎn)完成,其他列還沒有,所以過程并未結(jié)束。
這時假設(shè)第一列繼續(xù)翻轉(zhuǎn),當(dāng)?shù)诙蟹D(zhuǎn)完成,第一列已經(jīng)翻轉(zhuǎn)了mSpace * 1 + 180。那么繼續(xù)直到最后一列也完全翻轉(zhuǎn)過來,那么第一列實際翻轉(zhuǎn)了mSpace * (columnCount - 1) + 180。
所以mAnimationPercent * getTotalVaule(isVertical)實際上就是第一列當(dāng)前的翻轉(zhuǎn)角度了,這樣就可以計算出其他列的翻轉(zhuǎn)角度。為每個RotateView設(shè)置rotation即可。
但是注意這并不是真正的翻轉(zhuǎn)角度,當(dāng)已經(jīng)完全翻轉(zhuǎn)180度后就不再需要翻轉(zhuǎn)。
代碼中處理了四個方向的翻轉(zhuǎn),所以計算上多少有些不同,思路是一樣的。
3)自動翻轉(zhuǎn)百葉窗
自動階段通過實現(xiàn)startAnimation函數(shù),代碼如下: @Override public?void?startAnimation(boolean?isVertical,?MotionEvent?event,?float?toPercent){if(mAnimator?!=?null?&&?mAnimator.isRunning()){return;}mAnimator?=?ValueAnimator.ofFloat(mAnimationPercent,?toPercent);//動畫持續(xù)時間根據(jù)起始位置不同mAnimator.setDuration((long)?(Math.abs(toPercent?-?mAnimationPercent)?*?mDuration));mAnimator.start();OnAnimationListener?onAnimationListener?=?new?OnAnimationListener(isVertical,?toPercent);mAnimator.addUpdateListener(onAnimationListener);mAnimator.addListener(onAnimationListener); }class?OnAnimationListener?implements?ValueAnimator.AnimatorUpdateListener,?Animator.AnimatorListener{private?boolean?isVertical;private?float?toPercent;public?OnAnimationListener(boolean?isVertical,?float?toPercent){this.isVertical?=?isVertical;this.toPercent?=?toPercent;}@Overridepublic?void?onAnimationUpdate(ValueAnimator?animation)?{setAnimationPercent((float)animation.getAnimatedValue(),?null,?isVertical);}@Overridepublic?void?onAnimationStart(Animator?animation)?{}@Overridepublic?void?onAnimationEnd(Animator?animation)?{mAnimationPercent?=?0;if(mOnAnimationViewListener?==?null){return;}if(toPercent?==?1){mOnAnimationViewListener.pagePrevious();}else?if(toPercent?==?-1){mOnAnimationViewListener.pageNext();}}@Overridepublic?void?onAnimationCancel(Animator?animation)?{mAnimationPercent?=?0;}@Overridepublic?void?onAnimationRepeat(Animator?animation)?{} }通過代碼可以看到就是通過監(jiān)聽一個float的屬性動畫,然后通過setAnimationPrecent改變翻轉(zhuǎn)狀態(tài)即可。
注意在動畫結(jié)束時調(diào)用切頁的回調(diào)。
這部分與上一篇對折效果類似,就不細(xì)說了。
6、總結(jié)一下
通過這兩篇文章,大家應(yīng)該對AnimationListView這個框架有了了解。通過這個框架我們還可以實現(xiàn)更多更酷的效果,代碼大體上可以參考這兩個效果。關(guān)于這個框架及實現(xiàn)我們暫時告一段落,接下來會分析一些其他的,以后有機(jī)會我們可以在這個框架上實現(xiàn)更多的效果,大家如果有什么好的想法或自己實現(xiàn)的效果可以留言。源碼:
關(guān)注公眾號:BennuCTech,發(fā)送“FastWidget”獲取完整源碼總結(jié)
以上是生活随笔為你收集整理的Android魔法(第四弹)—— 一步步实现百叶窗效果的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android魔法(第三弹)—— 一步步
- 下一篇: 自定义Toolbar的一些小技巧