????????看效果圖
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]);}}
}