1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.foundation.gestures.snapping
18 
19 import androidx.compose.runtime.Stable
20 
21 /**
22  * Describes the snapping positioning (i.e. final positioning after snapping animation finishes) of
23  * a given snap item in its containing layout.
24  */
25 @Stable
26 interface SnapPosition {
27     /**
28      * Calculates the snap position where items will be aligned to in a snapping container. For
29      * instance, if [SnapPosition.Center] is used, once the snapping finishes the center of one of
30      * the items in the snapping container will be aligned exactly to the center of the snapping
31      * container, that is because the value returned by [position] was calculated as such a way that
32      * when one applies it to the item's current offset it will generate that final positioning.
33      *
34      * The reference point is with respect to the start of the layout (including the content
35      * padding)
36      *
37      * @sample androidx.compose.foundation.samples.SnapFlingBehaviorSnapPosition
38      * @param layoutSize The main axis layout size within which an item can be positioned.
39      * @param itemSize The main axis size for the item being positioned within this snapping layout.
40      * @param beforeContentPadding The content padding in pixels applied before this Layout's
41      *   content.
42      * @param afterContentPadding The content padding in pixels applied after this Layout's content.
43      * @param itemIndex The index of the item being positioned.
44      * @param itemCount The total amount of items in the snapping container.
45      * @return The offset of the snap position where items will be aligned to in a snapping
46      *   container.
47      */
positionnull48     fun position(
49         layoutSize: Int,
50         itemSize: Int,
51         beforeContentPadding: Int,
52         afterContentPadding: Int,
53         itemIndex: Int,
54         itemCount: Int
55     ): Int
56 
57     /** Aligns the center of the item with the center of the containing layout. */
58     object Center : SnapPosition {
59         override fun position(
60             layoutSize: Int,
61             itemSize: Int,
62             beforeContentPadding: Int,
63             afterContentPadding: Int,
64             itemIndex: Int,
65             itemCount: Int
66         ): Int {
67             val availableLayoutSpace = layoutSize - beforeContentPadding - afterContentPadding
68             // we use availableLayoutSpace / 2 as the main anchor point and we discount half
69             // an item size so the item appear aligned with the center of the container.
70             return availableLayoutSpace / 2 - itemSize / 2
71         }
72 
73         override fun toString(): String {
74             return "Center"
75         }
76     }
77 
78     /** Aligns the start of the item with the start of the containing layout. */
79     object Start : SnapPosition {
positionnull80         override fun position(
81             layoutSize: Int,
82             itemSize: Int,
83             beforeContentPadding: Int,
84             afterContentPadding: Int,
85             itemIndex: Int,
86             itemCount: Int
87         ): Int = 0
88 
89         override fun toString(): String {
90             return "Start"
91         }
92     }
93 
94     /** Aligns the end of the item with the end of the containing layout. */
95     object End : SnapPosition {
positionnull96         override fun position(
97             layoutSize: Int,
98             itemSize: Int,
99             beforeContentPadding: Int,
100             afterContentPadding: Int,
101             itemIndex: Int,
102             itemCount: Int
103         ): Int {
104             val availableLayoutSpace = layoutSize - beforeContentPadding - afterContentPadding
105             // the snap position for the item is the end of the layout, discounting the item
106             // size
107             return availableLayoutSpace - itemSize
108         }
109 
toStringnull110         override fun toString(): String {
111             return "End"
112         }
113     }
114 }
115 
calculateDistanceToDesiredSnapPositionnull116 internal fun calculateDistanceToDesiredSnapPosition(
117     mainAxisViewPortSize: Int,
118     beforeContentPadding: Int,
119     afterContentPadding: Int,
120     itemSize: Int,
121     itemOffset: Int,
122     itemIndex: Int,
123     snapPosition: SnapPosition,
124     itemCount: Int
125 ): Float {
126     val desiredDistance =
127         with(snapPosition) {
128                 position(
129                     mainAxisViewPortSize,
130                     itemSize,
131                     beforeContentPadding,
132                     afterContentPadding,
133                     itemIndex,
134                     itemCount
135                 )
136             }
137             .toFloat()
138 
139     return itemOffset - desiredDistance
140 }
141