Android 3D球形水平圆形旋转,旋转动态更换图片
看效果图
1、事件监听类
OnItemClickListener:3D旋转视图项点击监听器接口
public interface OnItemClickListener {/*** 当旋转视图中的项被点击时调用** @param view 被点击的视图对象* @param position 被点击项在旋转视图中的位置索引(从0开始)*/void onItemClick(View view, int position);
}
OnItemSelectedListener:3D旋转视图项选中监听器接口
public interface OnItemSelectedListener {/*** 当旋转视图中的选中项发生变化时调用** @param item 新选中项在旋转视图中的位置索引(从0开始)* @param view 新选中的视图对象*/void selected(int item, View view);
}
OnLoopViewTouchListener:3D旋转视图触摸事件监听器接口
public interface OnLoopViewTouchListener {/*** 当旋转视图接收到触摸事件时调用** @param event 触摸事件对象,包含触摸的类型、位置等信息*/void onTouch(MotionEvent event);
}
2、3D水平旋转轮播控件
我这里是参考 https://github.com/yixiaolunhui/LoopRotarySwitch ,然后进行一个小改动。
LoopRotarySwitchViewHandler.java 轮播图自动滚动处理器
/*** 轮播图自动滚动处理器* 用于控制轮播图的自动滚动功能* 特点:* 1. 支持自定义滚动时间间隔* 2. 支持开启/关闭自动滚动* 3. 支持自定义滚动方向*/
public abstract class LoopRotarySwitchViewHandler extends Handler {private boolean loop = false; // 是否开启自动滚动public long loopTime = 3000; // 滚动时间间隔(毫秒)public static final int msgid = 1000; // 消息IDprivate Message msg = createMsg(); // 创建消息对象/*** 构造方法* @param time 滚动时间间隔(毫秒)*/public LoopRotarySwitchViewHandler(int time) {this.loopTime = time;}/*** 处理消息* 当收到消息时,如果开启了自动滚动,则执行滚动并发送下一条消息*/@Overridepublic 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 水平旋转轮播控件
/*** 水平旋转轮播控件* 实现了一个可以水平旋转的轮播图效果,支持自动轮播和手动滑动* 特点:* 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; // 当前选中的itemprivate 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) {@Overridepublic 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> {@Overridepublic 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() {@Overridepublic 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;// 限制滑动范围,确保一次只能滑动一个itemfloat 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();}@Overrideprotected 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);}}@Overrideprotected 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() {@Overridepublic 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); //获取指定的子viewfinal int position = i;views.add(view);view.setOnClickListener(new OnClickListener() {@Overridepublic 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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {if (!touching) {angle = (Float) animation.getAnimatedValue();initView();}}});restAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic 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));}}}@Overridepublic void onAnimationCancel(Animator animation) {// 动画取消,也设置状态为非动画中isAnimating = false;}@Overridepublic void onAnimationRepeat(Animator animation) {}});if (complete != null) {restAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {complete.run();}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic void onAnimationRepeat(Animator animation) {}});}restAnimator.start();}/*** 通过角度计算是第几个item** @return*/private int calculateItem() {return (int) (angle / (360 / size)) % size;}/*** 触摸方法** @param event* @return*/@Overridepublic boolean onTouchEvent(MotionEvent event) {if (onLoopViewTouchListener != null) {onLoopViewTouchListener.onTouch(event);}isCanClickListener(event);// 确保我们始终消费触摸事件,不让它传递到其他视图return true;}/*** 触摸停止计时器,抬起设置可下啦刷新*/@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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
<?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
图片有点大就不上传了,图片资源可以去豆包生成。
<?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.LoopRotarySwitchViewandroid:id="@+id/mLoopRotarySwitchView"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="center"android:gravity="center"><ImageViewandroid:id="@+id/iv0"android:layout_width="250dp"android:layout_height="250dp"android:src="@drawable/girl1" /><ImageViewandroid:id="@+id/iv1"android:layout_width="250dp"android:layout_height="250dp"android:src="@drawable/girl2" /><ImageViewandroid:id="@+id/iv2"android:layout_width="250dp"android:layout_height="250dp"android:src="@drawable/girl3" /><ImageViewandroid:id="@+id/iv3"android:layout_width="250dp"android:layout_height="250dp"android:src="@drawable/girl4" /></com.custome.rotation.view.LoopRotarySwitchView>
</LinearLayout>
5、MainActivity.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数组中的索引@Overrideprotected 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=-3boolean 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 = 2currentImageIndex = (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 = 0currentImageIndex = (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=0imageIndex = (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]);}}
}