在上一篇文章里面,基本上算是实现了该效果的布局,有了布局,接下来就要对布局进行移动处理。

android 仿当乐游戏详情页面(一)
android 仿当乐游戏详情页面(二)
android 仿当乐游戏详情页面(三)

对于移动的分析

通过第一篇文章的分析,在所有控件里面,能移动的只有用于展示游戏简介和游戏相关数据的View,并且该View的移动有以下三种状态:

  1. 处于顶部的状态

    顶部状态

  2. 中间状态

    中间状态

  3. 底部状态:

    底部状态

如上面几张图片所示,处于顶部状态,TabLayout 悬停在Toolbar的下面,而此时,用于介绍游戏简介的View被移出布局;处于中间状态时,Toolbar变为全透明状态,当位于底部时,用于展示游戏简介的View被固定在底部,其它的内容将被移出界面之外。

位置状态

为了便于理解,首先像定义几个字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 ==> 位于顶部状态时,mContentView 的 Y轴坐标基准位置。
10. mCenterL ==> 位于中间状态时,mContentView 的 Y轴坐标基准位置。
11. mBottomL ==> 位于底部状态时,mContentView 的 Y轴坐标基准位置。
12. mRawY ==> mContentView相对于当前可视界面的 Y 轴坐标。

顶部状态分析

当处于顶部状态时,mGInfoView将被移出界面之外;在第一篇文章我们编写的布局里面,mContentView位于ToolBar下方,因此对于mContentView而言,它的基准坐标(y = 0)在Toolbar正下方;

为了将mGInfoView移除界面之外,mContentView需要将Y坐标移动到-mHeadH + mBarH的位置。

因此mTolL = -mHeadH + mBarH

中间状态分析

对于中间状态,便简单多了,中间状态时,mContentView只需要将Y坐标往下移动到任一位置即可,此时,Toolbar处于完全透明状态。

这里,我是将它往下距离它基准位置的150dp 的位置。

因此mCenterL = Util.dp2px(150)

底部状态分析

当mContentView处于底部状态,mGInfoView将被固定在屏幕底部,其它的内容将被除出界面之外。

通过分析,很容易知道:mBottomL = mScreenH - mStateBarH - mNBarH - mHeadH + mBarH

代码实现

现在,3种状态算是分析完成了,接下来便是代码的编写。

在android 里面,对控件的移动操作,首先想到的是使用手势。同时,在手势移动的过程中,还需要对ToolBar进行透明度处理。

mContentView的手势移动代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 回归操作

在上面的段落中,已经实现了对mContentView的移动操作。现在,我们可以随意对布局进行移动了;现在,如果对布局进行移动会发现,在对mContentView移动的过程中,如果放开手指,它并没有自动回弹到3个基准位置!这样的操作很不符合用户体验,并且也没有达到三个状态的要求。

因此,我们需要定义几个阀值,当手指离开屏幕的时候,mContentView可以根据这几个阀值来判断它应该回归到具体哪个基准位置。

阀值的定义如下:

1
2
3
1. 回归mTolL基准位置    ==> mRawY <= -mStateBarH
2. 回归mCenterL基准位置 ==> -mStateBarH < mRawY && mRawY <= mCenterL + (mBarH << 1)
3. 回归mBottomL基准位置 ==> mCenterL + (mBarH << 1) <= mRawY

具体的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@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);
}

现在我们实现了布局的移动,同时也实现了mContentView的回归操作。这是我们现在的效果:
现在的效果

游戏截图旋转实现

现在再看上面的效果,在对mContentView移动时,总感觉缺少点什么,再次回到当乐的游戏详情效果图,会看到,在移动的过程中,mImgShotView也会进行相应的操作,当mContentView从中间状态移动到底部状态时,mImgShotView会执行一个动画旋转操作。再看我们的效果,由于没有那个动画旋转效果,瞬间感觉low爆了。为了让效果更佳高大上,让我们来实现mImgShotView的旋转动画吧!!

mImgShotView旋转实现

在当乐的效果中,mImgShotView的旋转看起来是ViewPager的旋转,实则是对ViewPager中Fragment的ImageView进行旋转,在旋转的过程中,ImageView在旋转90°同时会填充整个屏幕。

在这里吐槽一下,看起来这种效果不难实现,但是等真正开发时会出现各种各样的坑,说多了都是泪,谁做谁知道 :( !!!翻遍了这个stackoverflow都没有好的解决方案,最终研究出来使用属性动画是最简单实现并且性能是最好的!!

为了便于理解,将定义一个字段 mBannerImg ==> 实则是对ViewPager中Fragment中真正用于展示游戏截图的ImageView.

为了实现这种旋转放大的效果,在进行属性动画编写时,需要同时执行以下三个步骤:

1
2
3
1. 将mBannerImg 进行90°旋转。
2. 将mBannerImg 移动到屏幕中间。
3. 将mBannerImg 放大并填充整个屏幕。

具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/** 旋转 */
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在进行旋转并填充到整个界面的过程中,需要改变自己的高度参数,而在运行中改变View如果需要改变自己的参数,需要在View.post(new runable(){....})的线程里面执行;也就意味着,如果要让上面两个旋转方法生效,就需要将它们放在post线程里面,因此需要使用到Handler来执行UI的更新操作;我在这里是采用HandlerThread来实现这异步更新UI的操作。完整的旋转代码实现可以参考我的Demo例子。

mImgShotView旋转操作

在上面的文章中,我们已经实现了mBannerImg的旋转,这个时候运行代码,移动mContentView时,将出现一个很有趣的现在mBannerImg旋转了,但只显示了一截,另一节被“吃掉了”。

出现这个问题的原因是:在对图片进行旋转的过程中,属性动画已经改变了mBannerImg的高度参数。比如在mContentView处于底部状态时,mBannerImg的高度已经变为屏幕的高度,但是作为Fragment容器的mImgShotView的高度还是没有被改变;这就导致刚才所说的那个问题。

知道了原因,解决就简单了,对mBannerImg进行操作前,只需要将mImgShotView的参数修改为与mBannerImg的参数一致便可,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 初始化游戏截图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);
}
}
}
});
}
});
}

最终的效果

最终效果



现在布局的移动和截图旋转算是完成了,接下来便是需要解决最困难的事件分发!!

Android 仿当乐游戏详情页(三)