在上一篇文章里面,基本上算是實現(xiàn)了該效果的布局,有了布局,接下來就要對布局進行移動處理。</br>
android 仿當樂游戲詳情頁面(一)
對于移動的分析
通過第一篇文章的分析,在所有控件里面,能移動的只有用于展示游戲簡介和游戲相關數(shù)據(jù)的View,并且該View的移動有以下三種狀態(tài):</br>
-
處于頂部的狀態(tài)</br>
頂部狀態(tài) -
中間狀態(tài)</br>
中間狀態(tài) -
底部狀態(tài):</br>
底部狀態(tài)
如上面幾張圖片所示,處于頂部狀態(tài),TabLayout 懸停在Toolbar的下面,而此時,用于介紹游戲簡介的View被移出布局;處于中間狀態(tài)時,Toolbar變?yōu)槿该鳡顟B(tài),當位于底部時,用于展示游戲簡介的View被固定在底部,其它的內容將被移出界面之外。</br>
位置狀態(tài)
為了便于理解,首先像定義幾個字段。
1. mImgShotView ==> 由于展示游戲截圖的View。
2. mContentView ==> 用于展示游戲信息的View。
3. mGInfoView ==> 用于展示游戲簡介信息的View。
3. mHeadView ==> 包含mGInfoView和TabLayout的View。
4. mHeadH ==> mHeadView的高度。
5. mBarH ==> Toolbar 和 TabLayout的高度。
6. mScreenH ==> 當前可視屏幕高度。
7. mStateBarH ==> stateBar高度。
8. mNBarH ==> NavigationBar高度。
9. mTopL ==> 位于頂部狀態(tài)時,mContentView 的 Y軸坐標基準位置。
10. mCenterL ==> 位于中間狀態(tài)時,mContentView 的 Y軸坐標基準位置。
11. mBottomL ==> 位于底部狀態(tài)時,mContentView 的 Y軸坐標基準位置。
12. mRawY ==> mContentView相對于當前可視界面的 Y 軸坐標。
頂部狀態(tài)分析
當處于頂部狀態(tài)時,mGInfoView將被移出界面之外;在第一篇文章我們編寫的布局里面,mContentView位于ToolBar下方,因此對于mContentView而言,它的基準坐標(y = 0)在Toolbar正下方;</br>
為了將mGInfoView移除界面之外,mContentView需要將Y坐標移動到-mHeadH + mBarH的位置。</br>
因此mTolL = -mHeadH + mBarH
中間狀態(tài)分析
對于中間狀態(tài),便簡單多了,中間狀態(tài)時,mContentView只需要將Y坐標往下移動到任一位置即可,此時,Toolbar處于完全透明狀態(tài)。</br>
這里,我是將它往下距離它基準位置的150dp 的位置。</br>
因此mCenterL = Util.dp2px(150)
底部狀態(tài)分析
當mContentView處于底部狀態(tài),mGInfoView將被固定在屏幕底部,其它的內容將被除出界面之外。</br>
通過分析,很容易知道:mBottomL = mScreenH - mStateBarH - mNBarH - mHeadH + mBarH
代碼實現(xiàn)
現(xiàn)在,3種狀態(tài)算是分析完成了,接下來便是代碼的編寫。</br>
在android 里面,對控件的移動操作,首先想到的是使用手勢。同時,在手勢移動的過程中,還需要對ToolBar進行透明度處理。</br>
mContentView的手勢移動代碼如下所示:
class SimpleGestureAction extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (mRawY <= mTopL && distanceY > 0) {
mRawY = mTopL;
return true;
}
if (mRawY >= mBottomL && distanceY < 0) {
mRawY = mBottomL;
return true;
}
mRawY -= distanceY;
if (mRawY < mCenterL) {
a += distanceY < 0 ? -0.03 : 0.03;
if (a < 0.0f) {
a = 0.0f;
} else if (a > 1.0f) {
a = 1.0f;
}
} else {
a = 0.0f;
}
if (mRawY <= mTopL) {
mRawY = mTopL;
a = 1.0f;
mBarBg.setAlpha(a);
mTemp.setAlpha(a);
}
mContent.setTranslationY(mRawY);
if (mRawY >= mCenterL + mBarH) {
rotationBanner(true);
}
return true;
}
}
以上是手勢移動的全部代碼,都是基本的控件移動操作。
mContentView 回歸操作
在上面的段落中,已經實現(xiàn)了對mContentView的移動操作?,F(xiàn)在,我們可以隨意對布局進行移動了;現(xiàn)在,如果對布局進行移動會發(fā)現(xiàn),在對mContentView移動的過程中,如果放開手指,它并沒有自動回彈到3個基準位置!這樣的操作很不符合用戶體驗,并且也沒有達到三個狀態(tài)的要求。</br>
因此,我們需要定義幾個閥值,當手指離開屏幕的時候,mContentView可以根據(jù)這幾個閥值來判斷它應該回歸到具體哪個基準位置。</br>
閥值的定義如下:
1. 回歸mTolL基準位置 ==> mRawY <= -mStateBarH
2. 回歸mCenterL基準位置 ==> -mStateBarH < mRawY && mRawY <= mCenterL + (mBarH << 1)
3. 回歸mBottomL基準位置 ==> mCenterL + (mBarH << 1) <= mRawY
具體的實現(xiàn)代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
if (mRawY <= -mStateBarH) {
toTop();
} else if ((-mStateBarH < mRawY && mRawY <= mCenterL + (mBarH << 1))) {
toCenter();
} else if (mCenterL + (mBarH << 1) <= mRawY) {
toBottom();
}
return true;
default:
if (0 <= a && a <= 1.0f) {
mBarBg.setAlpha(a);
mTemp.setAlpha(a);
}
mDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}
}
/** 回到頂部 */
private void toTop() {
AnimatorSet set = new AnimatorSet();
ObjectAnimator animator = ObjectAnimator.ofFloat(mContent, "translationY", mRawY, mTopL);
ObjectAnimator alpha = ObjectAnimator.ofFloat(mBarBg, "alpha", a, 1.0f);
ObjectAnimator alpha1 = ObjectAnimator.ofFloat(mTemp, "alpha", a, 1.0f);
set.setDuration(500);
set.play(animator).with(alpha).with(alpha1);
set.start();
mRawY = mTopL;
a = 1.0f;
mBarBg.setAlpha(a);
mTemp.setAlpha(a);
}
/** 回到中間 */
private void toCenter() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mContent, "translationY", mRawY, mCenterL);
animator.setDuration(500);
animator.start();
mRawY = mCenterL;
a = 0.0f;
mBarBg.setAlpha(a);
mTemp.setAlpha(a);
mCurrentState = STATE_CENTER;
rotationBanner(false);
}
/** 回到底部 */
private void toBottom() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mContent, "translationY", mRawY, mBottomL);
animator.setDuration(500);
animator.start();
mRawY = mBottomL;
a = 0.0f;
mBarBg.setAlpha(a);
mTemp.setAlpha(a);
mCurrentState = STATE_BOTTOM;
rotationBanner(true);
}
現(xiàn)在我們實現(xiàn)了布局的移動,同時也實現(xiàn)了mContentView的回歸操作。這是我們現(xiàn)在的效果:

游戲截圖旋轉實現(xiàn)
現(xiàn)在再看上面的效果,在對mContentView移動時,總感覺缺少點什么,再次回到當樂的游戲詳情效果圖,會看到,在移動的過程中,mImgShotView也會進行相應的操作,當mContentView從中間狀態(tài)移動到底部狀態(tài)時,mImgShotView會執(zhí)行一個動畫旋轉操作。再看我們的效果,由于沒有那個動畫旋轉效果,瞬間感覺low爆了。為了讓效果更佳高大上,讓我們來實現(xiàn)mImgShotView的旋轉動畫吧??!
mImgShotView旋轉實現(xiàn)
在當樂的效果中,mImgShotView的旋轉看起來是ViewPager的旋轉,實則是對ViewPager中Fragment的ImageView進行旋轉,在旋轉的過程中,ImageView在旋轉90°同時會填充整個屏幕。</br>
在這里吐槽一下,看起來這種效果不難實現(xiàn),但是等真正開發(fā)時會出現(xiàn)各種各樣的坑,說多了都是淚,誰做誰知道 :( ?。?!翻遍了這個stackoverflow都沒有好的解決方案,最終研究出來使用屬性動畫是最簡單實現(xiàn)并且性能是最好的??!</br>
為了便于理解,將定義一個字段 mBannerImg ==> 實則是對ViewPager中Fragment中真正用于展示游戲截圖的ImageView.
為了實現(xiàn)這種旋轉放大的效果,在進行屬性動畫編寫時,需要同時執(zhí)行以下三個步驟:
1. 將mBannerImg 進行90°旋轉。
2. 將mBannerImg 移動到屏幕中間。
3. 將mBannerImg 放大并填充整個屏幕。
具體的代碼如下:
/** 旋轉 */
private void rotation(ImageView img, boolean useAnim) {
int w = Util.getWindowWidth(getActivity()), h = Util.getWindowHeight(getActivity());
int iw = img.getMeasuredWidth(), ih = img.getMeasuredHeight();
if (useAnim) {
ObjectAnimator move = ObjectAnimator.ofFloat(img, "translationY", 0, (h - ih) / 2f);
move.setDuration(400);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(img, "scaleX", 1.0f, (float) h / iw);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(img, "scaleY", 1.0f, (float) w / ih);
ObjectAnimator rotation = ObjectAnimator.ofFloat(img, "rotation", 0f, 90f);
AnimatorSet set = new AnimatorSet();
set.play(scaleX).with(scaleY).with(rotation).with(move);
set.setDuration(600);
set.start();
} else {
img.setTranslationY((h - ih) / 2f);
img.setScaleX((float) h / iw);
img.setScaleY((float) w / ih);
img.setRotation(90f);
}
}
/** 恢復 */
private void resumeRotation(ImageView img, boolean useAnim) {
int w = Util.getWindowWidth(getActivity()), h = Util.getWindowHeight(getActivity());
int iw = img.getMeasuredWidth(), ih = img.getMeasuredHeight();
if (useAnim) {
ObjectAnimator move = ObjectAnimator.ofFloat(img, "translationY", (h - ih) / 2f, 0);
move.setDuration(400);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(img, "scaleX", (float) h / iw, 1.0f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(img, "scaleY", (float) w / ih, 1.0f);
ObjectAnimator rotation = ObjectAnimator.ofFloat(img, "rotation", 90f, 0f);
AnimatorSet set = new AnimatorSet();
set.play(scaleX).with(scaleY).with(rotation).with(move);
set.setDuration(600);
set.start();
} else {
img.setTranslationY(0f);
img.setScaleX(1.0f);
img.setScaleY(1.0f);
img.setRotation(0f);
}
}
以上便是旋轉的核心代碼。需要注意的是,mBannerImg在進行旋轉并填充到整個界面的過程中,需要改變自己的高度參數(shù),而在運行中改變View如果需要改變自己的參數(shù),需要在View.post(new runable(){....})的線程里面執(zhí)行;也就意味著,如果要讓上面兩個旋轉方法生效,就需要將它們放在post線程里面,因此需要使用到Handler來執(zhí)行UI的更新操作;我在這里是采用HandlerThread來實現(xiàn)這異步更新UI的操作。完整的旋轉代碼實現(xiàn)可以參考我的Demo例子。
mImgShotView旋轉操作
在上面的文章中,我們已經實現(xiàn)了mBannerImg的旋轉,這個時候運行代碼,移動mContentView時,將出現(xiàn)一個很有趣的現(xiàn)在mBannerImg旋轉了,但只顯示了一截,另一節(jié)被“吃掉了”。</br>
出現(xiàn)這個問題的原因是:在對圖片進行旋轉的過程中,屬性動畫已經改變了mBannerImg的高度參數(shù)。比如在mContentView處于底部狀態(tài)時,mBannerImg的高度已經變?yōu)槠聊坏母叨?,但是作為Fragment容器的mImgShotView的高度還是沒有被改變;這就導致剛才所說的那個問題。</br>
知道了原因,解決就簡單了,對mBannerImg進行操作前,只需要將mImgShotView的參數(shù)修改為與mBannerImg的參數(shù)一致便可,代碼如下所示:
/**
* 初始化游戲截圖ViewPager
*/
private void setupGameShotVp(final ViewPager viewPager) {
SimpleViewPagerAdapter adapter = new SimpleViewPagerAdapter(getSupportFragmentManager());
List<BannerEntity> data = getBannerData();
for (BannerEntity entity : data) {
adapter.addFrag(ScreenshotFragment.newInstance(entity), "");
}
viewPager.setAdapter(adapter);
viewPager.setOffscreenPageLimit(data.size());
mIndicator.setViewPager(viewPager);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
mShotVpPosition = position;
}
@Override
public void onPageSelected(int position) {}
@Override
public void onPageScrollStateChanged(int state) {}
});
//設置Banner圖片高度
new Handler().post(new Runnable() {
@Override
public void run() {
viewPager.post(new Runnable() {
@Override
public void run() {
SimpleViewPagerAdapter adapter = (SimpleViewPagerAdapter) mImgVP.getAdapter();
int h = (int) getResources().getDimension(R.dimen.game_detail_head_img_vp_height);
for (int i = 0, count = adapter.getCount(); i < count; i++) {
ScreenshotFragment fragment = (ScreenshotFragment) adapter.getItem(i);
if (fragment != null) {
fragment.setBannerHeight(h);
}
}
}
});
}
});
}
最終的效果

</br>
現(xiàn)在布局的移動和截圖旋轉算是完成了,接下來便是需要解決最困難的事件分發(fā)??!