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]);}}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/83599.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/83599.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/83599.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

48V帶極性反接保護-差共模浪涌防護方案

在工業自動化&#xff08;電動機驅動 / 工業機器人&#xff09;、交通基礎設施&#xff08;充電樁 / 車載電子&#xff09;、安防系統&#xff08;監控攝像頭 / 門禁&#xff09;、儲能設備&#xff08;BMS / 離網控制器&#xff09;等領域&#xff0c;DC48V 電源因安全特低電壓…

CentOS在vmware局域網內搭建DHCP服務器【踩坑記錄】

1. 重新設置環境 配置dhcp服務踩了不少坑&#xff0c;這里重頭搭建記錄一下&#xff1a; 1.1 centos 網卡還原 如果之前搭了亂七八糟的環境&#xff0c;導致NAT模式也沒法上網&#xff0c;這里重新還原 我們需要在NAT模式下聯網&#xff0c;下載DHCP服務 先把centos的網卡還…

《Docker》架構

文章目錄 架構模式單機架構應用數據分離架構應用服務器集群架構讀寫分離/主從分離架構冷熱分離架構垂直分庫架構微服務架構容器編排架構什么是容器&#xff0c;docker&#xff0c;鏡像&#xff0c;k8s 架構模式 單機架構 單機架構其實就是應用服務器和單機服務器都部署在同一…

Web3時代的數據保護挑戰與應對策略

隨著互聯網技術的飛速發展&#xff0c;我們正步入Web3時代&#xff0c;這是一個以去中心化、用戶主權和數據隱私為核心的新時代。然而&#xff0c;Web3時代也帶來了前所未有的數據保護挑戰。本文將探討這些挑戰&#xff0c;并提出相應的應對策略。 數據隱私挑戰 在Web3時代&a…

從零打造算法題刷題助手:Agent搭建保姆級攻略

我用Trae 做了一個有意思的Agent 「大廠機試助手」。 點擊 https://s.trae.com.cn/a/d2a596 立即復刻&#xff0c;一起來玩吧&#xff01; Agent 簡介 Agent名稱為大廠機試助手&#xff0c;主要功能有以下三點。 解題&#xff1a; 根據用戶給出的題目給出具體的解題思路引導做…

【JavaWeb】MVC三層架構

MVC三層架構 MVC 是什么&#xff1f;三層架構的組成&#xff08;View、Controller、Model&#xff09;各層職責劃分示例說明面試高頻問題與參考答案 MVC&#xff08;Model-View-Controller&#xff09;是一種經典的軟件設計模式&#xff0c;廣泛應用于 Web 應用開發中&#xf…

嵌入式分析利器:DuckDB與SqlSugar實戰

? 一、DuckDB 的核心特性與適用場景 DuckDB 是一款 嵌入式分析型數據庫&#xff08;OLAP&#xff09; &#xff0c;專為高效查詢設計&#xff0c;主要特點包括&#xff1a; 列式存儲與向量化引擎 數據按列存儲&#xff0c;提升聚合統計效率&#xff08;如 SUM/AVG&#xf…

React---day6、7

6、組件之間進行數據傳遞 **6.1 父傳子&#xff1a;**props傳遞屬性 父組件&#xff1a; <div><ChildCpn name"蔣乙菥" age"18" height"1,88" /> </div>子組件&#xff1a; export class ChildCpn extends React.Component…

Windows上用FFmpeg采集攝像頭推流 → MediaMTX服務器轉發流 → WSL2上拉流播放

1. Windows上 FFmpeg 推流&#xff08;攝像頭采集&#xff09; 設備名稱可用 ffmpeg -list_devices true -f dshow -i dummy 查詢&#xff0c;假設為Integrated Camera 采集推流示例&#xff08;推RTMP到MediaMTX&#xff09;&#xff1a; ffmpeg -rtbufsize 100M -f dshow …

SpringBoot1--簡單體驗

1 Helloworld 打開&#xff1a;https://start.spring.io/ 選擇maven配置。增加SpringWeb的依賴。 Generate之后解壓&#xff0c;代碼大致如下&#xff1a; hpDESKTOP-430500P:~/springboot2/demo$ tree ├── HELP.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── s…

MATLAB 中調整超參數的系統性方法

在深度學習中&#xff0c;超參數調整是提升模型性能的關鍵環節。以下是 MATLAB 中調整超參數的系統性方法&#xff0c;涵蓋核心參數、優化策略及實戰案例&#xff1a; 一、關鍵超參數及其影響 超參數作用典型范圍學習率 (Learning Rate)控制參數更新步長&#xff0c;影響收斂…

根目錄0xa0屬性對應的Ntfs!_SCB中的FileObject是什么時候被建立的----NTFS源代碼分析--重要

根目錄0xa0屬性對應的Ntfs!_SCB中的FileObject是什么時候被建立的 第一部分&#xff1a; 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…

(二)stm32使用4g模塊(移遠ec800k)連接mqtt

下面代碼是隨手寫的&#xff0c;沒有嚴謹測試僅供參考測試 uint8_t msgBuf[200]{"msg from mcu"}; uint8_t txBuf[250]{0}; uint16_t msgid0; uint16_t mqttTaskState0; uint16_t t100msCount0; uint8_t sendFlag10; uint8_t sendFlag20; void t100msTask1(void) { …

哈希表入門:用 C 語言實現簡單哈希表(開放尋址法解決沖突)

目錄 一、引言 二、代碼結構與核心概念解析 1. 數據結構定義 2. 初始化函數 initList 3. 哈希函數 hash 4. 插入函數 put&#xff08;核心邏輯&#xff09; 開放尋址法詳解&#xff1a; 三、主函數驗證與運行結果 1. 測試邏輯 2. 運行結果分析 四、完整代碼 五、優…

Windows下運行Redis并設置為開機自啟的服務

下載Redis-Windows 點擊redis-windows-7.4.0下載鏈接下載Redis 解壓之后得到如下文件 右鍵install_redis.cmd文件&#xff0c;選擇在記事本中編輯。 將這里改為redis.windows.conf后保存&#xff0c;退出記事本&#xff0c;右鍵后選擇以管理員身份運行。 在任務管理器中能夠…

2025年ESWA SCI1區TOP,改進成吉思汗鯊魚算法MGKSO+肝癌疾病預測,深度解析+性能實測

目錄 1.摘要2.成吉思汗鯊魚優化算法GKSO原理3.MGKSO4.結果展示5.參考文獻6.代碼獲取7.算法輔導應用定制讀者交流 1.摘要 本文針對肝癌&#xff08;HCC&#xff09;早期診斷難題&#xff0c;提出了一種基于改進成吉思汗鯊魚優化算法&#xff08;MGKSO&#xff09;的計算機輔助診…

李沐-動手學深度學習:RNN

1.RNN從零開始實現 import math import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2l#8.3.4節 #batch_size&#xff1a;每個小批量中子序列樣本的數目&#xff0c;num_steps&#xff1a;每個子序列中預定義的時間步數 #loa…

【C++ Qt】多元素控件(ListWidget、TableWidget、TreeWidget)

每日激勵&#xff1a;“不設限和自我肯定的心態&#xff1a;I can do all things。 — Stephen Curry” 緒論?&#xff1a; 本章將通過代碼示例詳細介紹了Qt中QListWidget、QTableWidget和QTreeWidget三種多元素控件的使用方法與核心功能&#xff0c;涵蓋列表的增刪操作、表格…

基于TI DSP控制的光伏逆變器最大功率跟蹤mppt

基于TI DSP&#xff08;如TMS320F28335&#xff09;控制的光伏逆變器最大功率跟蹤&#xff08;MPPT&#xff09;程序通常涉及以下幾個關鍵部分&#xff1a;硬件電路設計、MPPT算法實現、以及DSP的編程。以下是基于TI DSP的光伏逆變器MPPT程序的一個示例&#xff0c;主要采用擾動…

Python實現P-PSO優化算法優化卷積神經網絡CNN回歸模型項目實戰

說明&#xff1a;這是一個機器學習實戰項目&#xff08;附帶數據代碼文檔&#xff09;&#xff0c;如需數據代碼文檔可以直接到文章最后關注獲取。 1.項目背景 隨著人工智能和深度學習技術的快速發展&#xff0c;卷積神經網絡&#xff08;CNN&#xff09;在圖像分類、目標檢測…