Skip to content

Commit

Permalink
Improve content change animations (#259)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryankeller authored Aug 24, 2023
1 parent d45ab96 commit 64084e5
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 98 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated the default `HorizontalMonthLayoutOptions` to use a `restingPosition` of `.atLeadingEdgeOfEachMonth`, which is probably what most people want
- Updated more publicly-exposed types conform to `Hashable`, because why not - maybe API consumers want to stick things in a `Set` or `Dictionary`.
- Allowed `CalendarItemViewRepresentable` to have no `Content` type if the conforming view does not depend on any variable data
- Changed `ItemView` to determine user interaction capabilities from its content view's `hitTest` / `pointInside` functions
- Changed `ItemView` to determine user interaction capabilities from its content view's `hitTest` / `pointInside` functions
- Updated content-change animations so that the same scroll offset is maintained throughout the animation

## [v1.16.0](https://github.com/airbnb/HorizonCalendar/compare/v1.15.0...v1.16.0) - 2023-01-30

Expand Down
10 changes: 6 additions & 4 deletions Sources/Internal/FrameProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,8 @@ final class FrameProvider {
monthHeaderHeight: CGFloat)
-> CGFloat
{
let numberOfWeekRows = rowInMonth + 1
return dayItemFrame.maxY -
heightOfDayContent(forNumberOfWeekRows: numberOfWeekRows) -
dayItemFrame.minY -
((daySize.height + content.verticalDayMargin) * CGFloat(rowInMonth)) -
heightOfDaysOfTheWeekRowInMonth() -
content.monthDayInsets.top -
monthHeaderHeight
Expand Down Expand Up @@ -460,7 +459,10 @@ final class FrameProvider {
// For example, the returned height value for 5 week rows will be 5x the `daySize.height`, plus 4x
// the `content.verticalDayMargin`.
private func heightOfDayContent(forNumberOfWeekRows numberOfWeekRows: Int) -> CGFloat {
(CGFloat(numberOfWeekRows) * daySize.height) +
guard numberOfWeekRows > 0 else {
fatalError("Cannot calculate the height of day content if `numberOfWeekRows` is <= 0.")
}
return (CGFloat(numberOfWeekRows) * daySize.height) +
(CGFloat(numberOfWeekRows - 1) * content.verticalDayMargin)
}

Expand Down
38 changes: 32 additions & 6 deletions Sources/Internal/VisibleItemsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ final class VisibleItemsProvider {
scrollPosition: CalendarViewScrollPosition)
-> LayoutItem
{
var context = VisibleItemsContext(
centermostLayoutItem: LayoutItem(itemType: .monthHeader(month), frame: .zero))
let layoutItem = LayoutItem(itemType: .monthHeader(month), frame: .zero)
var context = VisibleItemsContext(centermostLayoutItem: layoutItem, firstLayoutItem: layoutItem)
let monthHeaderHeight = monthHeaderHeight(for: month, context: &context)

let monthOrigin: CGPoint
Expand Down Expand Up @@ -111,8 +111,8 @@ final class VisibleItemsProvider {
scrollPosition: CalendarViewScrollPosition)
-> LayoutItem
{
var context = VisibleItemsContext(
centermostLayoutItem: LayoutItem(itemType: .day(day), frame: .zero))
let layoutItem = LayoutItem(itemType: .day(day), frame: .zero)
var context = VisibleItemsContext(centermostLayoutItem: layoutItem, firstLayoutItem: layoutItem)
let month = day.month
let monthHeaderHeight = monthHeaderHeight(for: month, context: &context)

Expand Down Expand Up @@ -162,6 +162,7 @@ final class VisibleItemsProvider {
// calendar item models.
var context = VisibleItemsContext(
centermostLayoutItem: previouslyVisibleLayoutItem,
firstLayoutItem: previouslyVisibleLayoutItem,
calendarItemModelCache: .init(
minimumCapacity: previousCalendarItemModelCache?.capacity ?? 100))

Expand Down Expand Up @@ -274,6 +275,7 @@ final class VisibleItemsProvider {
return VisibleItemsDetails(
visibleItems: context.visibleItems,
centermostLayoutItem: context.centermostLayoutItem,
firstLayoutItem: context.firstLayoutItem,
visibleDayRange: visibleDayRange,
visibleMonthRange: visibleMonthRange,
framesForVisibleMonths: context.framesForVisibleMonths,
Expand Down Expand Up @@ -330,7 +332,9 @@ final class VisibleItemsProvider {

var lastHandledLayoutItemEnumeratingBackwards = previouslyVisibleLayoutItem
var lastHandledLayoutItemEnumeratingForwards = previouslyVisibleLayoutItem
var context = VisibleItemsContext(centermostLayoutItem: previouslyVisibleLayoutItem)
var context = VisibleItemsContext(
centermostLayoutItem: previouslyVisibleLayoutItem,
firstLayoutItem: previouslyVisibleLayoutItem)

layoutItemTypeEnumerator.enumerateItemTypes(
startingAt: previouslyVisibleLayoutItem.itemType,
Expand Down Expand Up @@ -391,6 +395,22 @@ final class VisibleItemsProvider {
return itemDistance < otherItemDistance ? item : otherItem
}

// Returns the layout item closest to the top/leading edge of `bounds`.
private func firstLayoutItem(comparing item: LayoutItem, to otherItem: LayoutItem) -> LayoutItem {
let itemOrigin: CGFloat
let otherItemOrigin: CGFloat
switch content.monthsLayout {
case .vertical:
itemOrigin = item.frame.minY
otherItemOrigin = otherItem.frame.minY
case .horizontal:
itemOrigin = item.frame.minX
otherItemOrigin = otherItem.frame.minX
}

return itemOrigin < otherItemOrigin ? item : otherItem
}

private func monthOrigin(
forMonthContaining layoutItem: LayoutItem,
monthHeaderHeight: CGFloat,
Expand Down Expand Up @@ -813,10 +833,14 @@ final class VisibleItemsProvider {
frame: layoutItem.frame)
context.visibleItems.insert(visibleItem)

context.centermostLayoutItem = self.centermostLayoutItem(
context.centermostLayoutItem = centermostLayoutItem(
comparing: layoutItem,
to: context.centermostLayoutItem,
inBounds: bounds)

context.firstLayoutItem = firstLayoutItem(
comparing: layoutItem,
to: context.firstLayoutItem)
}
} else {
shouldStop = true
Expand Down Expand Up @@ -1188,6 +1212,7 @@ final class VisibleItemsProvider {

private struct VisibleItemsContext {
var centermostLayoutItem: LayoutItem
var firstLayoutItem: LayoutItem
var firstVisibleDay: Day?
var lastVisibleDay: Day?
var firstVisibleMonth: Month?
Expand All @@ -1211,6 +1236,7 @@ private struct VisibleItemsContext {
struct VisibleItemsDetails {
let visibleItems: Set<VisibleItem>
let centermostLayoutItem: LayoutItem
let firstLayoutItem: LayoutItem?
let visibleDayRange: DayRange?
let visibleMonthRange: MonthRange?
let framesForVisibleMonths: [Month: CGRect]
Expand Down
9 changes: 8 additions & 1 deletion Sources/Public/CalendarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,14 @@ public final class CalendarView: UIView {
isAnchorLayoutItemValid = false
}

if !isAnchorLayoutItemValid {
if isAnchorLayoutItemValid {
// If we have a valid `anchorLayoutItem`, change it to be the topmost item. Normally, the
// `anchorLayoutItem` is the centermost item, but when our content changes, it can make the
// transition look better if our layout reference point is at the top of the screen.
anchorLayoutItem = visibleItemsDetails?.firstLayoutItem ?? anchorLayoutItem
} else {
// If the `anchorLayoutItem` is no longer valid (due to it no longer being in the visible day
// range), set it to nil. This will force us to find a new `anchorLayoutItem`.
anchorLayoutItem = nil
}

Expand Down
Loading

0 comments on commit 64084e5

Please sign in to comment.