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