介绍

类似的玩意当我还是个安卓程序员的时候就用过很多了。只不过ugui里没有viewpaper可以用,需要我们自己写个组件。然后动画要配合animaiton了。

实现过程

1.滑动事件封装

基本上原封不动把滑动相关参数封装起来做成事件,把回调方法预留出来。

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

public class UISpriteDraggable : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler {
    public class UISpriteDraggableDragEvent : UnityEvent<PointerEventData> {
    }

    public UISpriteDraggableDragEvent onBeginDrag = new UISpriteDraggableDragEvent(); //开始滑动事件
    public UISpriteDraggableDragEvent onEndDrag   = new UISpriteDraggableDragEvent(); //结束滑动事件
    public UISpriteDraggableDragEvent onDragging  = new UISpriteDraggableDragEvent(); //滑动中事件

    public void OnBeginDrag(PointerEventData eventData) {
        int inputCount = Input.touchCount;
        if (inputCount > 1) {
            return;
        }

        onBeginDrag?.Invoke(eventData);
    }

    public void OnEndDrag(PointerEventData eventData) {
        int inputCount = Input.touchCount;
        if (inputCount > 1) {
            return;
        }

        onEndDrag?.Invoke(eventData);
    }

    public void OnDrag(PointerEventData eventData) {
        int inputCount = Input.touchCount;
        if (inputCount > 1) {
            return;
        }

        onDragging?.Invoke(eventData);
    }
}

2.轮播组件实现

因为我们项目是手游,从这里开始就写在lua了。
我们也起了个ViewPaper的名字,把UISpriteDraggable传来的参数稍微封装一下,改成了左划成功,优化成功,滑动中,滑动取消,这4个事件。

--------------------------------------------------------------
--       @file  ViewPaper.lua
--      @brief  滑动切换页面
--
--     @author  Shadowrabbit
--
--   @Modified  2020/07/24 10:10
--  @Copyright  Copyright (c) 2020, Shadowrabbit
--============================================================
local assert = assert

local UIDef = class.get("UIDef") --- @type UIDef
local Logger = class.get("Logger") --- @type Logger

--- @class ViewPaper : UIBasePanel
local ViewPaper = assert(self)

ViewPaper.KeepAlive = true -- 对于UILayer表示是否游戏状态切换时需要保留,对于其它panel表示是否要放入缓存
ViewPaper.FullScreen = false -- 是否全屏,比全屏界面层级低的界面需要隐藏
ViewPaper.PanelType = UIDef.PanelType.UIRepeatable -- panel类型
ViewPaper.Reuse = true

ViewPaper.Modules = {}
ViewPaper.Bindings = {}

local kTriggerDistance = 1000

--- @brief 构造函数
--- @protected
function ViewPaper:Initialize()
end

--- @brief 析构函数
--- @protected
function ViewPaper:Finalize()
end

--- @brief 打开时回调
--- @protected
function ViewPaper:OnOpen()
    local compSpriteDraggable = self.transform:GetComponent("UISpriteDraggable")
    compSpriteDraggable.onBeginDrag:AddListener(self:CoSlot(self, self.OnBeginDrag))
    compSpriteDraggable.onEndDrag:AddListener(self:CoSlot(self, self.OnEndDrag))
    compSpriteDraggable.onDragging:AddListener(self:CoSlot(self, self.OnDragging))
end

--- @brief 关闭时回调
--- @protected
function ViewPaper:OnClose()
    local compSpriteDraggable = self.transform:GetComponent("UISpriteDraggable")
    compSpriteDraggable.onBeginDrag:RemoveAllListeners()
    compSpriteDraggable.onEndDrag:RemoveAllListeners()
    compSpriteDraggable.onDragging:RemoveAllListeners()
    self.beginDragPos = nil
    self.currentDragPos = nil
    self.endDragPos = nil
    self.onDragging = nil
    self.onCancelDrag = nil
    self.onDragToLeft = nil
    self.onDragToRight = nil
end

--- @brief 更新时回调
--- @protected
function ViewPaper:OnRefresh()
end

--- @brief 滑动中事件
--- @public
function ViewPaper:SetOnDraggingListener(listener)
    assert(listener, "listener == nil")
    self.onDragging = listener
end

--- @brief 左划事件
--- @public
function ViewPaper:SetOnDragToLeftListener(listener)
    assert(listener, "listener == nil")
    self.onDragToLeft = listener
end

--- @brief 右划事件
--- @public
function ViewPaper:SetOnDragToRightListener(listener)
    assert(listener, "listener == nil")
    self.onDragToRight = listener
end

--- @brief 取消滑动事件
--- @public
function ViewPaper:SetOnCancelDragListener(listener)
    assert(listener, "listener == nil")
    self.onCancelDrag = listener
end

--- @brief
--- @public
function ViewPaper:RemoveOnDraggingListener()
    self.onDragging = nil
end

--- @brief
--- @public
function ViewPaper:RemoveOnDragToLeftListener()
    self.onDragToLeft = nil
end

--- @brief
--- @public
function ViewPaper:RemoveOnDragToRightListener()
    self.onDragToRight = nil
end

--- @brief
--- @public
function ViewPaper:RemoveOnCancelDragListener()
    self.onCancelDrag = nil
end

--- @brief
--- @public
function ViewPaper:RemoveAllListeners()
    self:RemoveOnCancelDragListener()
    self:RemoveOnDraggingListener()
    self:RemoveOnDragToLeftListener()
    self:RemoveOnDragToRightListener()
end

--- @brief 开始滑动
--- @private
--- @param pointerEventData UnityEngine.EventSystems.PointerEventData
function ViewPaper:OnBeginDrag(pointerEventData)
    self.isDragging = true
    self.beginDragPos = pointerEventData.position
end

--- @brief 结束滑动
--- @private
function ViewPaper:OnEndDrag(pointerEventData)
    if not self.isDragging then
        return
    end
    self.isDragging = false
    self.endDragPos = pointerEventData.position
    --进度百分比 正数为右划
    local progressValue = (self.endDragPos.x - self.beginDragPos.x) / kTriggerDistance
    --右划事件
    if self.endDragPos.x - self.beginDragPos.x > kTriggerDistance / 4 then
        if not self.onDragToRight then
            return
        end
        self.onDragToRight(progressValue)
        return
    end
    --左划事件
    if self.beginDragPos.x - self.endDragPos.x > kTriggerDistance / 4 then
        if not self.onDragToLeft then
            return
        end
        self.onDragToLeft(progressValue)
        return
    end
    --取消滑动事件
    if not self.onCancelDrag then
        return
    end
    self.onCancelDrag(progressValue)
end

--- @brief 滑动中
--- @private
function ViewPaper:OnDragging(pointerEventData)
    self.currentDragPos = pointerEventData.position
    --滑动中回调
    if not self.onDragging then
        return
    end
    --进度百分比 正数为右划
    local progressValue = (self.currentDragPos.x - self.beginDragPos.x) / kTriggerDistance
    self.onDragging(progressValue)
end

3.实现类

把对应事件实现一下,panel2就是viewpaper的组件类,一共3个panel,分别是左中右三个页面,我们只对panel2操作,滑动结束后重置动画panel2就回到屏幕中间了。

---@brief 激活
---private
function XXXXX:OnActivate(gameObject)
    self.panel2:SetOnCancelDragListener(self:CoSlot(self, self.OnCancelDrag))
    self.panel2:SetOnDraggingListener(self:CoSlot(self, self.OnDragging))
    self.panel2:SetOnDragToLeftListener(self:CoSlot(self, self.OnDragToLeft))
    self.panel2:SetOnDragToRightListener(self:CoSlot(self, self.OnDragToRight))
end

关闭的时候别忘了取消监听

function XXXXX:OnClose()
    self.panel2:RemoveAllListeners()
end

对应几个事件的实现方法,我们在这里多实现了两个方法CoAutoPlayDraggingAnimationToEnd()和
CoAutoPlayDraggingAnimationToStart(),这两个方法都是跑在协成里的,滑动一半时松手,对应的补充滑动。

--- @private
function XXXXX:OnCancelDrag(progressValue)
    if self.isAutoPlaying then
        return
    end
    self:CoParameterizedSlot(self, self.CoAutoPlayDraggingAnimationToStart, progressValue)()
end

--- @private
function XXXXX:OnDragging(progressValue)
    if self.isAutoPlaying then
        return
    end
    local isDragToRight = progressValue > 0
    --右划没数据了
    if isDragToRight and self:CheckIfOutOfBound(self.currentPage - 1) then
        return
    end
    if not isDragToRight and self:CheckIfOutOfBound(self.currentPage + 1) then
        return
    end
    --修正透明度和缩放 当前有可能正在播放静态动画
    self.panelPageView.localScale = Vector3(1, 1, 1)
    local fixProgressValue = math.abs(progressValue)
    --左右滑动时关闭没有使用的立绘 防止暴力滑动导致左右立绘留在屏幕内
    self.panel1.gameObject:SetActive(progressValue >= 0)
    self.panel3.gameObject:SetActive(progressValue < 0)
    self:SetClip(self.panelPageView, isDragToRight and "youhua" or "zuohua")
    self:SetAnimationProgress(self.panelPageView, fixProgressValue)
end

--- @private
function XXXXX:OnDragToLeft(progressValue)
    if self:CheckIfOutOfBound(self.currentPage + 1) then
        return
    end
    self:CoParameterizedSlot(self, self.CoAutoPlayDraggingAnimationToEnd, progressValue)()
end

--- @private
function XXXXX:OnDragToRight(progressValue)
    if self:CheckIfOutOfBound(self.currentPage - 1) then
        return
    end
    self:CoParameterizedSlot(self, self.CoAutoPlayDraggingAnimationToEnd, progressValue)()
end

--- @brief 检测数据是否越界
--- @private
function XXXXX:CheckIfOutOfBound(chapterId)
    --范围越界
    if chapterId <= 0 then
        return true
    end
    local config = sq.facade:GetConfigManager():TryGetConfig("ChapterConf_Catalog", "ChapterId", chapterId)
    if config == nil then
        return true
    end
    return false
end

--- @brief
--- @private
function XXXXX:CoAutoPlayDraggingAnimationToStart(progressValue)
    self.isAutoPlaying = true
    local isDragToRight = progressValue > 0
    local fixProgressValue = math.abs(progressValue)
    --当前进度没到0% 补充滑动
    while fixProgressValue > 0 do
        --动画20帧 按原速度进度为0.05
        fixProgressValue = Mathf.Clamp01(fixProgressValue - 0.05)
        self:SetClip(self.panelPageView, isDragToRight and "youhua" or "zuohua")
        self:SetAnimationProgress(self.panelPageView, fixProgressValue)
        self:WaitForFrames(1)
    end
    --重置动画还原状态
    self:ResetAnimation(self.panelPageView, true)
    self.isAutoPlaying = false
end

--- @brief
--- @private
function XXXXX:CoAutoPlayDraggingAnimationToEnd(progressValue)
    self.isAutoPlaying = true
    local isDragToRight = progressValue > 0
    local fixProgressValue = math.abs(progressValue)
    --当前进度没到100% 补充滑动
    while fixProgressValue < 1 do
        --动画20帧 按原速度进度为0.05
        fixProgressValue = Mathf.Clamp01(fixProgressValue + 0.05)
        self:SetClip(self.panelPageView, isDragToRight and "youhua" or "zuohua")
        self:SetAnimationProgress(self.panelPageView, fixProgressValue)
        self:WaitForFrames(1)
    end
    --重置动画还原状态
    self:ResetAnimation(self.panelPageView, true)
    --刷新滑动后的数据
    self.currentPage = isDragToRight and self.currentPage - 1 or self.currentPage + 1
    self:RefreshView()
    self.isAutoPlaying = false
end

4.动画控制

原理是把动画的播放速度设置成0,完全移交给程序来控制进度。程序这边通过协成在每一帧改变动画的进度

--- @brief 设置动画进度
--- @public
function AnimationUtils.SetProgress(transform, progressValue)
    --- @type UnityEngine.Animation
    local compAni = transform:GetComponent("Animation")
    compAni:Play()
    --- @type UnityEngine.AnimationState get_Item为C#的this[]用法
    local state = compAni:get_Item(compAni.clip.name)
    state.normalizedTime = progressValue
    state.speed = 0
end

--- @brief 设置动画片段
--- @public
function AnimationUtils.SetClip(transform, name)
    --- @type UnityEngine.Animation
    local compAni = transform:GetComponent("Animation")
    compAni.clip = compAni:GetClip(name) --设置当前片段
end