DrawerKit
什么是 DrawerKit?
DrawerKit 是一个自定义视图控制器展示,模仿您在 Apple Maps 应用中看到的行为。它允许任何视图控制器模态地展示另一个视图控制器,以便展示的内容最初只部分显示,然后允许用户通过逐渐显示或隐藏该内容与之一交互,直到它完全展示或撤销。它目前并不是 Maps 应用中看到的行为的完整实现,仅仅因为我们的特定的需求要求如此。我们打算继续努力解决这个限制。
请玩一玩演示应用并尝试不同的配置选项,因为 DrawerKit
有许多配置方式,下面的 gif 图像最多只能反映出库所能做的事情中的一小部分。
需要或支持哪些版本的 iOS?
DrawerKit 与 iOS 10 及以上版本兼容。
如何使用它?
为了使 展示 视图控制器将另一个视图控制器(要展示的 视图控制器)作为抽屉来展示,需要某个对象遵守 DrawerCoordinating
协议,而 要展示的 视图控制器也需要遵守 DrawerPresentable
协议。展示视图控制器可能是遵守 DrawerCoordinating
协议的对象,但不必一定是。
public protocol DrawerCoordinating: class {
/// An object vended by the conforming object, whose responsibility is to control
/// the presentation, animation, and interactivity of/with the drawer.
var drawerDisplayController: DrawerDisplayController? { get }
}
public protocol DrawerPresentable: class {
/// The height at which the drawer must be presented when it's in its
/// partially expanded state. If negative, its value is clamped to zero.
var heightOfPartiallyExpandedDrawer: CGFloat { get }
/// The height at which the drawer must be presented when it's in its
/// collapsed state. If negative, its value is clamped to zero.
/// Default implementation returns 0.
var heightOfCollapsedDrawer: CGFloat { get }
}
之后,在以模态方式展示视图控制器方面,基本上是常态。下面是让一个视图控制器以抽屉方式展示另一个视图控制器的基本代码,其中展示视图控制器本身遵守 DrawerCoordinating
,
extension PresenterViewController {
func doModalPresentation() {
guard let vc = storyboard?.instantiateViewController(withIdentifier: "presented")
as? PresentedViewController else { return }
// you can provide the configuration values in the initialiser...
var configuration = DrawerConfiguration(/* ..., ..., ..., */)
// ... or after initialisation. All of these have default values so change only
// what you need to configure differently. They're all listed here just so you
// can see what can be configured. The values listed are the default ones,
// except where indicated otherwise.
configuration.totalDurationInSeconds = 3 // default is 0.4
configuration.durationIsProportionalToDistanceTraveled = false
// default is UISpringTimingParameters()
configuration.timingCurveProvider = UISpringTimingParameters(dampingRatio: 0.8)
configuration.fullExpansionBehaviour = .leavesCustomGap(gap: 100) // default is .coversFullScreen
configuration.supportsPartialExpansion = true
configuration.dismissesInStages = true
configuration.isDrawerDraggable = true
configuration.isFullyPresentableByDrawerTaps = true
configuration.numberOfTapsForFullDrawerPresentation = 1
configuration.isDismissableByOutsideDrawerTaps = true
configuration.numberOfTapsForOutsideDrawerDismissal = 1
configuration.flickSpeedThreshold = 3
configuration.upperMarkGap = 100 // default is 40
configuration.lowerMarkGap = 80 // default is 40
configuration.maximumCornerRadius = 15
var handleViewConfiguration = HandleViewConfiguration()
handleViewConfiguration.autoAnimatesDimming = true
handleViewConfiguration.backgroundColor = .gray
handleViewConfiguration.size = CGSize(width: 40, height: 6)
handleViewConfiguration.top = 8
handleViewConfiguration.cornerRadius = .automatic
configuration.handleViewConfiguration = handleViewConfiguration
let borderColor = UIColor(red: 205.0/255.0, green: 206.0/255.0, blue: 210.0/255.0, alpha: 1)
let drawerBorderConfiguration = DrawerBorderConfiguration(borderThickness: 0.5,
borderColor: borderColor)
configuration.drawerBorderConfiguration = drawerBorderConfiguration
let drawerShadowConfiguration = DrawerShadowConfiguration(shadowOpacity: 0.25,
shadowRadius: 4,
shadowOffset: .zero,
shadowColor: .black)
configuration.drawerShadowConfiguration = drawerShadowConfiguration
drawerDisplayController = DrawerDisplayController(presentingViewController: self,
presentedViewController: vc,
configuration: configuration,
inDebugMode: true)
present(vc, animated: true)
}
}
以下是实现对应的要展示视图控制器的一种方式
extension PresentedViewController: DrawerPresentable {
var heightOfPartiallyExpandedDrawer: CGFloat {
guard let view = self.view as? PresentedView else { return 0 }
return view.dividerView.frame.origin.y
}
}
自然地,要展示的视图控制器可以在任何时候,按照常规方法关闭自己。
extension PresentedViewController {
@IBAction func dismissButtonTapped() {
dismiss(animated: true)
}
}
它有多可配置?
DrawerKit 有许多可配置的属性,它们被方便地收集到一个结构体中,即 DrawerConfiguration
。以下是所有当前支持的配置选项的列表
/// Intial state of presented drawer. Default is `nil`, If `nil` then
/// state will be computed based on `supportsPartialExpansion` flag.
public var initialState: DrawerState?
/// The total duration, in seconds, for the drawer to transition from its
/// dismissed state to its fully-expanded state, or vice-versa. The default
/// value is 0.4 seconds.
public var totalDurationInSeconds: TimeInterval
/// When the drawer transitions between its dismissed and partially-expanded
/// states, or between its partially-expanded and its fully-expanded states, in
/// either direction, the distance traveled by the drawer is some fraction of
/// the total distance traveled between the dismissed and fully-expanded states.
/// You have a choice between having those fractional transitions take the same
/// amount of time as the full transition, and having them take a time that is
/// a fraction of the total time, where the fraction used is the fraction of
/// space those partial transitions travel. In the first case, all transitions
/// have the same duration (`totalDurationInSeconds`) but different speeds, while
/// in the second case different transitions have different durations but the same
/// speed. The default is `false`, that is, all transitions last the same amount
/// of time.
public var durationIsProportionalToDistanceTraveled: Bool
/// The type of timing curve to use for the animations. The full set of cubic
/// Bezier curves and spring-based curves is supported. Note that selecting a
/// spring-based timing curve may cause the `totalDurationInSeconds` parameter
/// to be ignored because the duration, for a fully general spring-based timing
/// curve provider, is computed based on the specifics of the spring-based curve.
/// The default is `UISpringTimingParameters()`, which is the system's global
/// spring-based timing curve.
public var timingCurveProvider: UITimingCurveProvider
/// Whether the drawer expands to cover the entire screen, the entire screen minus
/// the status bar, or the entire screen minus a custom gap. The default is to cover
/// the full screen.
public var fullExpansionBehaviour: FullExpansionBehaviour
/// When `true`, the drawer is presented first in its partially expanded state.
/// When `false`, the presentation is always to full screen and there is no
/// partially expanded state. The default value is `true`.
public var supportsPartialExpansion: Bool
/// When `true`, dismissing the drawer from its fully expanded state can result
/// in the drawer stopping at its partially expanded state. When `false`, the
/// dismissal is always straight to the dismissed state. Note that
/// `supportsPartialExpansion` being `false` implies `dismissesInStages` being
/// `false` as well but you can have `supportsPartialExpansion == true` and
/// `dismissesInStages == false`, which would result in presentations to the
/// partially expanded state but all dismissals would be straight to the dismissed
/// state. The default value is `true`.
public var dismissesInStages: Bool
/// Whether or not the drawer can be dragged up and down. The default value is `true`.
public var isDrawerDraggable: Bool
/// Whether or not the drawer can be fully presentable by tapping on it.
/// The default value is `true`.
public var isFullyPresentableByDrawerTaps: Bool
/// How many taps are required for fully presenting the drawer by tapping on it.
/// The default value is 1.
public var numberOfTapsForFullDrawerPresentation: Int
/// Whether or not the drawer can be dismissed by tapping anywhere outside of it.
/// The default value is `true`.
public var isDismissableByOutsideDrawerTaps: Bool
/// How many taps are required for dismissing the drawer by tapping outside of it.
/// The default value is 1.
public var numberOfTapsForOutsideDrawerDismissal: Int
/// How fast one needs to "flick" the drawer up or down to make it ignore the
/// partially expanded state. Flicking fast enough up always presents to full screen
/// and flicking fast enough down always collapses the drawer. A typically good value
/// is around 3 points per screen height per second, and that is also the default
/// value of this property.
public var flickSpeedThreshold: CGFloat
/// There is a band around the partially expanded position of the drawer where
/// ending a drag inside will cause the drawer to move back to the partially
/// expanded position (subjected to the conditions set by `supportsPartialExpansion`
/// and `dismissesInStages`, of course). Set `inDebugMode` to `true` to see lines
/// drawn at those positions. This value represents the gap *above* the partially
/// expanded position. The default value is 40 points.
public var upperMarkGap: CGFloat
/// There is a band around the partially expanded position of the drawer where
/// ending a drag inside will cause the drawer to move back to the partially
/// expanded position (subjected to the conditions set by `supportsPartialExpansion`
/// and `dismissesInStages`, of course). Set `inDebugMode` to `true` to see lines
/// drawn at those positions. This value represents the gap *below* the partially
/// expanded position. The default value is 40 points.
public var lowerMarkGap: CGFloat
/// The animating drawer also animates the radius of its top left and top right
/// corners, from 0 to the value of this property. Setting this to 0 prevents any
/// corner animations from taking place. The default value is 15 points.
public var maximumCornerRadius: CGFloat
/// How the drawer should animate its corner radius if specified. The
/// default value is `maximumAtPartialY`.
public var cornerAnimationOption: CornerAnimationOption
/// The configuration options for the handle view, should it be shown. Set this
/// property to `nil` to hide the handle view. The default value is
/// `HandleViewConfiguration()`.
public var handleViewConfiguration: HandleViewConfiguration?
/// The configuration options for the drawer's border, should it be shown. Set this
/// property to `nil` so as not to have a drawer border. The default value is `nil`.
public var drawerBorderConfiguration: DrawerBorderConfiguration?
/// The configuration options for the drawer's shadow, should it be shown. Set this
/// property to `nil` so as not to have a drawer shadow. The default value is `nil`.
public var drawerShadowConfiguration: DrawerShadowConfiguration?
/// In what states touches should be passed through to the presenting view.
/// By default touches will not be passed through only in `fullyExpanded` state.
public var passthroughTouchesInStates: PassthroughOptions
public enum FullExpansionBehaviour: Equatable {
case coversFullScreen
case dosNotCoverStatusBar
case leavesCustomGap(gap: CGFloat)
}
public struct HandleViewConfiguration {
/// Whether or not to automatically dim the handle view as the drawer approaches
/// its collapsed or fully expanded states. The default is `true`. Set it to `false`
/// when configuring the drawer not to cover the full screen so that the handle view
/// is always visible in that case.
public var autoAnimatesDimming: Bool
/// The handle view's background color. The default value is `UIColor.gray`.
public var backgroundColor: UIColor
/// The handle view's bounding rectangle's size. The default value is
/// `CGSize(width: 40, height: 6)`.
public var size: CGSize
/// The handle view's vertical distance from the top of the drawer. In other words,
/// the constant to be used when setting up the layout constraint
/// `handleView.topAnchor.constraint(equalTo: presentedView.topAnchor, constant: top)`
/// The default value is 8 points.
public var top: CGFloat
/// The handle view's corner radius. The default is `CornerRadius.automatic`, which
/// results in a corner radius equal to half the handle view's height.
public var cornerRadius: CornerRadius
}
public struct DrawerBorderConfiguration {
/// The drawer's layer’s border thickness. The default value is 0,
/// so effectively the default is not to have any border at all.
public let borderThickness: CGFloat
/// The drawer's layer’s border's color. The default value is `nil`, so
/// effectively the default is not to have any border at all.
public let borderColor: UIColor?
public init(borderThickness: CGFloat = 0, borderColor: UIColor? = nil)
}
public struct DrawerShadowConfiguration {
/// The drawer's layer’s shadow's opacity. The default value is 0, so
/// effectively the default is not to have any shadow at all.
public let shadowOpacity: CGFloat
/// The blur radius (in points) used to render the drawer's layer’s shadow.
/// The default value is 0, so effectively the default is not to have any
/// shadow at all.
public let shadowRadius: CGFloat
/// The offset (in points) of the drawer's layer’s shadow. The default value is
/// `CGSize.zero`, so effectively the default is not to have any shadow at all.
public let shadowOffset: CGSize
/// The drawer's layer’s shadow's color. The default value is `nil`, so
/// effectively the default is not to have any shadow at all.
public let shadowColor: UIColor?
public init(shadowOpacity: CGFloat = 0,
shadowRadius: CGFloat = 0,
shadowOffset: CGSize = .zero,
shadowColor: UIColor? = nil)
}
实际的抽屉行为逻辑是什么?
抽屉如何以及在什么情况下完全展示、部分展示或折叠(关闭)的行为,可以用以下伪代码总结
if isMovingUpQuickly { show fully expanded }
if isMovingDownQuickly { collapse all the way (ie, dismiss) }
if isAboveUpperMark {
if isMovingUp || isNotMoving {
show fully expanded
} else { // is moving down
collapse to the partially expanded state or all the way (ie, dismiss),
depending on the values of `supportsPartialExpansion` and `dismissesInStages`
}
}
if isAboveLowerMark { // ie, in the band surrounding the partially expanded state
if isMovingDown {
collapse all the way (ie, dismiss)
} else { // not moving or moving up
expand to the partially expanded state or all the way (ie, full-screen),
depending on the value of `supportsPartialExpansion`
}
}
// below the band surrounding the partially expanded state
collapse all the way (ie, dismiss)
Carthage
如果你使用 Carthage 来管理你的依赖,只需将 DrawerKit 添加到你的 Cartfile
github "Babylonpartners/DrawerKit" ~> 1.0
如果你使用 Carthage 来构建你的依赖,请确保你已经将 DrawerKit.framework
添加到目标的 "链接的框架和库" 部分,并且在 Carthage 框架复制构建阶段中包含它们。
CocoaPods
如果您使用 CocoaPods 来管理依赖,只需将 DrawerKit 添加到您的 Podfile
pod 'DrawerKit', '~> 1.0'