Android 3D球形水平圆形旋转,旋转动态更换图片

看效果图

1、事件监听类

OnItemClickListener:3D旋转视图项点击监听器接口

java 复制代码
public interface OnItemClickListener {
    /**
     * 当旋转视图中的项被点击时调用
     *
     * @param view     被点击的视图对象
     * @param position 被点击项在旋转视图中的位置索引(从0开始)
     */
    void onItemClick(View view, int position);
}

OnItemSelectedListener:3D旋转视图项选中监听器接口

java 复制代码
public interface OnItemSelectedListener {
    /**
     * 当旋转视图中的选中项发生变化时调用
     *
     * @param item 新选中项在旋转视图中的位置索引(从0开始)
     * @param view 新选中的视图对象
     */
    void selected(int item, View view);
}

OnLoopViewTouchListener:3D旋转视图触摸事件监听器接口

java 复制代码
public interface OnLoopViewTouchListener {
    /**
     * 当旋转视图接收到触摸事件时调用
     *
     * @param event 触摸事件对象,包含触摸的类型、位置等信息
     */
    void onTouch(MotionEvent event);
}

2、3D水平旋转轮播控件

我这里是参考 https://github.com/yixiaolunhui/LoopRotarySwitch ,然后进行一个小改动。

LoopRotarySwitchViewHandler.java 轮播图自动滚动处理器

java 复制代码
/**
 * 轮播图自动滚动处理器
 * 用于控制轮播图的自动滚动功能
 * 特点:
 * 1. 支持自定义滚动时间间隔
 * 2. 支持开启/关闭自动滚动
 * 3. 支持自定义滚动方向
 */
public abstract class LoopRotarySwitchViewHandler extends Handler {

    private boolean loop = false;           // 是否开启自动滚动
    public long loopTime = 3000;           // 滚动时间间隔(毫秒)
    public static final int msgid = 1000;  // 消息ID

    private Message msg = createMsg();     // 创建消息对象

    /**
     * 构造方法
     * @param time 滚动时间间隔(毫秒)
     */
    public LoopRotarySwitchViewHandler(int time) {
        this.loopTime = time;
    }

    /**
     * 处理消息
     * 当收到消息时,如果开启了自动滚动,则执行滚动并发送下一条消息
     */
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what = msgid) {
            case msgid:
                if (loop) {
                    doScroll();    // 执行滚动
                    sendMsg();     // 发送下一条消息
                }
                break;
        }
        super.handleMessage(msg);
    }

    /**
     * 设置是否开启自动滚动
     * @param loop true开启自动滚动,false关闭自动滚动
     */
    public void setLoop(boolean loop) {
        this.loop = loop;
        if (loop) {
            sendMsg();    // 开启自动滚动,发送消息
        } else {
            try {
                removeMessages(msgid);    // 关闭自动滚动,移除消息
            } catch (Exception e) {
            }
        }
    }

    /**
     * 发送消息
     * 移除之前的消息,创建新消息并延迟发送
     */
    private void sendMsg() {
        try {
            removeMessages(msgid);    // 移除之前的消息
        } catch (Exception e) {
        }
        msg = createMsg();    // 创建新消息
        this.sendMessageDelayed(msg, loopTime);    // 延迟发送消息
    }

    /**
     * 创建消息对象
     * @return 消息对象
     */
    public Message createMsg() {
        Message msg = new Message();
        msg.what = msgid;
        return msg;
    }

    /**
     * 设置滚动时间间隔
     * @param loopTime 时间间隔(毫秒)
     */
    public void setLoopTime(long loopTime) {
        this.loopTime = loopTime;
    }

    /**
     * 获取滚动时间间隔
     * @return 时间间隔(毫秒)
     */
    public long getLoopTime() {
        return loopTime;
    }

    /**
     * 获取是否开启自动滚动
     * @return true开启,false关闭
     */
    public boolean isLoop() {
        return loop;
    }

    /**
     * 执行滚动
     * 由子类实现具体的滚动逻辑
     */
    public abstract void doScroll();
}

LoopRotarySwitchView.java 水平旋转轮播控件

java 复制代码
/**
 * 水平旋转轮播控件
 * 实现了一个可以水平旋转的轮播图效果,支持自动轮播和手动滑动
 * 特点:
 * 1. 支持水平方向旋转
 * 2. 支持自动轮播和手动滑动
 * 3. 支持自定义轮播方向
 * 4. 支持自定义轮播时间间隔
 * 5. 支持点击事件和选择事件
 */
public class LoopRotarySwitchView extends RelativeLayout {
    private final String TAG = "LoopRotarySwitchView";
    private final static int LoopR = 200; // 默认半径

    private final static int vertical = 0;   // 竖直方向
    private final static int horizontal = 1; // 水平方向

    private int mOrientation = horizontal;   // 当前方向,默认水平

    private Context mContext;                // 上下文

    private ValueAnimator restAnimator = null; // 回位动画
    private ValueAnimator rAnimation = null;   // 半径动画
    private ValueAnimator zAnimation = null;   // Z轴旋转动画
    private ValueAnimator xAnimation = null;   // X轴旋转动画

    private int loopRotationX = 0, loopRotationZ = 0; // X轴和Z轴的旋转角度

    private GestureDetector mGestureDetector = null; // 手势检测器

    private int selectItem = 0;    // 当前选中的item
    private int size = 4;          // item总数

    private float r = LoopR;       // 当前半径
    private float multiple = 2f;   // 倍数
    private float distance = multiple * r; // 观察距离,影响大小差异

    private float angle = 0;       // 当前旋转角度
    private float last_angle = 0;  // 上一次的角度

    private boolean autoRotation = false;    // 是否自动旋转
    private boolean touching = false;        // 是否正在触摸
    private boolean isAnimating = false;     // 是否正在动画中

    private AutoScrollDirection autoRotatinDirection = AutoScrollDirection.left; // 自动滚动方向

    private List<View> views = new ArrayList<View>(); // 子视图列表

    private OnItemSelectedListener onItemSelectedListener = null;    // 选择监听器
    private OnLoopViewTouchListener onLoopViewTouchListener = null;  // 触摸监听器
    private OnItemClickListener onItemClickListener = null;          // 点击监听器

    private boolean isCanClickListener = true; // 是否可以点击
    private float x;                          // 触摸的X坐标
    private float limitX = 30;                // 滑动阈值

    float spacingFactor = 1.2f; // 设置图片之间间距系数,可以调整这个值来改变间距

    private static boolean isFirstOpen = false; // 是否第一次打开这个页面


    /**
     * 自动滚动方向枚举
     */
    public enum AutoScrollDirection {
        left, right
    }

    /**
     * 构造方法
     */
    public LoopRotarySwitchView(Context context) {
        this(context, null);
    }

    /**
     * 构造方法
     */
    public LoopRotarySwitchView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * 构造方法
     * 初始化控件的基本属性和动画
     */
    public LoopRotarySwitchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        // 获取自定义属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoopRotarySwitchView);
        mOrientation = typedArray.getInt(R.styleable.LoopRotarySwitchView_orientation, horizontal);
        autoRotation = typedArray.getBoolean(R.styleable.LoopRotarySwitchView_autoRotation, false);
        r = typedArray.getDimension(R.styleable.LoopRotarySwitchView_r, LoopR);
        int direction = typedArray.getInt(R.styleable.LoopRotarySwitchView_direction, 0);
        typedArray.recycle();

        // 初始化手势检测器
        mGestureDetector = new GestureDetector(context, getGeomeryController());

        // 设置旋转方向
        if (mOrientation == horizontal) {
            loopRotationZ = 0;
        } else {
            loopRotationZ = 90;
        }

        // 设置自动滚动方向
        if (direction == 0) {
            autoRotatinDirection = AutoScrollDirection.left;
        } else {
            autoRotatinDirection = AutoScrollDirection.right;
        }

        // 启动自动滚动
        loopHandler.setLoop(autoRotation);
    }

    /**
     * handler处理
     */
    @SuppressLint("HandlerLeak")
    LoopRotarySwitchViewHandler loopHandler = new LoopRotarySwitchViewHandler(3000) {
        @Override
        public void doScroll() {
            try {
                if (size != 0) {//判断自动滑动从那边开始
                    int perAngle = 0;
                    switch (autoRotatinDirection) {
                        case left:
                            perAngle = 360 / size;
                            break;
                        case right:
                            perAngle = -360 / size;
                            break;
                    }
                    if (angle == 360) {
                        angle = 0f;
                    }
                    animRotationTo(angle + perAngle, null);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };

    /**
     * 排序
     * 对子View 排序,然后根据变化选中是否重绘,这样是为了实现view 在显示的时候来控制当前要显示的是哪三个view,可以改变排序看下效果
     *
     * @param list
     */
    @SuppressWarnings("unchecked")
    private <T> void sortList(List<View> list) {

        @SuppressWarnings("rawtypes")
        Comparator comparator = new SortComparator();
        T[] array = list.toArray((T[]) new Object[list.size()]);

        Arrays.sort(array, comparator);
        int i = 0;
        ListIterator<T> it = (ListIterator<T>) list.listIterator();
        while (it.hasNext()) {
            it.next();
            it.set(array[i++]);
        }
        for (int j = 0; j < list.size(); j++) {
            list.get(j).bringToFront();
        }
    }

    /**
     * 筛选器
     */
    private class SortComparator implements Comparator<View> {
        @Override
        public int compare(View lhs, View rhs) {
            int result = 0;
            try {
                result = (int) (1000 * lhs.getScaleX() - 1000 * rhs.getScaleX());
            } catch (Exception e) {
            }
            return result;
        }
    }

    /**
     * 手势
     *
     * @return
     */
    private GestureDetector.SimpleOnGestureListener getGeomeryController() {
        return new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                // 降低滑动灵敏度,将系数从1/9改为1/12,使滑动更平滑
                float sensitivity = 12.0f; // 滑动灵敏度参数,值越大灵敏度越低
                float deltaAngle = (float) (Math.cos(Math.toRadians(loopRotationZ)) * (distanceX / sensitivity)
                        + Math.sin(Math.toRadians(loopRotationZ)) * (distanceY / sensitivity));

                // 计算滑动后的角度
                float newAngle = angle + deltaAngle;

                // 计算每个item占的角度
                float itemAngle = 360f / size;

                // 限制滑动范围,确保一次只能滑动一个item
                float angleDiff = Math.abs(newAngle - last_angle);
                if (angleDiff <= itemAngle) {
                    angle = newAngle;
                    initView();
                }
                return true;
            }
        };
    }

    /**
     * 初始化视图
     * 计算每个item的位置、大小和透明度
     */
    public void initView() {
        for (int i = 0; i < views.size(); i++) {
            double radians = angle + 180 - i * 360 / size;
            float x0 = (float) Math.sin(Math.toRadians(radians)) * r;
            float y0 = (float) Math.cos(Math.toRadians(radians)) * r;

            // 使用单个变量控制缩放效果
            float scaleRange = 0.5f; // 缩放范围,值越大,中间和两侧的差异越大
            float minScale = 1.0f - scaleRange; // 最小缩放比例 = 1.0 - 缩放范围

            // 计算缩放比例
            float baseScale = (distance - y0) / (distance + r);
            float scale0 = minScale + baseScale * scaleRange;
            views.get(i).setScaleX(scale0);
            views.get(i).setScaleY(scale0);

            // 计算位置
            float adjustedX0 = x0 * spacingFactor; // 增加水平方向的间距
            float rotationX_y = (float) Math.sin(Math.toRadians(loopRotationX * Math.cos(Math.toRadians(radians)))) * r;
            float rotationZ_y = -(float) Math.sin(Math.toRadians(-loopRotationZ)) * adjustedX0;
            float rotationZ_x = (((float) Math.cos(Math.toRadians(-loopRotationZ)) * adjustedX0) - adjustedX0);
            views.get(i).setTranslationX(adjustedX0 + rotationZ_x);
            views.get(i).setTranslationY(rotationX_y + rotationZ_y);

            // 设置透明度
            float alpha = 1.0f;
            float normalizedAngle = (float) (radians % 360);
            if (normalizedAngle < 0) {
                normalizedAngle += 360;
            }
            // 中间位置不透明,两侧半透明
            if (Math.abs(normalizedAngle - 180) < 30) {
                alpha = 1.0f;
            } else {
                alpha = 0.3f;
            }
            views.get(i).setAlpha(alpha);
        }

        // 对视图进行排序
        List<View> arrayViewList = new ArrayList<>();
        arrayViewList.clear();
        for (int i = 0; i < views.size(); i++) {
            arrayViewList.add(views.get(i));
        }
        sortList(arrayViewList);
        postInvalidate();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        initView();
        if (autoRotation) {
            loopHandler.sendEmptyMessageDelayed(LoopRotarySwitchViewHandler.msgid, loopHandler.loopTime);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        Log.d(TAG, "===== onLayout() =====");
        if (changed) {
            checkChildView();
            if (onItemSelectedListener != null) {
                isCanClickListener = true;
                onItemSelectedListener.selected(selectItem, views.get(selectItem));
            }
            Log.d(TAG, "isFirstOpen:" + isFirstOpen);
            // 如果是第一次打开,就执行动画
            if (!isFirstOpen) {
                isFirstOpen = true;
                rAnimation(); // 执行,启动动画

            }else{
                 // 直接初始化视图,不执行动画
                 initView();
            }
        }
    }

    public void rAnimation() {
        rAnimation(1f, r);
    }

    public void rAnimation(boolean fromZeroToLoopR) {
        if (fromZeroToLoopR) {
            rAnimation(1f, LoopR);
        } else {
            rAnimation(LoopR, 1f);
        }
    }

    public void rAnimation(float from, float to) {
        rAnimation = ValueAnimator.ofFloat(from, to);
        rAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                r = (Float) valueAnimator.getAnimatedValue();
                initView();
            }
        });
        rAnimation.setInterpolator(new DecelerateInterpolator());
        rAnimation.setDuration(2000);
        rAnimation.start();
    }


    /**
     * 初始化view
     */
    public void checkChildView() {
        //for (int i = 0; i < views.size(); i++) {//先清空views里边可能存在的view防止重复
        //    views.remove(i);
        //}
        views.clear();
        final int count = getChildCount(); //获取子View的个数
        size = count;

        for (int i = 0; i < count; i++) {
            View view = getChildAt(i); //获取指定的子view
            final int position = i;
            views.add(view);
            view.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    //对子view添加点击事件
                    /*if (position != selectItem) {
                        setSelectItem(position);
                    } else {
                        if (isCanClickListener && onItemClickListener != null) {
                            onItemClickListener.onItemClick(views.get(position),position);
                        }
                    }*/
                    // 只保留点击回调,不进行切换
                    if (isCanClickListener && onItemClickListener != null) {
                        onItemClickListener.onItemClick(views.get(position), position);
                    }
                }
            });
        }
    }

    /**
     * 复位
     */
    private void restPosition() {
        if (size == 0) {
            return;
        }
        float finall = 0;
        float part = 360 / size;//一份的角度
        if (angle < 0) {
            part = -part;
        }
        float minvalue = (int) (angle / part) * part;//最小角度
        float maxvalue = (int) (angle / part) * part + part;//最大角度

        // 优化复位逻辑,使动画更流畅
        if (angle >= 0) {
            if (angle - last_angle > 0) {
                // 向右滑动,移动到下一个位置
                finall = maxvalue;
            } else {
                // 向左滑动,移动到上一个位置
                finall = minvalue;
            }
        } else {
            if (angle - last_angle < 0) {
                // 向右滑动,移动到下一个位置
                finall = maxvalue;
            } else {
                // 向左滑动,移动到上一个位置
                finall = minvalue;
            }
        }
        animRotationTo(finall, null);
    }


    /**
     * 动画
     *
     * @param finall
     * @param complete
     */
    private void animRotationTo(float finall, final Runnable complete) {
        if (angle == finall) {//如果相同说明不需要旋转
            return;
        }

        // 设置动画状态为正在动画中
        isAnimating = true;

        restAnimator = ValueAnimator.ofFloat(angle, finall);
        // 使用更平滑的插值器
        restAnimator.setInterpolator(new DecelerateInterpolator(1.5f));
        // 增加动画时间,使旋转更平滑
        restAnimator.setDuration(500);

        restAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if (!touching) {
                    angle = (Float) animation.getAnimatedValue();
                    initView();
                }
            }
        });
        restAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                // 动画结束,设置状态为非动画中
                isAnimating = false;

                if (touching == false) {
                    selectItem = calculateItem();
                    if (selectItem < 0) {
                        selectItem = size + selectItem;
                    }
                    if (onItemSelectedListener != null) {
                        if(views.size()<=0){
                            views.add(new View(mContext));
                            views.add(new View(mContext));
                            views.add(new View(mContext));
                            views.add(new View(mContext));
                        }
                        onItemSelectedListener.selected(selectItem, views.get(selectItem));
                    }
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                // 动画取消,也设置状态为非动画中
                isAnimating = false;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });

        if (complete != null) {
            restAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    complete.run();
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                }

                @Override
                public void onAnimationRepeat(Animator animation) {
                }
            });
        }
        restAnimator.start();
    }

    /**
     * 通过角度计算是第几个item
     *
     * @return
     */
    private int calculateItem() {
        return (int) (angle / (360 / size)) % size;
    }

    /**
     * 触摸方法
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (onLoopViewTouchListener != null) {
            onLoopViewTouchListener.onTouch(event);
        }
        isCanClickListener(event);
        // 确保我们始终消费触摸事件,不让它传递到其他视图
        return true;
    }


    /**
     * 触摸停止计时器,抬起设置可下啦刷新
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        onTouch(ev);
        if (onLoopViewTouchListener != null) {
            onLoopViewTouchListener.onTouch(ev);
        }
        isCanClickListener(ev);
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 触摸操作
     *
     * @param event
     * @return
     */
    private boolean onTouch(MotionEvent event) {
        // 如果正在动画中,不处理触摸事件
        if (isAnimating) {
            return true;
        }

        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            last_angle = angle;
            touching = true;
        }
        boolean sc = mGestureDetector.onTouchEvent(event);
        if (sc) {
            this.getParent().requestDisallowInterceptTouchEvent(true);//通知父控件勿拦截本控件
        }
        if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
            touching = false;
            restPosition();
            return true;
        }
        return true;
    }

    /**
     * 是否可以点击回调
     *
     * @param event
     */
    public void isCanClickListener(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                x = event.getX();
                if (autoRotation) {
                    loopHandler.removeMessages(LoopRotarySwitchViewHandler.msgid);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(event.getX() - x) > limitX) {
                    isCanClickListener = false;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (autoRotation) {
                    loopHandler.sendEmptyMessageDelayed(LoopRotarySwitchViewHandler.msgid, loopHandler.loopTime);
                }
                if (Math.abs(event.getX() - x) <= limitX) {
                    isCanClickListener = true;
                }
                break;
        }
    }

    /**
     * 获取所有的view
     *
     * @return
     */
    public List<View> getViews() {
        return views;
    }

    /**
     * 获取角度
     *
     * @return
     */
    public float getAngle() {
        return angle;
    }


    /**
     * 设置角度
     *
     * @param angle
     */
    public void setAngle(float angle) {
        this.angle = angle;
    }

    /**
     * 获取距离
     *
     * @return
     */
    public float getDistance() {
        return distance;
    }

    /**
     * 设置距离
     *
     * @param distance
     */
    public void setDistance(float distance) {
        this.distance = distance;
    }

    /**
     * 获取半径
     *
     * @return
     */
    public float getR() {
        return r;
    }

    /**
     * 获取选择是第几个item
     *
     * @return
     */
    public int getSelectItem() {
        return selectItem;
    }

    /**
     * 设置选中方法
     *
     * @param selectItem
     */
    public void setSelectItem(int selectItem) {

        if (selectItem >= 0) {
            float jiaodu = 0;
            if (getSelectItem() == 0) {
                if (selectItem == views.size() - 1) {
                    jiaodu = angle - (360 / size);
                } else {
                    jiaodu = angle + (360 / size); // 686行
                }
            } else if (getSelectItem() == views.size() - 1) {
                if (selectItem == 0) {
                    jiaodu = angle + (360 / size);
                } else {
                    jiaodu = angle - (360 / size);
                }
            } else {
                if (selectItem > getSelectItem()) {
                    jiaodu = angle + (360 / size);
                } else {
                    jiaodu = angle - (360 / size);
                }
            }

            float finall = 0;
            float part = 360 / size;//一份的角度
            if (jiaodu < 0) {
                part = -part;
            }
            float minvalue = (int) (jiaodu / part) * part;//最小角度
            float maxvalue = (int) (jiaodu / part) * part;//最大角度
            if (jiaodu >= 0) {//分为是否小于0的情况
                if (jiaodu - last_angle > 0) {
                    finall = maxvalue;
                } else {
                    finall = minvalue;
                }
            } else {
                if (jiaodu - last_angle < 0) {
                    finall = maxvalue;
                } else {
                    finall = minvalue;
                }
            }

            if (size > 0) animRotationTo(finall, null);
        }
    }

    /**
     * 设置半径
     *
     * @param r
     */
    public LoopRotarySwitchView setR(float r) {
        this.r = r;
        distance = multiple * r;
        return this;
    }

    /**
     * 选中回调接口实现
     *
     * @param onItemSelectedListener
     */
    public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
        this.onItemSelectedListener = onItemSelectedListener;
    }

    /**
     * 点击事件回调
     *
     * @param onItemClickListener
     */
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    /**
     * 触摸时间回调
     *
     * @param onLoopViewTouchListener
     */
    public void setOnLoopViewTouchListener(OnLoopViewTouchListener onLoopViewTouchListener) {
        this.onLoopViewTouchListener = onLoopViewTouchListener;
    }

    /**
     * 设置是否自动切换
     *
     * @param autoRotation
     */
    public LoopRotarySwitchView setAutoRotation(boolean autoRotation) {
        this.autoRotation = autoRotation;
        loopHandler.setLoop(autoRotation);
        return this;
    }

    /**
     * 获取自动切换时间
     *
     * @return
     */
    public long getAutoRotationTime() {
        return loopHandler.loopTime;
    }

    /**
     * 设置自动切换时间间隔
     *
     * @param autoRotationTime
     */
    public LoopRotarySwitchView setAutoRotationTime(long autoRotationTime) {
        loopHandler.setLoopTime(autoRotationTime);
        return this;
    }

    /**
     * 是否自动切换
     *
     * @return
     */
    public boolean isAutoRotation() {
        return autoRotation;
    }

    /**
     * 设置倍数
     *
     * @param mMultiple 设置这个必须在setR之前调用,否则无效
     * @return
     */
    public LoopRotarySwitchView setMultiple(float mMultiple) {
        this.multiple = mMultiple;
        return this;
    }

    public LoopRotarySwitchView setAutoScrollDirection(AutoScrollDirection mAutoScrollDirection) {
        this.autoRotatinDirection = mAutoScrollDirection;
        return this;
    }

    public void createXAnimation(int from, int to, boolean start) {
        if (xAnimation != null) if (xAnimation.isRunning() == true) xAnimation.cancel();
        xAnimation = ValueAnimator.ofInt(from, to);
        xAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                loopRotationX = (Integer) animation.getAnimatedValue();
                initView();
            }
        });
        xAnimation.setInterpolator(new DecelerateInterpolator());
        xAnimation.setDuration(2000);
        if (start) xAnimation.start();
    }


    public ValueAnimator createZAnimation(int from, int to, boolean start) {
        if (zAnimation != null) if (zAnimation.isRunning() == true) zAnimation.cancel();
        zAnimation = ValueAnimator.ofInt(from, to);
        zAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                loopRotationZ = (Integer) animation.getAnimatedValue();
                initView();
            }
        });
        zAnimation.setInterpolator(new DecelerateInterpolator());
        zAnimation.setDuration(2000);
        if (start) zAnimation.start();
        return zAnimation;
    }

    /**
     * 设置方向
     *
     * @param mOrientation
     * @return
     */
    public LoopRotarySwitchView setOrientation(int mOrientation) {
        setHorizontal(mOrientation == horizontal, false);
        return this;
    }

    public LoopRotarySwitchView setHorizontal(boolean horizontal, boolean anim) {
        if (anim) {
            if (horizontal) {
                createZAnimation(getLoopRotationZ(), 0, true);
            } else {
                createZAnimation(getLoopRotationZ(), 90, true);
            }
        } else {
            if (horizontal) {
                setLoopRotationZ(0);
            } else {
                setLoopRotationZ(90);
            }
            initView();
        }
        return this;
    }

    public LoopRotarySwitchView setLoopRotationX(int loopRotationX) {
        this.loopRotationX = loopRotationX;
        return this;
    }

    public LoopRotarySwitchView setLoopRotationZ(int loopRotationZ) {
        this.loopRotationZ = loopRotationZ;
        return this;
    }

    public int getLoopRotationX() {
        return loopRotationX;
    }

    public int getLoopRotationZ() {
        return loopRotationZ;
    }

    public ValueAnimator getRestAnimator() {
        return restAnimator;
    }

    public ValueAnimator getrAnimation() {
        return rAnimation;
    }

    public void setzAnimation(ValueAnimator zAnimation) {
        this.zAnimation = zAnimation;
    }

    public ValueAnimator getzAnimation() {
        return zAnimation;
    }

    public void setxAnimation(ValueAnimator xAnimation) {
        this.xAnimation = xAnimation;
    }

    public ValueAnimator getxAnimation() {
        return xAnimation;
    }
}

3、自定义属性

values/attrs.xml

java 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--3D旋转-->
    <declare-styleable name="LoopRotarySwitchView">
        <attr name="orientation" format="integer">
            <enum name="vertical" value="0" />
            <enum name="horizontal" value="1" />
        </attr>
        <attr name="autoRotation" format="boolean" />
        <attr name="r" format="dimension" />

        <attr name="direction" format="integer">
            <enum name="left" value="0" />
            <enum name="right" value="1" />
        </attr>
    </declare-styleable>
</resources>

4、布局activity_main.xml

图片有点大就不上传了,图片资源可以去豆包生成。

java 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.custome.rotation.view.LoopRotarySwitchView
        android:id="@+id/mLoopRotarySwitchView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center">

        <ImageView
            android:id="@+id/iv0"
            android:layout_width="250dp"
            android:layout_height="250dp"
            android:src="@drawable/girl1" />

        <ImageView
            android:id="@+id/iv1"
            android:layout_width="250dp"
            android:layout_height="250dp"
            android:src="@drawable/girl2" />

        <ImageView
            android:id="@+id/iv2"
            android:layout_width="250dp"
            android:layout_height="250dp"
            android:src="@drawable/girl3" />

        <ImageView
            android:id="@+id/iv3"
            android:layout_width="250dp"
            android:layout_height="250dp"
            android:src="@drawable/girl4" />
    </com.custome.rotation.view.LoopRotarySwitchView>
</LinearLayout>

5、MainActivity.java

java 复制代码
public class MainActivity extends AppCompatActivity {

    private String TAG = "MainActivity";
    private ImageView[] ivs = new ImageView[4];
    private int[] imageViews = {
            R.drawable.girl1, R.drawable.girl2, R.drawable.girl3, R.drawable.girl4, R.drawable.girl5,
            R.drawable.girl6, R.drawable.girl7, R.drawable.girl8, R.drawable.girl9, R.drawable.girl10};
    private LoopRotarySwitchView mLoopRotarySwitchView;

    private int lastSelectedPosition = 0; // 记录上一次选中的位置
    private int currentImageIndex = 0; // 当前显示图片在imageViews数组中的索引

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mLoopRotarySwitchView = findViewById(R.id.mLoopRotarySwitchView);

        mLoopRotarySwitchView
                .setR(220)// 设置3D旋转视图的半径,值越大图片间距越大,旋转效果越明显
                .setAutoScrollDirection(LoopRotarySwitchView.AutoScrollDirection.left)//切换方向
        .setAutoRotation(true)//是否自动切换
        .setAutoRotationTime(2000);//自动切换的时间  单位毫秒

        ivs[0] = findViewById(R.id.iv0);
        ivs[1] = findViewById(R.id.iv1);
        ivs[2] = findViewById(R.id.iv2);
        ivs[3] = findViewById(R.id.iv3);

        // TODO 设置轮播图的点击监听
        mLoopRotarySwitchView.setOnItemClickListener((view, position) -> {
            Log.d("MainActivity", "轮播图点击位置: " + mLoopRotarySwitchView.getSelectItem());
        });

        // TODO 轮播图滑动切换事件
        mLoopRotarySwitchView.setOnItemSelectedListener((position, view) -> {
            Log.d(TAG,"===== mLoopRotarySwitchView.setOnItemSelectedListener() =====");
            Log.d("MainActivity", "当前是第" + position + "个item,lastPosition:" + lastSelectedPosition);

            if(position ==lastSelectedPosition ){
                Log.d(TAG,"位置一致,不进行切换");
                return;
            }
            // 计算位置变化值,用于判断滑动方向
            // 假设按顺序滑动的情况:
            // position=0, lastPosition=0: 0-0=0 (初始状态,无变化)
            // position=1, lastPosition=0: 1-0=1 (向右滑动1位)
            // position=2, lastPosition=1: 2-1=1 (向右滑动1位)
            // position=3, lastPosition=2: 3-2=1 (向右滑动1位)
            // position=0, lastPosition=3: 0-3=-3 (从最右到最左,delta为负数且小于-1)
            // 
            // 如果反向滑动:
            // position=2, lastPosition=3: 2-3=-1 (向左滑动1位)
            // position=1, lastPosition=2: 1-2=-1 (向左滑动1位)
            // position=0, lastPosition=1: 0-1=-1 (向左滑动1位)
            // position=3, lastPosition=0: 3-0=3 (从最左到最右,delta为正数且大于1)
            int delta = position - lastSelectedPosition;

            // 判断滑动方向
            // delta == 1: 正常向右滑动一格的情况
            // delta < -1: 从最右边(position=3)滑动到最左边(position=0)的情况,此时delta=-3
            boolean isNext = (delta == 1 || delta < -1); // 向右滑动或从最右到最左

            // 检查是否可以播放下一曲或上一曲
            if (isNext) {
                // 下一图片。 例如 currentImageIndex=0,共计10张图
                // (0 + 1) % 4 = 1
                // (1 + 1) % 4 = 2
                // (2 + 1) % 4 = 3
                // (3 + 1) % 4 = 0
                // (4 + 1) % 4 = 1
                // (5 + 1) % 4 = 2
                // (6 + 1) % 4 = 3
                // (7 + 1) % 4 = 0
                // (8 + 1) % 4 = 1
                // (9 + 1) % 4 = 2
                currentImageIndex = (currentImageIndex + 1) % imageViews.length;
            } else {
                // 上一图片。例如 currentImageIndex=0,共计10张图
                // (0 - 1 + 4) % 4 = 3
                // (1 - 1 + 4) % 4 = 0
                // (2 - 1 + 4) % 4 = 1
                // (3 - 1 + 4) % 4 = 2
                // (4 - 1 + 4) % 4 = 3
                // (5 - 1 + 4) % 4 = 0
                // (6 - 1 + 4) % 4 = 1
                // (7 - 1 + 4) % 4 = 2
                // (8 - 1 + 4) % 4 = 3
                // (9 - 1 + 4) % 4 = 0
                currentImageIndex = (currentImageIndex - 1 + imageViews.length) % imageViews.length;
            }

            // 更新上一次的位置
            lastSelectedPosition = position;

            // 更新所有专辑封面
            updateRotatingImages();
        });
    }


    private void updateRotatingImages() {
        Log.d(TAG,"===== updateRotatingImages() =====");
        int total = imageViews.length;

        if (total < 2) {
            Log.d(TAG, "歌曲数量少于2张图,不进行复杂图片切换");
            return;
        }

        // 记录当前图片索引
        Log.d(TAG, "updateRotatingImages: currentImageIndex=" + currentImageIndex);

        // 获取当前选中的3D图片位置(0-3)
        int currentViewPosition = mLoopRotarySwitchView.getSelectItem();
        Log.d(TAG, "当前选中的3D轮播位置: " + currentViewPosition);

        // 根据当前选中的位置,设置图片的显示顺序
        for (int i = 0; i < ivs.length; i++) {
            int imageIndex;

            // 判断条件1:当前遍历到的位置(i)就是被选中的位置(currentViewPosition)
            // 例如:currentViewPosition=2时,当i=2时这个条件为true
            // 这种情况下,我们希望在当前选中位置显示currentImageIndex对应的图片
            if (i == currentViewPosition) {
                // 当前选中的位置显示图片
                // 例如:currentViewPosition=1, i=1时
                // 此处直接使用currentImageIndex,不需要计算
                imageIndex = currentImageIndex;
                Log.d(TAG, "位置 " + i + " (当前选中): 显示当前图片,索引=" + imageIndex);
            }
            
            // 判断条件2:判断当前遍历位置(i)是否是选中位置的右侧(顺时针下一个)
            // 条件有两部分:
            // 第一部分:i == (currentViewPosition + 1) % 4
            //   - 正常情况下,右侧位置就是(当前位置+1)%4
            //   - 例如:currentViewPosition=1时,右侧是(1+1)%4=2
            //   - 例如:currentViewPosition=2时,右侧是(2+1)%4=3
            // 第二部分:(currentViewPosition == 3 && i == 0)
            //   - 特殊情况:当选中的是最后一个位置(3)时,右侧应该是第一个位置(0)
            //   - 例如:currentViewPosition=3时,右侧是0而不是(3+1)%4=0,这是同一个结果
            //   - 这个条件是为了明确指出这种情况
            else if ((i == (currentViewPosition + 1) % 4) || (currentViewPosition == 3 && i == 0)) {
                // 右侧位置(顺时针下一个)显示下张图片
                // 例如:currentViewPosition=1, currentImageIndex=5时
                // 对于i=2: (1+1)%4=2, 条件成立
                // 图片索引计算: (5+1)%10=6
                // 对于currentViewPosition=3情况: 
                // 特殊处理i=0位置,因为(3+1)%4=0
                imageIndex = (currentImageIndex + 1) % total;
                Log.d(TAG, "位置 " + i + " (右侧): 显示下张图片,索引=" + imageIndex);
            }

            // 判断条件3:判断当前遍历位置(i)是否是选中位置的对面位置(隔着一个)
            // 条件有两部分:
            // 第一部分:i == (currentViewPosition + 2) % 4
            //   - 正常情况下,对面位置就是(当前位置+2)%4
            //   - 例如:currentViewPosition=0时,对面是(0+2)%4=2
            //   - 例如:currentViewPosition=1时,对面是(1+2)%4=3
            // 第二部分:(currentViewPosition >= 2 && i == (currentViewPosition - 2 + 4) % 4)
            //   - 另一种表达方式:当选中位置>=2时,对面位置也可以表示为(当前位置-2+4)%4
            //   - 例如:currentViewPosition=2时,对面是(2-2+4)%4=0
            //   - 例如:currentViewPosition=3时,对面是(3-2+4)%4=1
            //   - 这是为了保持逻辑的一致性和代码的可读性
            else if ((i == (currentViewPosition + 2) % 4) || (currentViewPosition >= 2 && i == (currentViewPosition - 2 + 4) % 4)) {
                // 对面位置(隔一个)显示下下张图片
                // 例如:currentViewPosition=1, currentImageIndex=5时
                // 对于i=3: (1+2)%4=3, 条件成立
                // 图片索引计算: (5+2)%10=7
                // 
                // 对于currentViewPosition=2情况:
                // i=0时, (2-2+4)%4=0, 条件成立
                // 对于currentViewPosition=3情况:
                // i=1时, (3-2+4)%4=1, 条件成立
                imageIndex = (currentImageIndex + 2) % total;
                Log.d(TAG, "位置 " + i + " (对面): 显示下下张图片,索引=" + imageIndex);
            }

            // 判断条件4:当上述所有条件都不满足时,当前位置(i)就是选中位置的左侧(顺时针前一个)
            // 这相当于:i == (currentViewPosition - 1 + 4) % 4
            // - 例如:currentViewPosition=1时,左侧是(1-1+4)%4=0
            // - 例如:currentViewPosition=2时,左侧是(2-1+4)%4=1
            // - 例如:currentViewPosition=0时,左侧是(0-1+4)%4=3
            // 注意:加4是为了避免负数,确保结果在0-3之间
            else {
                // 左侧位置(顺时针前一个)显示上一张图片
                // 例如:currentViewPosition=1, currentImageIndex=5时
                // 对于i=0: 不满足上述所有条件,所以是左侧位置
                // 图片索引计算: (5-1+10)%10=4
                // 
                // 注意:加上total(10)是为了避免负数,如当currentImageIndex=0时:
                // (0-1+10)%10=9,确保能够循环到最后一张图片
                imageIndex = (currentImageIndex - 1 + total) % total;
                Log.d(TAG, "位置 " + i + " (左侧): 显示上一张图片,索引=" + imageIndex);
            }

            // 设置对应位置的ImageView显示相应的图片
            ivs[i].setImageResource(imageViews[imageIndex]);
        }
    }
}
相关推荐
CYRUS_STUDIO2 小时前
FART 脱壳某大厂 App + CodeItem 修复 dex + 反编译还原源码
android·安全·逆向
Shujie_L4 小时前
【Android基础回顾】四:ServiceManager
android
Think Spatial 空间思维5 小时前
【实施指南】Android客户端HTTPS双向认证实施指南
android·网络协议·https·ssl
louisgeek5 小时前
Git 使用 SSH 连接
android
二流小码农5 小时前
鸿蒙开发:实现一个标题栏吸顶
android·ios·harmonyos
八月林城6 小时前
echarts在uniapp中使用安卓真机运行时无法显示的问题
android·uni-app·echarts
雨白7 小时前
搞懂 Fragment 的生命周期
android
casual_clover7 小时前
Android 之 kotlin语言学习笔记三(Kotlin-Java 互操作)
android·java·kotlin
梓仁沐白7 小时前
【Kotlin】数字&字符串&数组&集合
android·开发语言·kotlin
技术小甜甜7 小时前
【Godot】如何导出 Release 版本的安卓项目
android·游戏引擎·godot