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

本文地址,转载请注明 http://blog.csdn.net/mr_immortalz/article/details/51918560

嗯,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所在的位置,这样就实现了我们的无限循环滚动,向下滚动也是如此

    @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 flingSpeed = (yVelocity - standerSpeed) > 0 ? (int) (yVelocity - standerSpeed) : 0;
    addCount = flingSpeed / 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进行判断)。

具体的可以参考代码。

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

时间: 2024-08-15 00:26:49

从零开始打造一个Android 3D立体旋转容器的相关文章

浅谈如何打造一个安全稳定高效的容器云平台

本文介绍了容器的现状和发展趋势,容器集群编排引擎选型,跨主机网络通信,定制化方案,公有云,私有云及混合云的场景及实现等内容,说明如何打造简单而强大的容器云平台. 1. 容器技术现状及发展趋势 什么是容器? 我们可以将容器理解为一种沙盒,每个容器具有独立的操作系统资源,不同的容器之间相互隔离,也可以建立通信,应用跑在各自的容器中,避免了环境中有冲突的资源使用,做到一次封装,到处运行. 那容器与虚拟机的区别在哪? 容器可以看做轻量的虚拟机,虚机启动可能需要数分钟或者更长,而容器只需几十毫秒.传统虚拟

3D立体旋转

<!DOCTYPE html><html><head>    <title></title>    <meta charset="utf-8">    <style type="text/css">        body{            perspective: 1000px;        }        .one{            position: relat

css3立体旋转菜单

css3立体旋转菜单,css3,3D,立体旋转,立体菜单,菜单导航,css3立体旋转菜单是一款纯css3实现的三维立体旋转导航菜单. 源码下载页:http://www.huiyi8.com/sc/7127.html css3立体旋转菜单,布布扣,bubuko.com

Android 3D滑动菜单完全解析,实现推拉门式的立体特效

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/10471245 在上一篇文章中,我们学习了Camera的基本用法,并借助它们编写了一个例子,实现了类似于API Demos里的图片中轴旋转功能.不过那个例子的核心代码是来自于API Demos中带有的Rotate3dAnimation这个类,是它帮助我们完成了所有的三维旋转操作,所有Matrix和Camera相关的代码也是封装在这个类中. 这样说来的话,大家心里会不会痒痒的呢?虽然

Android立体旋转动画实现与封装(支持以X、Y、Z三个轴为轴心旋转)

本文主要介绍Android立体旋转动画,或者3D旋转,下图是我自己实现的一个界面 立体旋转分为以下三种: 1. 以X轴为轴心旋转 2. 以Y轴为轴心旋转 3. 以Z轴为轴心旋转--这种等价于android默认自带的旋转动画RotateAnimation 实现立体旋转核心步骤: 1. 继承系统Animation重写applyTransformation方法 通过applyTransformation方法的回调参数 float interpolatedTime, Transformation t 来

Android高级控件(五)——如何打造一个企业级应用对话列表,以QQ,微信为例

Android高级控件(五)--如何打造一个企业级应用对话列表,以QQ,微信为例 看标题这么高大上,实际上,还是运用我么拿到listview去扩展,我们讲什么呢,就是研究一下QQ,微信的这种对话列表,我们先看一个传统的ListView是怎么样的,我们做一个通讯录吧,通讯录的组成就是一个头像,一个名字,一个电话号码,一个点击拨打的按钮,既然这样,那我们的item就出来了 call_list_item.xml <?xml version="1.0" encoding="ut

Android 从无到有打造一个炫酷的进度条效果

从无到有打造一个炫酷的进度条效果

打造一个全命令行的Android构建系统

IDE都是给小白程序猿的,大牛级别的程序猿一定是命令行控,终端控,你看大牛都是使用vim,emacs 就一切搞定" 这话说的尽管有些绝对.可是也不无道理.做开发这行要想效率高,自己主动化还真是缺少不了命令行工具,由于仅仅有命令行才是最佳的人机交互工具. 事实上IDE也是底层也是调用命令行工具而已,仅仅只是给普通开发人员呈现一个更友好的开发界面. 这里可不是宣扬让大家放弃IDE都改命令行,仅仅是每种事物都有他存在的理由,不管是编程语言还是工具都是一个原则 "没有最好的,仅仅有最合适的&q

用.Net打造一个移动客户端(Android/IOS)的服务端框架NHM——Android端消息处理机制

NhmFramework Android端的消息处理机制原理 1.概要表述:在我们的框架中,Android客户端通过继承Application来控制整个应用程序的生命周期,在Application onCreate()方法中,我们将启动一个MainService,这个Service将负责Activity的异步消息处理(包括异步Http请求).任务调度.数据共享等大部分持久化操作.那么这样做的目的何在呢? 1)异步消息处理:在Service中实现异步消息处理是为了将Activity的界面显示的操作