mirror of
https://github.com/zhigang1992/GitHawk.git
synced 2026-05-28 06:36:05 +08:00
268 lines
10 KiB
Swift
Executable File
268 lines
10 KiB
Swift
Executable File
//
|
|
// SwipeActionsView.swift
|
|
//
|
|
// Created by Jeremy Koch
|
|
// Copyright © 2017 Jeremy Koch. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
protocol SwipeActionsViewDelegate: class {
|
|
func swipeActionsView(_ swipeActionsView: SwipeActionsView, didSelect action: SwipeAction)
|
|
}
|
|
|
|
class SwipeActionsView: UIView {
|
|
weak var delegate: SwipeActionsViewDelegate?
|
|
|
|
let transitionLayout: SwipeTransitionLayout
|
|
var layoutContext: ActionsViewLayoutContext
|
|
|
|
var feedbackGenerator: SwipeFeedback
|
|
|
|
var expansionAnimator: SwipeAnimator?
|
|
|
|
var expansionDelegate: SwipeExpanding? {
|
|
return options.expansionDelegate ?? (expandableAction?.hasBackgroundColor == false ? ScaleAndAlphaExpansion.default : nil)
|
|
}
|
|
|
|
let orientation: SwipeActionsOrientation
|
|
let actions: [SwipeAction]
|
|
let options: SwipeTableOptions
|
|
|
|
var buttons: [SwipeActionButton] = []
|
|
|
|
var minimumButtonWidth: CGFloat = 0
|
|
var maximumImageHeight: CGFloat {
|
|
return actions.reduce(0, { initial, next in max(initial, next.image?.size.height ?? 0) })
|
|
}
|
|
|
|
var visibleWidth: CGFloat = 0 {
|
|
didSet {
|
|
let preLayoutVisibleWidths = transitionLayout.visibleWidthsForViews(with: layoutContext)
|
|
|
|
layoutContext = ActionsViewLayoutContext.newContext(for: self)
|
|
|
|
transitionLayout.container(view: self, didChangeVisibleWidthWithContext: layoutContext)
|
|
|
|
setNeedsLayout()
|
|
layoutIfNeeded()
|
|
|
|
notifyVisibleWidthChanged(oldWidths: preLayoutVisibleWidths,
|
|
newWidths: transitionLayout.visibleWidthsForViews(with: layoutContext))
|
|
}
|
|
}
|
|
|
|
var preferredWidth: CGFloat {
|
|
return minimumButtonWidth * CGFloat(actions.count)
|
|
}
|
|
|
|
var contentSize: CGSize {
|
|
if options.expansionStyle?.elasticOverscroll != true || visibleWidth < preferredWidth {
|
|
return CGSize(width: visibleWidth, height: bounds.height)
|
|
} else {
|
|
let scrollRatio = max(0, visibleWidth - preferredWidth)
|
|
return CGSize(width: preferredWidth + (scrollRatio * 0.25), height: bounds.height)
|
|
}
|
|
}
|
|
|
|
private(set) var expanded: Bool = false
|
|
|
|
var expandableAction: SwipeAction? {
|
|
return options.expansionStyle != nil ? actions.last : nil
|
|
}
|
|
|
|
init(maxSize: CGSize, options: SwipeTableOptions, orientation: SwipeActionsOrientation, actions: [SwipeAction]) {
|
|
self.options = options
|
|
self.orientation = orientation
|
|
self.actions = actions.reversed()
|
|
|
|
switch options.transitionStyle {
|
|
case .border:
|
|
transitionLayout = BorderTransitionLayout()
|
|
case .reveal:
|
|
transitionLayout = RevealTransitionLayout()
|
|
default:
|
|
transitionLayout = DragTransitionLayout()
|
|
}
|
|
|
|
self.layoutContext = ActionsViewLayoutContext(numberOfActions: actions.count, orientation: orientation)
|
|
|
|
feedbackGenerator = SwipeFeedback(style: .light)
|
|
feedbackGenerator.prepare()
|
|
|
|
super.init(frame: .zero)
|
|
|
|
clipsToBounds = true
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
backgroundColor = options.backgroundColor ?? #colorLiteral(red: 0.862745098, green: 0.862745098, blue: 0.862745098, alpha: 1)
|
|
|
|
buttons = addButtons(for: self.actions, withMaximum: maxSize)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func addButtons(for actions: [SwipeAction], withMaximum size: CGSize) -> [SwipeActionButton] {
|
|
let buttons: [SwipeActionButton] = actions.map({ action in
|
|
let actionButton = SwipeActionButton(action: action)
|
|
actionButton.addTarget(self, action: #selector(actionTapped(button:)), for: .touchUpInside)
|
|
actionButton.autoresizingMask = [.flexibleHeight, orientation == .right ? .flexibleRightMargin : .flexibleLeftMargin]
|
|
actionButton.spacing = options.buttonSpacing ?? 8
|
|
actionButton.contentEdgeInsets = buttonEdgeInsets(fromOptions: options)
|
|
return actionButton
|
|
})
|
|
|
|
let maximum = options.maximumButtonWidth ?? (size.width - 30) / CGFloat(actions.count)
|
|
minimumButtonWidth = buttons.reduce(options.minimumButtonWidth ?? 74, { initial, next in max(initial, next.preferredWidth(maximum: maximum)) })
|
|
|
|
buttons.enumerated().forEach { (index, button) in
|
|
let action = actions[index]
|
|
let frame = CGRect(origin: .zero, size: CGSize(width: bounds.width, height: bounds.height))
|
|
let wrapperView = SwipeActionButtonWrapperView(frame: frame, action: action, orientation: orientation, contentWidth: minimumButtonWidth)
|
|
wrapperView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
|
|
wrapperView.addSubview(button)
|
|
|
|
if let effect = action.backgroundEffect {
|
|
let effectView = UIVisualEffectView(effect: effect)
|
|
effectView.frame = wrapperView.frame
|
|
effectView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
|
|
effectView.contentView.addSubview(wrapperView)
|
|
addSubview(effectView)
|
|
} else {
|
|
addSubview(wrapperView)
|
|
}
|
|
|
|
button.frame = wrapperView.contentRect
|
|
button.maximumImageHeight = maximumImageHeight
|
|
button.verticalAlignment = options.buttonVerticalAlignment
|
|
button.shouldHighlight = action.hasBackgroundColor
|
|
}
|
|
|
|
return buttons
|
|
}
|
|
|
|
func actionTapped(button: SwipeActionButton) {
|
|
guard let index = buttons.index(of: button) else { return }
|
|
|
|
delegate?.swipeActionsView(self, didSelect: actions[index])
|
|
}
|
|
|
|
func buttonEdgeInsets(fromOptions options: SwipeTableOptions) -> UIEdgeInsets {
|
|
let padding = options.buttonPadding ?? 8
|
|
return UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
|
|
}
|
|
|
|
func setExpanded(expanded: Bool, feedback: Bool = false) {
|
|
guard self.expanded != expanded else { return }
|
|
|
|
self.expanded = expanded
|
|
|
|
if feedback {
|
|
feedbackGenerator.impactOccurred()
|
|
feedbackGenerator.prepare()
|
|
}
|
|
|
|
let timingParameters = expansionDelegate?.animationTimingParameters(buttons: buttons.reversed(), expanding: expanded)
|
|
|
|
if expansionAnimator?.isRunning == true {
|
|
expansionAnimator?.stopAnimation(true)
|
|
}
|
|
|
|
if #available(iOS 10, *) {
|
|
expansionAnimator = UIViewPropertyAnimator(duration: timingParameters?.duration ?? 0.6, dampingRatio: 1.0)
|
|
} else {
|
|
expansionAnimator = UIViewSpringAnimator(duration: timingParameters?.duration ?? 0.6,
|
|
damping: 1.0,
|
|
initialVelocity: 1.0)
|
|
}
|
|
|
|
expansionAnimator?.addAnimations {
|
|
self.setNeedsLayout()
|
|
self.layoutIfNeeded()
|
|
}
|
|
|
|
expansionAnimator?.startAnimation(afterDelay: timingParameters?.delay ?? 0)
|
|
|
|
notifyExpansion(expanded: expanded)
|
|
}
|
|
|
|
func notifyVisibleWidthChanged(oldWidths: [CGFloat], newWidths: [CGFloat]) {
|
|
DispatchQueue.main.async {
|
|
oldWidths.enumerated().forEach { index, oldWidth in
|
|
let newWidth = newWidths[index]
|
|
|
|
if oldWidth != newWidth {
|
|
let context = SwipeActionTransitioningContext(actionIdentifier: self.actions[index].identifier,
|
|
button: self.buttons[index],
|
|
newPercentVisible: newWidth / self.minimumButtonWidth,
|
|
oldPercentVisible: oldWidth / self.minimumButtonWidth,
|
|
wrapperView: self.subviews[index])
|
|
|
|
self.actions[index].transitionDelegate?.didTransition(with: context)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func notifyExpansion(expanded: Bool) {
|
|
guard let expandedButton = buttons.last else { return }
|
|
|
|
expansionDelegate?.actionButton(expandedButton, didChange: expanded, otherActionButtons: buttons.dropLast().reversed())
|
|
}
|
|
|
|
func createDeletionMask() -> UIView {
|
|
let mask = UIView(frame: CGRect(x: min(0, frame.minX), y: 0, width: bounds.width * 2, height: bounds.height))
|
|
mask.backgroundColor = UIColor.white
|
|
return mask
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
for subview in subviews.enumerated() {
|
|
transitionLayout.layout(view: subview.element, atIndex: subview.offset, with: layoutContext)
|
|
}
|
|
|
|
if expanded {
|
|
subviews.last?.frame.origin.x = 0 + bounds.origin.x
|
|
}
|
|
}
|
|
}
|
|
|
|
class SwipeActionButtonWrapperView: UIView {
|
|
let contentRect: CGRect
|
|
|
|
init(frame: CGRect, action: SwipeAction, orientation: SwipeActionsOrientation, contentWidth: CGFloat) {
|
|
switch orientation {
|
|
case .left:
|
|
contentRect = CGRect(x: frame.width - contentWidth, y: 0, width: contentWidth, height: frame.height)
|
|
case .right:
|
|
contentRect = CGRect(x: 0, y: 0, width: contentWidth, height: frame.height)
|
|
}
|
|
|
|
super.init(frame: frame)
|
|
|
|
configureBackgroundColor(with: action)
|
|
}
|
|
|
|
func configureBackgroundColor(with action: SwipeAction) {
|
|
guard action.hasBackgroundColor else { return }
|
|
|
|
if let backgroundColor = action.backgroundColor {
|
|
self.backgroundColor = backgroundColor
|
|
} else {
|
|
switch action.style {
|
|
case .destructive:
|
|
backgroundColor = #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1)
|
|
default:
|
|
backgroundColor = #colorLiteral(red: 0.862745098, green: 0.862745098, blue: 0.862745098, alpha: 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|