天天看點

Android 3D立體旋轉效果實作

說明:之前在網上到處搜尋類似的旋轉效果 但搜到的結果都不是十分滿意 原因不多追述 

     ( 由于之前隻是交流學習使用 現在已經改進為能用狀态   )

(最新去github下載下傳   修複滑動時偶有卡頓的問題 , 修正設定某些視圖旋轉到超過90° 殘影問題  頁面切換 修正為滑動到 角度設定的一半時  ,添加了自動歸位為正面顯示效果)

(如果有人找到過相關 比較好的效果 可以發一下連接配接 一起共同進步)

一 效果展示 :如非您所需要的效果 也希望能給些微幫助 (已經添加了上下滾動内容 )

Android 3D立體旋轉效果實作
Android 3D立體旋轉效果實作

具體操作以及實作 效果 請看項目例子

二 使用方式

Android 3D立體旋轉效果實作

此控件繼承FrameLayout 

//aar引用 現更新為如下 建議不要使用(Bintray 要停了 有空換個倉庫)

implementation 'com.icefordog:3dviewGroup:2.0.0'

1.所有包含子控件 現已經自動 重新布局到相應位置以及調整了大小

2.添加了上下滾動 以及代碼主動翻頁 和翻頁回調

3.添加了自定義屬性調整部分功能機體如下

# 使用方式如下

   <com.burning.foethedog.Rota3DSwithView
        android:id="@+id/_test_Rota3DVSwithView"
                android:layout_width="400dp"
                android:layout_height="300dp"
                app:autoscroll="true"
                app:heightRatio="0.8"

                app:rotateV="true"
                app:rotation="40"
                app:widthRatio="0.6">

           #是否自動滾動      app:autoscroll="true"
           #子控件對于改viegroup 中的寬高比例 預設無為0.7  
                       app:heightRatio  app:widthRatio
           #垂直或者水準滑動    app:rotateV="true"
           #偏轉角度      app:rotation="40"
           setR3DPagechange   監聽滑動頁面正面頁的變化
                #此處list偷懶借用了RecyclerView.Adapter 暫時僅可設定單一模闆
    <com.burning.foethedog.Rota3DSwithViewList
        android:id="@+id/rota3DSwithViewList_test"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="@color/colorAccent"
        app:autoscroll="true" />


           
Android 3D立體旋轉效果實作

子空間直接添加如同framelayout 相同 如要如圖效果 唯一要求子空間必須位于父控件中心且寬高等大小 為了友善擴充而做 如有其他需求可自行更改 (注 所有子控件 最好添加上背景 由于繪制機制和動畫原因 沒有背景會有部分重貼) 内部子view 可為 任意ViewGroup 。 弱使用過程中遇見任何BUG 歡迎提出。

三 實作原理

      實作原理由Camera 與Maxtrix 組合修改View的繪制而得 具體Camera 與Maxtrix 的變換 過程請自行搜尋。 在此不班門弄斧。具體修改有

@Override
protected void dispatchDraw(Canvas canvas) {
    int indexleft = getWidth() / 2;//中間顯示視圖 ----左邊的位置
    int postTranslateX = rotationX * childWith / 2 / rotation;//設-----定邊移動 距離
    //定點  又稱頂點
    //  chilDrawforCamera(canvas, postTranslateX, indexleft, 3);//預繪制 的 縣繪制  防止遮擋
    for (int i = 0; i < 4; i++)
        chilDrawforCamera(canvas, postTranslateX, indexleft, i);
    if (!isTouch)
        handler.sendEmptyMessageDelayed(1, 100);
}           

重新編寫 dispathDraw() 進而達到 不必要去修改子view的内容 而添加擴充性 

具體變換包括 

private void chilDrawforCamera(Canvas canvas, int postTranslateX, int indexleft, int i) {
    canvas.save();
    mCamera.save();
    mMaxtrix.reset();
    mCamera.translate(postTranslateX, 0, 0);
    mCamera.rotateY(rotationX);
    mCamera.translate(postTranslateX, 0, 0);
    if (postTranslateX == 0) {
        if (isright)
            setCameraChange(childWith, rotation, i);
        else
            setCameraChange(-childWith, -rotation, i);
    } else if (postTranslateX > 0) {
        setCameraChange(childWith, rotation, i);
    } else if (postTranslateX < 0) {
        setCameraChange(-childWith, -rotation, i);
    }
    mCamera.getMatrix(mMaxtrix);
    mCamera.restore();
    mMaxtrix.preTranslate(-indexleft, -getHeight() / 2);//指定在 螢幕上 運作的棱 是哪一條
    mMaxtrix.postTranslate(indexleft, getHeight() / 2);//運作路徑
    canvas.concat(mMaxtrix);
    //繪制
    View childAt = getChildAt((swithView(i) + 2 * getChildCount()) % getChildCount());
    drawChild(canvas, childAt, 0);
    canvas.restore();
}           

指定需要繪制的子view 先後順序以及哪些子view 

private int swithView(int i) {
    int k = 0;

    switch (i) {
        case 0:
            if (isright)
                k = index - 2;
            else
                k = index + 2;
            break;
        case 1:
            if (isright)
                k = index + 1;
            else
                k = index - 1;
            break;
        case 2:
            if (isright)
                k = index - 1;
            else
                k = index + 1;
            break;
        case 3:
            k = index;
            break;
    }
    return k;
             

 具體的網上其他 類似效果到底有什麼不同于優勢在此不多做描述 實作方式上有哪些不同 嗯 下次有空再細說(由于需要大量圖文描述 3D的變換過程才講的清楚,有些間隔的時間也稍長 隻記得大概的思路過程)。

private void setCameraChange(int translate, int roat, int i) {
    switch (i) {
        case 0:
            //預繪制 的VIEW
            mCamera.translate(-translate / 2, 0, 0);
            mCamera.rotateY(-roat);
            mCamera.translate(-translate / 2, 0, 0);

            mCamera.translate(-translate / 2, 0, 0);
            mCamera.rotateY(-roat);
            mCamera.translate(-translate / 2, 0, 0);
            break;
        //目前位置兩側的View
        case 1:
            mCamera.translate(translate / 2, 0, 0);
            mCamera.rotateY(roat);
            mCamera.translate(translate / 2, 0, 0);
            break;

        case 2:
            mCamera.translate(-translate / 2, 0, 0);
            mCamera.rotateY(-roat);
            mCamera.translate(-translate / 2, 0, 0);
            break;
        //最後繪制 目前顯示位置 防止 被遮擋
        case 3:
            mCamera.rotateY(0);
            break;
    }


} 
           

其他剩下的就是 index 選中切換 以及滑動内容 和分發修改等。

整個類複制也可以(以下為重新修改後的完整代碼)

package com.burning.foethedog;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;


/**
 * Created by burning on 2017/5/2.
 * When I wrote this, only God and I understood what I was doing
 * Now, God only knows
 * -------------------------//┏┓   ┏┓
 * -------------------------//┏┛┻━━━┛┻┓
 * -------------------------//┃       ┃
 * -------------------------//┃   ━   ┃
 * -------------------------//┃ ┳┛ ┗┳ ┃
 * -------------------------//┃       ┃
 * -------------------------//┃   ┻   ┃
 * -------------------------//┃       ┃
 * -------------------------//┗━┓   ┏━┛
 * -------------------------//┃   ┃  神獸保佑
 * -------------------------//┃   ┃  代碼無BUG!
 * -------------------------//┃   ┗━━━┓
 * -------------------------//┃       ┣┓
 * -------------------------//┃       ┏┛
 * -------------------------//┗┓┓┏━┳┓┏┛
 * -------------------------// ┃┫┫ ┃┫┫
 * -------------------------// ┗┻┛ ┗┻┛
 */

public class Rota3DSwithView extends FrameLayout {
    Camera mCamera;
    Matrix mMaxtrix;

    public Rota3DSwithView(Context context) {
        super(context);
        initRoat3D();
    }

    private void initRoat3D() {
        mCamera = new Camera();
        mMaxtrix = new Matrix();
        setWillNotDraw(false);
    }

    private void initRoat3DStyle(AttributeSet attrs) {
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.rota3DVSwithView);
        rotateV = typedArray.getBoolean(R.styleable.rota3DVSwithView_rotateV, false);
        autoscroll = typedArray.getBoolean(R.styleable.rota3DVSwithView_autoscroll, true);
        setAutoscroll(autoscroll);
        rotation = typedArray.getInt(R.styleable.rota3DVSwithView_rotation, 40);
        heightRatio = typedArray.getFloat(R.styleable.rota3DVSwithView_heightRatio, 0.7f);
        widthRatio = typedArray.getFloat(R.styleable.rota3DVSwithView_widthRatio, 0.7f);

    }

    float widthRatio = 0.7f;
    float heightRatio = 0.7f;

    public Rota3DSwithView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initRoat3DStyle(attrs);
        initRoat3D();
    }

    public Rota3DSwithView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initRoat3DStyle(attrs);
        initRoat3D();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public Rota3DSwithView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initRoat3DStyle(attrs);
        initRoat3D();
    }

    //擷取子View的寬或者高--作為旋轉和移動依據
    int childHeight;
    int childWith;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int rx = (int) ((right - left) * (1 - widthRatio) / 2);
        int ry = (int) ((bottom - top) * (1 - heightRatio) / 2);
        childHeight = (int) ((bottom - top) * heightRatio);
        childWith = (int) ((right - left) * widthRatio);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            child.layout(rx, ry, right - left - rx, bottom - top - ry);
            child.setClickable(true);
            ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
            if (layoutParams.width != childWith) {
                layoutParams.width = childWith;
                layoutParams.height = childHeight;
                child.setLayoutParams(layoutParams);
            }
        }

    }

    //錄影機 為點光源  正真的直角  反而看起來 并不是直角
    int rotation = 40;// 設定外角
    int moveRotation = 00;
    int index = 0;

    private void disDrawrX(Canvas canvas) {
        int indexleft = getWidth() / 2;//中間顯示視圖 ----左邊的位置
        int postTranslateX = moveRotation * childWith / 2 / rotation;//設-----定邊移動 距離
        for (int i = 0; i < 4; i++)
            chilDrawforCameraX(canvas, postTranslateX, indexleft, i);
    }

    private void disDrawrY(Canvas canvas) {
        int indexleft = getHeight() / 2;//中間顯示視圖 ----左邊的位置
        int postTranslateX = moveRotation * childHeight / 2 / rotation;//設-----定邊移動 距離
        //定點  又稱頂點
        for (int i = 0; i < 4; i++) {
            chilDrawforCameraY(canvas, postTranslateX, indexleft, i);
        }
    }

    boolean rotateV = false;

    public boolean isRotateV() {
        return rotateV;
    }

    public void setRotateV(boolean rotateV) {
        this.rotateV = rotateV;
        this.invalidate();
    }

    public int getmoveRotation() {
        return moveRotation;
    }

    public void setmoveRotation(int moveRotation) {
        this.moveRotation = moveRotation;
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (getChildCount() == 0) {
            return;
        }
        if (rotateV) {
            disDrawrY(canvas);
        } else {
            disDrawrX(canvas);
        }
    }

    boolean autoscroll = true;

    public boolean isAutoscroll() {
        return autoscroll;
    }

    public void setAutoscroll(boolean autoscroll) {
        if (autoscroll) {
            senMessageStart();
        }
        this.autoscroll = autoscroll;
    }

    private void senMessageStart() {
        handler.sendEmptyMessageDelayed(2, 10);
    }

    private void setCameraChangeY(int translate, int roat, int i) {
        switch (i) {
            case 0:
                //預繪制 的VIEW
                mCamera.translate(0, -translate / 2, 0);
                mCamera.rotateX(-roat);
                mCamera.translate(0, -translate / 2, 0);

                mCamera.translate(0, -translate / 2, 0);
                mCamera.rotateX(-roat);
                mCamera.translate(0, -translate / 2, 0);
                break;
            //目前位置兩側的View
            case 1:
                mCamera.translate(0, translate / 2, 0);
                mCamera.rotateX(roat);
                mCamera.translate(0, translate / 2, 0);
                break;

            case 2:
                mCamera.translate(0, -translate / 2, 0);
                mCamera.rotateX(-roat);
                mCamera.translate(0, -translate / 2, 0);
                break;
            //最後繪制 目前顯示位置 防止 被遮擋
            case 3:
                mCamera.rotateX(0);
                break;
        }


    }

    private boolean isleftortop = false;

    public boolean isIsleftortop() {
        return isleftortop;
    }

    public void setIsleftortop(boolean isleftortop) {
        startAnimation(isleftortop);
    }

    Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 2: {
                    if (getChildCount() == 0) {
                        return;
                    }
                    if (isTouch) {
                        return;
                    }
                    if (isleftortop)
                        moveRotation++;
                    else
                        moveRotation--;
                    if (Math.abs(moveRotation) == rotation) {
                        moveRotation = 0;
                        int position = index % getChildCount();
                        reSetIndex(position);
                        if (isleftortop) {
                            position = index - 1;
                        } else {
                            position = index + 1;
                        }
                        reSetIndex(position);
                    }
                    showIndex = index;
                    rotaViewtangle(moveRotation);
                    if (isAutoscroll())
                        senMessageStart();
                }
            }
        }
    };


    private void chilDrawforCameraY(Canvas canvas, int postTranslateX, int indexleft, int i) {
        canvas.save();
        mCamera.save();
        mMaxtrix.reset();
        mCamera.translate(0, postTranslateX, 0);
        mCamera.rotateX(moveRotation);
        mCamera.translate(0, postTranslateX, 0);
        if (postTranslateX == 0) {
            if (isleftortop)
                setCameraChangeY(childHeight, rotation, i);
            else
                setCameraChangeY(-childHeight, -rotation, i);
        } else if (postTranslateX > 0) {
            setCameraChangeY(childHeight, rotation, i);
        } else if (postTranslateX < 0) {
            setCameraChangeY(-childHeight, -rotation, i);
        }
        mCamera.getMatrix(mMaxtrix);
        mCamera.restore();
        mMaxtrix.preTranslate(-getWidth() / 2, -indexleft);//指定在 螢幕上 運作的棱 是哪一條
        mMaxtrix.postTranslate(getWidth() / 2, indexleft);//運作路徑
        canvas.concat(mMaxtrix);
        //繪制
        View childAt = getChildAt(swithView(i)/*(swithView(i) + 2 * getChildCount()) % getChildCount()*/);
        if (childAt != null)
            drawChild(canvas, childAt, 0);
        canvas.restore();
    }

    private int swithView(int i) {
        int k = 0;

        switch (i) {
            case 0:
                if (isleftortop)
                    k = index - 2;
                else
                    k = index + 2;
                break;
            case 1:
                if (isleftortop)
                    k = index + 1;
                else
                    k = index - 1;
                break;
            case 2:
                if (isleftortop)
                    k = index - 1;
                else
                    k = index + 1;
                break;
            case 3:
                k = index;
                break;
        }
        int j = k % getChildCount();
        if (j >= 0) {
            return j;
        } else {
            return (j + getChildCount());
        }
//        return k;
    }

    boolean isTouch = false;
    int downXorY = 0;

    public boolean dispatchTouchEvent(MotionEvent event) {
        if (getChildCount() == 0) {
            return super.dispatchTouchEvent(event);
        }
        //這裡我們就 就隻分發給目前index子View
        if (!onInterceptTouchEvent(event)) {
            return getChildAt(swithView(3)).dispatchTouchEvent(event);
        }
        return super.dispatchTouchEvent(event);
    }

    int thisRx = 0;
    int showIndex;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (getChildCount() == 0) {
            return super.onInterceptTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (isRotateV()) {
                    downXorY = (int) event.getY();
                } else {
                    downXorY = (int) event.getX();
                }
                showIndex = index;
                thisRx = moveRotation;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isRotateV()) {
                    if (Math.abs(event.getY() - downXorY) > 50) {
                        return true /*onTouchEvent(event)*/;
                    }
                } else {
                    if (Math.abs(event.getX() - downXorY) > 50) {
                        return true /*onTouchEvent(event)*/;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                return isTouch;
        }
        return super.onInterceptTouchEvent(event);
    }

    boolean touching_auto = false;

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (getChildCount() == 0) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                isTouch = true;
                ontouchView(event);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                isTouch = false;
                senMessageStart();
                break;
        }
        return true;
    }

    private int ontouchView(MotionEvent event) {
        int movedistance = 0;
        int moveRxory = 0;
        if (isRotateV()) {
            movedistance = -((int) event.getY() - downXorY);
            moveRxory = thisRx + movedistance * rotation * 2 / (getHeight() + 100);
        } else {
            movedistance = (int) event.getX() - downXorY;
            moveRxory = thisRx + movedistance * rotation * 2 / (getWidth() + 100);
        }
        isleftortop = (moveRxory > 0) ? true : false;
        rotaViewtangle(moveRxory);
        return movedistance;
    }

    private int rotaViewtangle(int moveRxory) {

        int removeItem = moveRxory / rotation;
        int position = showIndex - removeItem;
        reSetIndex(position);
        moveRotation = moveRxory % rotation;
        this.invalidate();
        return moveRotation;
    }

    private void setCameraChangeX(int translate, int roat, int i) {
        switch (i) {
            case 0:
                //預繪制 的VIEW
                mCamera.translate(-translate / 2, 0, 0);
                mCamera.rotateY(-roat);
                mCamera.translate(-translate / 2, 0, 0);

                mCamera.translate(-translate / 2, 0, 0);
                mCamera.rotateY(-roat);
                mCamera.translate(-translate / 2, 0, 0);
                break;
            //目前位置兩側的View
            case 1:
                mCamera.translate(translate / 2, 0, 0);
                mCamera.rotateY(roat);
                mCamera.translate(translate / 2, 0, 0);
                break;

            case 2:
                mCamera.translate(-translate / 2, 0, 0);
                mCamera.rotateY(-roat);
                mCamera.translate(-translate / 2, 0, 0);
                break;
            //最後繪制 目前顯示位置 防止 被遮擋
            case 3:
                mCamera.rotateY(0);
                break;
        }


    }

    private void chilDrawforCameraX(Canvas canvas, int postTranslateX, int indexleft, int i) {
        canvas.save();
        mCamera.save();
        mMaxtrix.reset();
        mCamera.translate(postTranslateX, 0, 0);
        mCamera.rotateY(moveRotation);
        mCamera.translate(postTranslateX, 0, 0);
        if (postTranslateX == 0) {
            if (isleftortop)
                setCameraChangeX(childWith, rotation, i);
            else
                setCameraChangeX(-childWith, -rotation, i);
        } else if (postTranslateX > 0) {
            setCameraChangeX(childWith, rotation, i);
        } else if (postTranslateX < 0) {
            setCameraChangeX(-childWith, -rotation, i);
        }
        mCamera.getMatrix(mMaxtrix);
        mCamera.restore();
        mMaxtrix.preTranslate(-indexleft, -getHeight() / 2);//指定在 螢幕上 運作的棱 是哪一條
        mMaxtrix.postTranslate(indexleft, getHeight() / 2);//運作路徑
        canvas.concat(mMaxtrix);
        //繪制
        View childAt = getChildAt(swithView(i) /*( + 2 * getChildCount()) % getChildCount()*/);
        if (childAt != null)
            drawChild(canvas, childAt, 0);
        canvas.restore();
    }


    public void destory() {
        handler.removeCallbacksAndMessages(null);
        handler = null;
    }

    private void reSetIndex(int position) {
        index = position;
        showDataPage(position);
    }


    @SuppressLint("ObjectAnimatorBinding")
    public void returnPage() {
        startAnimation(true);
    }

    private boolean isAnimationStarting = false;

    @SuppressLint("ObjectAnimatorBinding")
    public void nextPage() {
        startAnimation(false);
    }

    @SuppressLint("ObjectAnimatorBinding")
    private void startAnimation(boolean rightortop) {
        if (isAnimationStarting) {
            return;
        }
        isAnimationStarting = true;
        isleftortop = rightortop;
        showIndex = index;
        Interpolator interpolator = new AccelerateInterpolator();
        ObjectAnimator mAnimator = ObjectAnimator.ofInt(this, "xxxxx", moveRotation, rightortop ? rotation : -rotation);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int animatedValue = (int) animation.getAnimatedValue();
                rotaViewtangle(animatedValue);
            }
        });
        mAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                isAnimationStarting = false;
            }
        });
        mAnimator.setInterpolator(interpolator);
        mAnimator.setDuration(120);
        mAnimator.start();
    }

    int showPage = 0;

    private void showDataPage(int position) {
        int i = position % getChildCount();
        int mathpage = 0;
        if (i >= 0) {
            mathpage = i;
        } else {
            mathpage = i + getChildCount();
        }
        if (mathpage != showPage) {
            showPage = mathpage;
            if (r3DPagechange != null) {
                r3DPagechange.onPageChanged(showPage);
            }
        }
    }

    R3DPagechange r3DPagechange;

    public void setR3DPagechange(R3DPagechange r3DPagechange) {
        this.r3DPagechange = r3DPagechange;
    }

    public interface R3DPagechange {
        void onPageChanged(int position);
    }
}           

*為下載下傳了之前資源的說聲抱歉-之前那個并不适用直接使用僅作為一個半成品

github下載下傳位址:https://github.com/liuyangxiao/3DViewGroup

如有疑問可私信交流^_^