美文网首页Android开发那些事Android开发者俱乐部开源代码
从零开始打造一个Android 3D立体旋转容器

从零开始打造一个Android 3D立体旋转容器

作者: ImmortalZ | 来源:发表于2016-07-15 20:09 被阅读1148次

    本文地址,转载请注明
    代码下载地址 :https://github.com/ImmortalZ/StereoView

    嗯,2个月没有写博客,是要好好反省下,趁着放暑假把这两个月看的东西好好沉淀下。嗯,就立下这个Flag,希望不要自己再打自己脸。

    1.概述

    回到正题,这次带来的效果,是一个Android 的3D立体旋转的效果。
    当然灵感的来源,来自早些时间微博上看到的效果图。
    非常酷有木有!作为程序猿我当然要把它加入我的下一个项目中啦!
    原效果

    这里写图片描述

    我们实现的效果:

    (为了更加可定制化,我在原图基础上新增了新的效果)

    这里写图片描述

    可以快速滚动,并且无限循环

    这里写图片描述

    这个是对一些参数的进行设定

    这里写图片描述

    对图片的包裹效果

    这里写图片描述

    因为本身继承自ViewGroup,所以基本控件都是可以包裹的

    2.分析

    因为代码量有点大,感觉把代码全部粘贴上来也不现实。所以想了解我的思路的盆友可以先来这里下载代码。然后边看代码边看我的分析

    下载地址 :https://github.com/ImmortalZ/StereoView

    通过我们实现的效果图可以发现:

    1.切换的时候是一个3D立体的效果

    2.布局中的每一个Item可以自由切换,且无限循环滚动

    要解决上面的效果,我们需要什么技术点呢?

    1.要想实现一个3D效果,我们可以借助Android中的Camera、Matrix

    2.要想实现滚动,毫无疑问,我们需要借助Scroller

    当然一切看起来很简单,其实不然,除此之外,你还需要对于滑动冲突进行处理等等,下面我开始介绍啦。

    这就是我们这次项目的大致

    这里写图片描述

    3.实现

    因为我们是要打造一个容器类,所以肯定得继承自 ViewGroup
    
    按照一般的思路,我们肯定是先要进行一些变量的申明,onMeasure,onLayout操作
    
    
    private void init(Context context) {
        mCamera = new Camera();
        mMatrix = new Matrix();
        if (mScroller == null) {
            mScroller = new Scroller(context);
        }
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        //滑动到设置的StartScreen位置
        scrollTo(0, mStartScreen * mHeight);
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childTop = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                child.layout(0, childTop,
                        child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
                childTop = childTop + child.getMeasuredHeight();
            }
        }
    }
    

    完成这些操作后,我们需要在onTouchEvent中进行滑动事件的处理

    3.1 完成无限循环滑动滚动

    我们的item数量是有限的,如何实现无限循环滚动呢?很简单,以3个item为例子(分别为1,2,3),我们让屏幕显示的是2

    如此反复,屏幕所在的位置始终是第2个item所在的位置,这样就实现了我们的无限循环滚动,向下滚动也是如此

    QQ截图20160715190642.png
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
            }
            mVelocityTracker.addMovement(event);
            float y = event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    if (!mScroller.isFinished()) {
                        //当上一次滑动没有结束时,再次点击,强制滑动在点击位置结束
                        mScroller.setFinalY(mScroller.getCurrY());
                        mScroller.abortAnimation();
                        scrollTo(0, getScrollY());
                    }
                    mDownY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    int realDelta = (int) (mDownY - y);
                    mDownY = y;
                    if (mScroller.isFinished()) {
                        //因为要循环滚动
                        recycleMove(realDelta);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float yVelocity = mVelocityTracker.getYVelocity();
                    //滑动的速度大于规定的速度,或者向上滑动时,上一页页面展现出的高度超过1/2。则设定状态为State.ToPre
                    if (yVelocity > standerSpeed || ((getScrollY() + mHeight / 2) / mHeight < mStartScreen)) {
                        mState = State.ToPre;
                    } else if (yVelocity < -standerSpeed || ((getScrollY() + mHeight / 2) / mHeight > mStartScreen)) {
                        //滑动的速度大于规定的速度,或者向下滑动时,下一页页面展现出的高度超过1/2。则设定状态为State.ToNext
                        mState = State.ToNext;
                    } else {
                        mState = State.Normal;
                    }
                    //根据mState进行相应的变化
                    changeByState(yVelocity);
                    if (mVelocityTracker != null) {
                        mVelocityTracker.recycle();
                        mVelocityTracker = null;
                    }
                    break;
            }
            //返回true,消耗点击事件
            return true;
        }
    

    当手从屏幕上移开时,我们来看下这个方法changeByState(yVelocity);

    这里写图片描述

    我们以mState = State.ToPre 为例子来说明

    /**
     * mState = State.ToPre 时进行的动作
     * @param yVelocity 竖直方向的速度
     */
    private void toPreAction(float yVelocity) {
        int startY;
        int delta;
        int duration;
        mState = State.ToPre;
        addPre();//增加新的页面
        //计算松手后滑动的item个数
        int flingSpeedCount= (yVelocity - standerSpeed) > 0 ? (int) (yVelocity - standerSpeed) : 0;
        addCount = flingSpeedCount/ flingSpeed + 1;
        //mScroller开始的坐标
        startY = getScrollY() + mHeight;
        setScrollY(startY);
        //mScroller 移动的距离
        delta = -(startY - mStartScreen * mHeight) - (addCount - 1) * mHeight;
        duration = (Math.abs(delta)) * 3;
        mScroller.startScroll(0, startY, 0, delta, duration);
        addCount--;
    }
    

    然后会进入addPre方法中

    /**
     * 把最后一个item移动到第一个item位置
     */
    private void addPre() {
        mCurScreen = ((mCurScreen - 1) + getChildCount()) % getChildCount();
        int childCount = getChildCount();
        View view = getChildAt(childCount - 1);
        removeViewAt(childCount - 1);
        addView(view, 0);
        if (iStereoListener != null) {
            iStereoListener.toPre(mCurScreen);
        }
    }
    

    最后mScroller.startScroll(0, startY, 0, delta, duration); 开始执行。
    执行的过程中会回调这个函数方法computeScroll

    这里写图片描述

    完成到这一步,我们的无限滑动滚动就算是完成了

    3.2 实现3D切换效果。

    正常情况下,我们自定义ViewGroup并不需要重写dispatchDraw 方法。
    而这里我们则需要重写

     @Override
        protected void dispatchDraw(Canvas canvas) {
            if (!isAdding && isCan3D) {
                //当开启3D效果并且当前状态不属于 computeScroll中 addPre() 或者addNext()
                //如果不做这个判断,addPre() 或者addNext()时页面会进行闪动一下
                //我当时写的时候就被这个坑了,后来通过log判断,原来是computeScroll中的onlayout,和子Child的draw触发的顺序导致的。
                //知道原理的朋友希望可以告知下
                for (int i = 0; i < getChildCount(); i++) {
                    drawScreen(canvas, i, getDrawingTime());
                }
            } else {
                isAdding = false;
                super.dispatchDraw(canvas);
            }
        }
    

    好,我们来drawScreen这个方法

    private void drawScreen(Canvas canvas, int i, long drawingTime) {
            int curScreenY = mHeight * i;
            //屏幕中不显示的部分不进行绘制
            if (getScrollY() + mHeight < curScreenY) {
                return;
            }
            if (curScreenY < getScrollY() - mHeight) {
                return;
            }
            float centerX = mWidth / 2;
            float centerY = (getScrollY() > curScreenY) ? curScreenY + mHeight : curScreenY;
            float degree = mAngle * (getScrollY() - curScreenY) / mHeight;
            if (degree > 90 || degree < -90) {
                return;
            }
            canvas.save();
    
            mCamera.save();
            mCamera.rotateX(degree);
            mCamera.getMatrix(mMatrix);
            mCamera.restore();
    
            mMatrix.preTranslate(-centerX, -centerY);
            mMatrix.postTranslate(centerX, centerY);
            canvas.concat(mMatrix);
            drawChild(canvas, getChildAt(i), drawingTime);
            canvas.restore();
    
        }
    

    这里面的关键就在于
    mCamera.rotateX(degree);
    mMatrix.preTranslate(-centerX, -centerY);
    mMatrix.postTranslate(centerX, centerY);

    对于Camera我们知道我们整个布局都是平铺的,为什么会产生3D的效果呢?原因就是这个Camera类,人如其名,它就相当于一个相机,它对物体进行拍照。我们把相机正对物体拍摄,拍摄出的效果就是平面的,当我们把相机旋转了90度再来拍摄原来物体,物体就相当于旋转了90度。
    Camera拍摄完毕后,然后把拍摄的参数值传到Matrix中,Matrix再和Canvas绑定,由Canvas进行绘制。最终显示在屏幕中。

    那么preTranslate,postTranslate又是怎么一回事呢?
    很简单,我们知道坐标系是以(0,0)作为参照点的。现在我们对拍摄的对象进行的缩放变形操作是在物体的中心。我们需要把物体的中心先移动到(0,0)位置,最后再移动到物体原来中心位置即可。

    具体的大家可以参考下这篇文章
    http://blog.csdn.net/rav009/article/details/7763223 ( Android postTranslate和preTranslate的理解)

    不过对于Camera的坐标系我还有一点点疑问,我准备有机会写一篇关于Camera和Matrix文章。

    3.3 滑动事件冲突的处理(先看后面的更新说明)

    完成上面两个步骤,那么我们就算Over了吗?

    不!还有很重要的一点,就是事件冲突的处理。 举个例子:我们把手放到我们的容器上,系统怎么知道我们这个滑动事件是给容器还是要给容器的子类的呢?

    (给容器自己,则进行滑动的操作,给容器的子类,则容器的子类可以进行点击事件的判断处理)

    对于这种情况,我就很大度啦,全部交给容器子类处理!子类不要,OK,那容器你自己拿来玩吧。

    ————之所以不走寻常路:交给容器处理,容器不需要再交给子类

    原因在于:容器拿到滑动事件只需要做滑动操作,而子类则不同,它有点击事件需要判断,一个容器有很多子类,而很多子类只有一个共同的容器,如果把控制权交给容器,那么容器怎么可能能够判断得出不同的子类到底需不需要这个滑动事件呢?所以,既然这么麻烦,那么统统交给子类处理。

    交给子类处理,则容器中onInterceptTouchEvent需要做如下操作

     @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                return false;
            }
            return true;
        }
    

    而子类(用CustomEdittext为例)的dispatchTouchEvent需要做如下判断

    @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (!isContain(event)) {
                        //子类不需要,交给容器自己处理
                        getParent().requestDisallowInterceptTouchEvent(false);
                        setFocusable(false);
                    } else {
                        //子类自己做操作
                        setFocusableInTouchMode(true);
                    }
                    break;
                case MotionEvent.ACTION_UP:
    
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    

    在isContain中,我做的是点击的坐标是否在Edittext中,在则拦截,子类处理,不在,则交给父类容器

     private boolean isContain(MotionEvent event) {
            region.set(rect);
            if (region.contains((int) event.getX(), (int) event.getY())) {
                return true;
            }
            return false;
        }
    

    当然交给子类这样也导致了一个问题,就是我如果需要给容器中的子类进行点击事件,则都需要自定义一个View(例如上面的CustomEdittext 继承自Edittext)。

    例如我就自定义了三个View,不过还是很简单的,几分钟的事就搞定了(在自定义View中dispatchTouchEvent进行判断)。

    具体的可以参考代码。

    这里写图片描述

    更新说明 2016/8/5

    滑动冲突之前我是把控制权交给了子类,这里https://github.com/Y-bao 这位作者提交的pull
    request中将事件冲突交给了父类(StereoView)
    ,我这边通过了pull,我觉得写得挺好的,把点击事件的控制权转移给父类,就不需要自定义View。
    如果你还想查看控制权转移给子类的代码(我之前的),可以点击这里

    3.4 点击水纹波效果

    细心的人会发现,我这里还有个RippleView。
    没错这就是点击后有水纹波的效果。
    Android本身可以在XML中用ripple实现,不过是Android 5.0以上,个人觉得兼容性不太好,就自己随便写了一个简易的,哈哈,效率不能保证,各位看客看看就好啦。

    4.应用

    4.1 定义的方法

    使用方法也和其他的没有什么区别,我这里自定义了几个方法,我这里说明下。

    自定义的方法

    setStartScreen(int startScreen) :设置第一页展示的页面 @param startScreen (0,getChildCount-1)

    setResistance(float resistance) : 设置滑动阻力 @param resistance (0,...)

    setInterpolator(Interpolator mInterpolator) : 设置滚动时interpolator插补器

    setAngle(float mAngle):设置滚动时两个item的夹角度数 [0f,180f]

    setCan3D(boolean can3D) : 是否开启3D效果

    setItem(int itemId) : 跳转到指定的item @param itemId [0,getChildCount-1]

    toPre() : 上一页

    toNext() : 下一页

    定义的回调接口

    这里写图片描述

    4.2 使用方法

    直接在布局中

    这里写图片描述

    在代码中

    这里写图片描述

    4.3 缺陷说明

    目前容器的item数量需要大于等于3,小于3个滑动时会些问题。设置的最开始展示的item位置不能是第一个或者最后一个,这么做是为了保证第1个或者最后一个被隐藏,从而保证最开始向上滑动或者向下滑动时的正常。

    5.下载

    如果觉得对你有帮助,欢迎 star,fork,如果对于我感兴趣,欢迎follow 我

    下载地址 :https://github.com/ImmortalZ/StereoView

    参考文章:

    http://blog.csdn.net/dawanganban/article/details/38421221

    http://blog.csdn.net/rav009/article/details/7763223

    相关文章

      网友评论

      • 监控都不:有很多行代码多余,特别是处理滑动那部分,加了跟没加是一样的,楼主看看修改一下
        监控都不:比如isSliding标志在onTouchEvent方法是必定是true
        监控都不:很多必然事件,楼主也添加了判断逻辑
      • 果子熊:哎哟,不错world哥, 之前是在IOS上面看到这样的效果,觉得很不错,自己尝试用翻转动画的组合来做过,效果没题主的好, 先研究下题主源码后再来评论后半段
      • BrokenDust:看看咋写的先
      • 路人葵:可以,很强势
      • 捡淑:666666
      • 8e7b8c86c7a5:正在研究中
        ImmortalZ:@上官小凯 有bug欢迎反馈
      • coco猫:你是成都的?
        ImmortalZ:@coco猫 不,只是在这边上学罢了
      • 一息尚存:很屌啊!我在想,这个应该是可以使用ViewAnimator+动画来实现的,这样的话偶合性就低了,通用性变强了,岂不更好?
        ImmortalZ:@一息尚存 之前写过一个直接用动画旋转的效果,但貌似并不能随着手指的滑动而相应的改变,整个改变的时间过程动画本身直接控制了,所以达不到我想要的效果,不知道你指的是哪一个?
      • nbpzjy:已follow&已star
        nbpzjy:@nbpzjy 多来好文章多来干货!!!!!😊
        ImmortalZ:@nbpzjy 😏多谢
      • DLLCNX:得好好看看
        ImmortalZ:@阿猫遇见阿狗 😁
      • f828342b6b81:基督教多久
        ImmortalZ:@Diaossss很忙__ 我不信基督教😲
      • 健康早餐:你怎么不上天?
        ImmortalZ:@帅气的昵称啊123 😂
      • zml_smile:感觉好屌…
        ImmortalZ:@zml_smile :innocent: 喜欢就好
        zml_smile:@zml_smile 😍
      • 52d0cec6868d:不想装大师,看着好有个性,可是不会弄。:joy:
        ImmortalZ:@弹钢琴丶梵高 参考下github上的代码,里面我有写几个测试用的demo的
        ImmortalZ:@弹钢琴丶梵高 哈哈,使用还是很方便的,就和我们平常使用Linearlayout这种ViewGroup类型没有什么区别,如果需要点击事件,还需要对里面包裹的View进行下事件分发处理(文中的3.3有说明) :innocent:
      • c68b2c8ebd49:addCount = flingSpeed / flingSpeed + 1;
        博主,我把你的项目运行,为什么这一行会报错
        Process: com.example.a3dapplication, PID: 7756
        java.lang.ArithmeticException: divide by zero
        c68b2c8ebd49:哪个变量名?博主能给个联系方式讨论下不
        ImmortalZ:@c68b2c8ebd49 我的锅,我上传到github上的代码一个变量名和一个临时变量相同了,结果就导致除数为0报错,我修改修正好了,奖励你一朵小红花 :hibiscus: :joy:
        ImmortalZ:@c68b2c8ebd49 纳尼😱我回去用电脑看看
      • sososeen09:效果不错,需要好好研究
        ImmortalZ: @三川草民

        多谢😏

      本文标题:从零开始打造一个Android 3D立体旋转容器

      本文链接:https://www.haomeiwen.com/subject/pqgyjttx.html