看效果图
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; // 消息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 水平旋转轮播控件
/**
* 水平旋转轮播控件
* 实现了一个可以水平旋转的轮播图效果,支持自动轮播和手动滑动
* 特点:
* 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
<?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.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
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]);
}
}
}