[vNext] AvatarGroup: Creating smoother animations (#648)

This commit is contained in:
Sophia Lee 2021-12-17 08:48:44 -08:00 коммит произвёл GitHub
Родитель 93c46c0c35
Коммит 113cb1f8c3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 93 добавлений и 44 удалений

Просмотреть файл

@ -138,7 +138,8 @@ class AvatarGroupDemoController: DemoTableViewController {
NSLayoutConstraint.activate([
cell.contentView.leadingAnchor.constraint(equalTo: avatarGroupView.leadingAnchor, constant: -20),
cell.contentView.topAnchor.constraint(equalTo: avatarGroupView.topAnchor, constant: -15),
cell.contentView.bottomAnchor.constraint(equalTo: avatarGroupView.bottomAnchor, constant: 15)
cell.contentView.bottomAnchor.constraint(equalTo: avatarGroupView.bottomAnchor, constant: 15),
cell.contentView.trailingAnchor.constraint(equalTo: avatarGroupView.trailingAnchor, constant: 20)
])
cell.backgroundColor = self.isUsingAlternateBackgroundColor ? Colors.tableCellBackgroundSelected : Colors.tableCellBackground
@ -359,7 +360,7 @@ class AvatarGroupDemoController: DemoTableViewController {
}
}
private var maxDisplayedAvatars: Int = 4 {
private var maxDisplayedAvatars: Int = 3 {
didSet {
if oldValue != maxDisplayedAvatars {
maxAvatarsTextField.text = "\(maxDisplayedAvatars)"
@ -439,8 +440,11 @@ class AvatarGroupDemoController: DemoTableViewController {
return textField
}()
private var avatarCount: Int = 5 {
private var avatarCount: Int = 4 {
didSet {
guard oldValue != avatarCount && avatarCount >= 0 else {
return
}
AvatarGroupDemoSection.allCases.filter({ section in
return section.isDemoSection
}).forEach { section in
@ -475,6 +479,10 @@ class AvatarGroupDemoController: DemoTableViewController {
}
@objc private func subtractAvatarCount(_ cell: ActionsCell) {
guard avatarCount > 0 else {
return
}
avatarCount -= 1
}

Просмотреть файл

@ -176,7 +176,7 @@ public struct Avatar: View {
let ringOuterGap: CGFloat = isRingVisible ? tokens.ringOuterGap : 0
let avatarImageSize: CGFloat = tokens.avatarSize!
let ringInnerGapSize: CGFloat = avatarImageSize + (ringInnerGap * 2)
let ringSize: CGFloat = ringInnerGapSize + ( ringThickness * 2)
let ringSize: CGFloat = ringInnerGapSize + (ringThickness * 2)
let ringOuterGapSize: CGFloat = ringSize + (ringOuterGap * 2)
let presenceIconSize: CGFloat = tokens.presenceIconSize!
let presenceIconOutlineSize: CGFloat = presenceIconSize + (tokens.presenceIconOutlineThickness * 2)
@ -355,6 +355,17 @@ public struct Avatar: View {
var yOrigin: CGFloat
var cutoutSize: CGFloat
public var animatableData: AnimatablePair<AnimatablePair<CGFloat, CGFloat>, CGFloat> {
get {
AnimatablePair(AnimatablePair(xOrigin, yOrigin), cutoutSize)
}
set {
xOrigin = newValue.first.first
yOrigin = newValue.first.second
cutoutSize = newValue.second
}
}
public func path(in rect: CGRect) -> Path {
var cutoutFrame = Rectangle().path(in: rect)
cutoutFrame.addPath(Circle().path(in: CGRect(x: xOrigin,
@ -467,7 +478,9 @@ public struct Avatar: View {
}
/// Properties available to customize the state of the avatar
class MSFAvatarStateImpl: NSObject, ObservableObject, MSFAvatarState {
class MSFAvatarStateImpl: NSObject, ObservableObject, Identifiable, MSFAvatarState {
public var id = UUID()
@Published var backgroundColor: UIColor?
@Published var foregroundColor: UIColor?
@Published var hasButtonAccessibilityTrait: Bool = false
@ -518,7 +531,7 @@ class MSFAvatarStateImpl: NSObject, ObservableObject, MSFAvatarState {
return avatarImageSize + (ringOuterGap * 2)
} else {
let ringThickness: CGFloat = isRingVisible ? tokens.ringThickness : 0
let ringInnerGap: CGFloat = isRingVisible ? tokens.ringInnerGap : 0
let ringInnerGap: CGFloat = isRingVisible && hasRingInnerGap ? tokens.ringInnerGap : 0
return ((ringInnerGap + ringThickness + ringOuterGap) * 2 + avatarImageSize)
}
}

Просмотреть файл

@ -157,7 +157,7 @@ public struct AvatarGroup: View {
self.tokens = state.tokens
}
/// Renders the avatar with an optional cutout
/// Renders the avatar with an optional cutout for the Stack group style.
@ViewBuilder
private func avatarCutout(_ avatar: Avatar,
_ needsCutout: Bool,
@ -177,6 +177,7 @@ public struct AvatarGroup: View {
public var body: some View {
let avatars: [MSFAvatarStateImpl] = state.avatars
let avatarViews: [Avatar] = avatars.map { Avatar($0) }
let enumeratedAvatars = Array(avatars.enumerated())
let maxDisplayedAvatars: Int = avatars.prefix(state.maxDisplayedAvatars).count
let overflowCount: Int = (avatars.count > maxDisplayedAvatars ? avatars.count - maxDisplayedAvatars : 0) + state.overflowCount
@ -185,50 +186,77 @@ public struct AvatarGroup: View {
let ringOuterGap: CGFloat = tokens.ringOuterGap
let ringGapOffset: CGFloat = ringOuterGap * 2
let ringOffset: CGFloat = tokens.ringThickness + tokens.ringInnerGap + tokens.ringOuterGap
HStack(spacing: 0) {
ForEach(0 ..< maxDisplayedAvatars, id: \.self) { index in
// If the avatar is part of Stack style and is not the last avatar in the sequence, create a cutout
let avatar = avatars[index]
let avatarView = avatarViews[index]
let needsCutout = tokens.style == .stack && (overflowCount > 0 || index + 1 < maxDisplayedAvatars)
let avatarSize: CGFloat = avatarView.state.totalSize()
let nextAvatarSize: CGFloat = needsCutout ? avatarViews[index + 1].state.totalSize() : 0
let isLastDisplayed = index == maxDisplayedAvatars - 1
let groupHeight: CGFloat = imageSize + (ringOffset * 2)
let currentAvatarHasRing = avatar.isRingVisible
let nextAvatarHasRing = index + 1 < maxDisplayedAvatars ? avatars[index + 1].isRingVisible : false
let avatarSizeDifference = avatarSize - nextAvatarSize
let sizeDiff = !isLastDisplayed ? (currentAvatarHasRing ? avatarSizeDifference : avatarSizeDifference - ringGapOffset) :
currentAvatarHasRing ? (avatarSize - ringGapOffset) - imageSize : (avatarSize - (ringGapOffset * 2)) - imageSize
let x = avatarSize + tokens.interspace - ringGapOffset
@ViewBuilder
var avatarGroupContent: some View {
HStack(spacing: 0) {
ForEach(enumeratedAvatars.prefix(maxDisplayedAvatars), id: \.1) { index, avatar in
// If the avatar is part of Stack style and is not the last avatar in the sequence, create a cutout.
let avatarView = avatarViews[index]
let needsCutout = tokens.style == .stack && (overflowCount > 0 || index + 1 < maxDisplayedAvatars)
let avatarSize: CGFloat = avatarView.state.totalSize()
let nextAvatarSize: CGFloat = needsCutout ? avatarViews[index + 1].state.totalSize() : 0
let isLastDisplayed = index == maxDisplayedAvatars - 1
let ringPaddingInterspace = nextAvatarHasRing ? interspace - (ringOffset + ringOuterGap) : interspace - ringOffset
let noRingPaddingInterspace = nextAvatarHasRing ? interspace - ringOuterGap : interspace
let rtlRingPaddingInterspace = (nextAvatarHasRing ? -x - ringOuterGap : -x + ringOffset)
let rtlNoRingPaddingInterspace = (nextAvatarHasRing ? -x - ringOffset - ringOuterGap : -x)
let stackPadding = (currentAvatarHasRing ? ringPaddingInterspace : noRingPaddingInterspace)
// Calculating the size delta of the current and next avatar based off of ring visibility, which helps determine
// starting coordinates for the cutout.
let currentAvatarHasRing = avatar.isRingVisible
let nextAvatarHasRing = index + 1 < maxDisplayedAvatars ? avatars[index + 1].isRingVisible : false
let avatarSizeDifference = avatarSize - nextAvatarSize
let sizeDiff = !isLastDisplayed ? (currentAvatarHasRing ? avatarSizeDifference : avatarSizeDifference - ringGapOffset) :
currentAvatarHasRing ? (avatarSize - ringGapOffset) - imageSize : (avatarSize - (ringGapOffset * 2)) - imageSize
let x = avatarSize + tokens.interspace - ringGapOffset
let xPosition = currentAvatarHasRing ? x - ringOuterGap - ringOuterGap : x - ringOuterGap
let xPositionRTL = currentAvatarHasRing ? rtlRingPaddingInterspace : rtlNoRingPaddingInterspace
let xOrigin = Locale.current.isRightToLeftLayoutDirection() ? xPositionRTL : xPosition
let yOrigin = sizeDiff / 2
let cutoutSize = isLastDisplayed ? (ringOuterGap * 2) + imageSize : nextAvatarSize
// Calculating the different interspace scenarios considering rings, RTL, and group style.
let ringPaddingInterspace = nextAvatarHasRing ? interspace - (ringOffset + ringOuterGap) : interspace - ringOffset
let noRingPaddingInterspace = nextAvatarHasRing ? interspace - ringOuterGap : interspace
let rtlRingPaddingInterspace = nextAvatarHasRing ? -x + ringGapOffset : -x + ringOffset + (ringOuterGap * 3)
let rtlNoRingPaddingInterspace = nextAvatarHasRing ? -x - ringOffset - ringGapOffset : -x - ringOuterGap
let stackPadding = currentAvatarHasRing ? ringPaddingInterspace : noRingPaddingInterspace
// Hand the rendering of the avatar to a helper function to appease Swift's
// strict type-checking timeout.
self.avatarCutout(avatarView,
needsCutout,
xOrigin,
yOrigin,
cutoutSize,
tokens.style == .stack ? stackPadding : interspace)
}
if overflowCount > 0 {
createOverflow(count: overflowCount)
}
// Finalized calculations for x and y coordinates of the Avatar if it needs a cutout.
let xPosition = currentAvatarHasRing ? x - ringGapOffset : x - ringOuterGap
let xPositionRTL = currentAvatarHasRing ? rtlRingPaddingInterspace : rtlNoRingPaddingInterspace
let xOrigin = Locale.current.isRightToLeftLayoutDirection() ? xPositionRTL : xPosition
let yOrigin = sizeDiff / 2
let cutoutSize = isLastDisplayed ? ringGapOffset + imageSize : nextAvatarSize
VStack {
avatarView
.transition(.identity)
.modifyIf(needsCutout, { view in
view.mask(Avatar.AvatarCutout(
xOrigin: xOrigin,
yOrigin: yOrigin,
cutoutSize: cutoutSize)
.fill(style: FillStyle(eoFill: true)))
})
}
.padding(.trailing, tokens.style == .stack ? stackPadding : interspace)
.animation(Animation.linear(duration: animationDuration))
.transition(AnyTransition.move(edge: .leading))
}
if overflowCount > 0 {
VStack {
createOverflow(count: overflowCount)
}
.animation(Animation.linear(duration: animationDuration))
.transition(AnyTransition.move(edge: .leading))
}
}
.frame(maxWidth: .infinity,
minHeight: groupHeight,
maxHeight: .infinity,
alignment: .leading)
}
return avatarGroupContent
}
private let animationDuration: CGFloat = 0.1
private func createOverflow(count: Int) -> Avatar {
var avatar = Avatar(style: .overflow, size: tokens.size)
let data = MSFAvatarStateImpl(style: .overflow, size: tokens.size)