Android 仿当乐游戏详情页面(二)


在上一篇文章里面,基本上算是实现了该效果的布局,有了布局,接下来就要对布局进行移动处理。</br>
android 仿当乐游戏详情页面(一)

对于移动的分析

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

  1. 处于顶部的状态</br>


    顶部状态
  2. 中间状态</br>


    中间状态
  3. 底部状态:</br>


    底部状态

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

位置状态

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

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正下方;</br>
为了将mGInfoView移除界面之外,mContentView需要将Y坐标移动到-mHeadH + mBarH的位置。</br>
因此mTolL = -mHeadH + mBarH

中间状态分析

对于中间状态,便简单多了,中间状态时,mContentView只需要将Y坐标往下移动到任一位置即可,此时,Toolbar处于完全透明状态。</br>
这里,我是将它往下距离它基准位置的150dp 的位置。</br>
因此mCenterL = Util.dp2px(150)

底部状态分析

当mContentView处于底部状态,mGInfoView将被固定在屏幕底部,其它的内容将被除出界面之外。</br>
通过分析,很容易知道:mBottomL = mScreenH - mStateBarH - mNBarH - mHeadH + mBarH

代码实现

现在,3种状态算是分析完成了,接下来便是代码的编写。</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 回归操作

在上面的段落中,已经实现了对mContentView的移动操作。现在,我们可以随意对布局进行移动了;现在,如果对布局进行移动会发现,在对mContentView移动的过程中,如果放开手指,它并没有自动回弹到3个基准位置!这样的操作很不符合用户体验,并且也没有达到三个状态的要求。</br>
因此,我们需要定义几个阀值,当手指离开屏幕的时候,mContentView可以根据这几个阀值来判断它应该回归到具体哪个基准位置。</br>
阀值的定义如下:

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

具体的实现代码如下:

@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°同时会填充整个屏幕。</br>
在这里吐槽一下,看起来这种效果不难实现,但是等真正开发时会出现各种各样的坑,说多了都是泪,谁做谁知道 :( !!!翻遍了这个stackoverflow都没有好的解决方案,最终研究出来使用属性动画是最简单实现并且性能是最好的!!</br>

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

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

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在进行旋转并填充到整个界面的过程中,需要改变自己的高度参数,而在运行中改变View如果需要改变自己的参数,需要在View.post(new runable(){....})的线程里面执行;也就意味着,如果要让上面两个旋转方法生效,就需要将它们放在post线程里面,因此需要使用到Handler来执行UI的更新操作;我在这里是采用HandlerThread来实现这异步更新UI的操作。完整的旋转代码实现可以参考我的Demo例子。

mImgShotView旋转操作

在上面的文章中,我们已经实现了mBannerImg的旋转,这个时候运行代码,移动mContentView时,将出现一个很有趣的现在mBannerImg旋转了,但只显示了一截,另一节被“吃掉了”。</br>
出现这个问题的原因是:在对图片进行旋转的过程中,属性动画已经改变了mBannerImg的高度参数。比如在mContentView处于底部状态时,mBannerImg的高度已经变为屏幕的高度,但是作为Fragment容器的mImgShotView的高度还是没有被改变;这就导致刚才所说的那个问题。</br>
知道了原因,解决就简单了,对mBannerImg进行操作前,只需要将mImgShotView的参数修改为与mBannerImg的参数一致便可,代码如下所示:

/**
 * 初始化游戏截图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>
现在布局的移动和截图旋转算是完成了,接下来便是需要解决最困难的事件分发!!

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容