[SwiftUI 100 天] 用 DragGesture 和 offset() 来移动视图

2020/7/28 23:04:12

本文主要是介绍[SwiftUI 100 天] 用 DragGesture 和 offset() 来移动视图,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

译自 www.hackingwithswift.com/books/ios-s…

更多内容,欢迎关注公众号 「Swift花园」

喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

用 DragGesture 和 offset() 来移动视图

SwiftUI 允许我们添加手势给任意视图,然后利用手势产生的数值来操作视图。为了说明这一点,我们要添加一个 DragGestureCardView,以便我们能移动卡片。我们还要使用手势产生的数值来控制视图的透明度和旋转角度 —— 当卡片被拖动后,它会旋转出画面, 同时渐隐消失。实现这些效果只需要很少的代码,因为大部分工作是 SwiftUI 替我们完成的,相信你一定会为之惊艳!

首先,添加下面这个新的 @State 属性到 CardView,用以跟踪用户拖拽的距离:

@State private var offset = CGSize.zero
复制代码

接下来我们要添加三个 modifier 到 CardView,直接放在 frame() modifier 之下。记住:你应用 modifier 的顺序是很重要的,对于这里的 offset 和 rotation 尤其关键。

如果我们先旋转再偏移,那么偏移将会基于视图旋转后的坐标轴。例如,如果我们往左移动某样东西 100 像素,然后旋转 90 度,我们将得到左偏移 100 像素加上 90 度的旋转角度。但如果我们先旋转 90 度再向左移动 100 像素,我们将得到某个旋转了 90 度,并且往下移动了 100 像素的东西,因为这里的 “左” 方向已经被旋转了。

当你把 SwiftUI 通过包装 modifier 会创建出新视图这一机制考虑在内时,事情就变得更为复杂。对于移动和旋转,当我们希望视图向正西方向(无视旋转)的滑动的同时旋转自身,我们需要先添加 rotation,然后再添加 offset !(听起来有点反直觉是不是?)

现在,offset.width 会包含我们拖拽卡片的距离,但我们不会把这个值直接作为旋转的角度值使用。因为那样的话卡片会非常快的旋转,所以我们采用它的 1/5:

.rotationEffect(.degrees(Double(offset.width / 5)))
复制代码

接下来是应用移动,卡片会相对于拖拽数值做水平方向上的移动。再一次,我们不会使用原始的offset.width,因为那会要求用户拖拽较长的距离才能得到有意义的结果,所以我们采用它的 5 倍数值,通过小手势就能轻扫卡片。

把下面这个 modifier 添加到前面那个之后:

.offset(x: offset.width * 5, y: 0)
复制代码

进行到这里,我们要基于拖拽手势再添加一个 modifier:我们要让卡片随着自身被拖远时淡出。

这里的计算需要一点思考,工作方式如下:

  • 我们要使用拖拽数值的 1/50,以便卡片不会太快隐去。
  • 在改变卡片透明度时,我们并不关心他们是被移到左边(负值)还是被移到右边(正值),因此我们会把数值放进 abs() 函数。
  • 然后我们用 2 减去这个绝对值。

这里采用 2 是有意的,因为这样可以确保卡片在被拖拽不远时保持透明度。注意,大于 1.0 的透明度都等同于 1.0。如果用户向左或者向右拖动 50 个点,50 除以 50 得到 1,2 减去 1 得到的透明度还是 1 —— 卡片仍然是完全不透明的。但是当移动距离超出 50 个点时,卡片就开始渐隐,直到 100 个点时完全隐去。

把这个 modifier 添加到前面两个之后:

.opacity(2 - Double(abs(offset.width / 50)))
复制代码

现在,我们已经创建了一个属性用于存储拖拽数值,并且添加了三个 modifier 来改变视图的渲染方式。接下来是最重要的部分:我们需要添加一个 DragGesture 给我们的卡片,以便用户拖拽卡片时能更新 offset 数值。 拖拽手势自身有两个有用的 modifier,可以让我们在手势变化时触发函数(每当手指移动时就触发),以及在手势结束时触发函数(手指离开屏幕)。

这两个函数都会抛出当前的手势状态,以便我们拿来评估。在我们例子中,我们需要读取 translation 属性,用它来设置我们的 offset 属性,同时你也可以读取开始位置,预测的结束位置,等等。而对于 ended 函数,我们会检查用户是否移动了超过 100 个点,以便我们决定是否移除卡片。如果没有,我们会把 offset 设置回 0。

把下面这个 gesture() modifier 添加到前面三个之后:

.gesture(
    DragGesture()
        .onChanged { gesture in
            self.offset = gesture.translation
        }

        .onEnded { _ in
            if abs(self.offset.width) > 100 {
                // 移除卡片
            } else {
                self.offset = .zero
            }
        }
)
复制代码

运行应用:你应该能看到卡片会随着拖拽手势移动,旋转和渐隐,当你拖拽到某个距离时,卡片就保持在被拖到的位置,不会跳回原始位置。

要完成最后一步我们需要把 // 移除卡片 注释替换为实际的实现:把卡片从父视图中真正移除。现在的问题是,我们不希望让 CardView 去向上直接要求 ContentView 维护数据,因为这样会带来难以维护的套娃程序。相反,更好的解决方案是在 CardView 中存储一个闭包,可以稍后添加任意代码 —— 这意味着,我们拥有了从ContentView 获得回调的灵活性,同时不必显性地把两个视图耦合在一起。

把下面这个新属性添加到 CardViewcard 属性下方:

var removal: (() -> Void)? = nil
复制代码

如你所见,这个闭包不接收参数,也不返回任何东西,并且默认为 nil,所以除非显式需求,我们可以不用给它赋值。

现在可以把 // 移除卡片 注释替换为对移除闭包的调用:

self.removal?()
复制代码

提示: 问号表明这个闭包只有在非空的时候才能被调用。

回到 ContentView,我们现在可以写一个专门处理卡片移除的方法,然后将它连接到前面的闭包:

func removeCard(at index: Int) {
    cards.remove(at: index)
}
复制代码

我们还需要在 CardView 的创建代码处利用拖尾闭包给卡片添加移除的处理代码,我们会用 withAnimation() 调用包装移除的代码,这样卡片就能平滑地滑出。

代码如下:

ForEach(0..<cards.count, id: \.self) { index in
    CardView(card: self.cards[index]) {
       withAnimation {
           self.removeCard(at: index)
       }
    }
    .stacked(at: index, in: self.cards.count)
}
复制代码

再次运行应用 —— 效果还不赖,你可以轻扫卡片栈里的卡片,直到它们滑出屏幕。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~

Swift花园微信公众号

这篇关于[SwiftUI 100 天] 用 DragGesture 和 offset() 来移动视图的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程