1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14
15package androidx.leanback.widget;
16
17import static androidx.leanback.widget.BaseGridView.WINDOW_ALIGN_BOTH_EDGE;
18import static androidx.leanback.widget.BaseGridView.WINDOW_ALIGN_HIGH_EDGE;
19import static androidx.leanback.widget.BaseGridView.WINDOW_ALIGN_LOW_EDGE;
20import static androidx.leanback.widget.BaseGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED;
21import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL;
22
23/**
24 * Maintains Window Alignment information of two axis.
25 */
26class WindowAlignment {
27
28    /**
29     * Maintains alignment information in one direction.
30     */
31    public static class Axis {
32        /**
33         * Right or bottom edge of last child.
34         */
35        private int mMaxEdge;
36        /**
37         * Left or top edge of first child
38         */
39        private int mMinEdge;
40        /**
41         * Scroll distance to align last child, it defines limit of scroll.
42         */
43        private int mMaxScroll;
44        /**
45         * Scroll distance to align first child, it defines limit of scroll.
46         */
47        private int mMinScroll;
48
49        static final int PF_KEYLINE_OVER_LOW_EDGE = 1;
50        static final int PF_KEYLINE_OVER_HIGH_EDGE = 1 << 1;
51
52        /**
53         * By default we prefer low edge over keyline, prefer keyline over high edge.
54         */
55        private int mPreferredKeyLine = PF_KEYLINE_OVER_HIGH_EDGE;
56
57        private int mWindowAlignment = WINDOW_ALIGN_BOTH_EDGE;
58
59        private int mWindowAlignmentOffset = 0;
60
61        private float mWindowAlignmentOffsetPercent = 50f;
62
63        private int mSize;
64
65        /**
66         * Padding at the min edge, it is the left or top padding.
67         */
68        private int mPaddingMin;
69
70        /**
71         * Padding at the max edge, it is the right or bottom padding.
72         */
73        private int mPaddingMax;
74
75        private boolean mReversedFlow;
76
77        private String mName; // for debugging
78
79        public Axis(String name) {
80            reset();
81            mName = name;
82        }
83
84        public final int getWindowAlignment() {
85            return mWindowAlignment;
86        }
87
88        public final void setWindowAlignment(int windowAlignment) {
89            mWindowAlignment = windowAlignment;
90        }
91
92        final void setPreferKeylineOverLowEdge(boolean keylineOverLowEdge) {
93            mPreferredKeyLine = keylineOverLowEdge
94                    ? mPreferredKeyLine | PF_KEYLINE_OVER_LOW_EDGE
95                    : mPreferredKeyLine & ~PF_KEYLINE_OVER_LOW_EDGE;
96        }
97
98        final void setPreferKeylineOverHighEdge(boolean keylineOverHighEdge) {
99            mPreferredKeyLine = keylineOverHighEdge
100                    ? mPreferredKeyLine | PF_KEYLINE_OVER_HIGH_EDGE
101                    : mPreferredKeyLine & ~PF_KEYLINE_OVER_HIGH_EDGE;
102        }
103
104        final boolean isPreferKeylineOverHighEdge() {
105            return (mPreferredKeyLine & PF_KEYLINE_OVER_HIGH_EDGE) != 0;
106        }
107
108        final boolean isPreferKeylineOverLowEdge() {
109            return (mPreferredKeyLine & PF_KEYLINE_OVER_LOW_EDGE) != 0;
110        }
111
112        public final int getWindowAlignmentOffset() {
113            return mWindowAlignmentOffset;
114        }
115
116        public final void setWindowAlignmentOffset(int offset) {
117            mWindowAlignmentOffset = offset;
118        }
119
120        public final void setWindowAlignmentOffsetPercent(float percent) {
121            if ((percent < 0 || percent > 100)
122                    && percent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
123                throw new IllegalArgumentException();
124            }
125            mWindowAlignmentOffsetPercent = percent;
126        }
127
128        public final float getWindowAlignmentOffsetPercent() {
129            return mWindowAlignmentOffsetPercent;
130        }
131
132        /**
133         * Returns scroll distance to align min child.
134         */
135        public final int getMinScroll() {
136            return mMinScroll;
137        }
138
139        public final void invalidateScrollMin() {
140            mMinEdge = Integer.MIN_VALUE;
141            mMinScroll = Integer.MIN_VALUE;
142        }
143
144        /**
145         * Returns scroll distance to align max child.
146         */
147        public final int getMaxScroll() {
148            return mMaxScroll;
149        }
150
151        public final void invalidateScrollMax() {
152            mMaxEdge = Integer.MAX_VALUE;
153            mMaxScroll = Integer.MAX_VALUE;
154        }
155
156        void reset() {
157            mMinEdge = Integer.MIN_VALUE;
158            mMaxEdge = Integer.MAX_VALUE;
159        }
160
161        public final boolean isMinUnknown() {
162            return mMinEdge == Integer.MIN_VALUE;
163        }
164
165        public final boolean isMaxUnknown() {
166            return mMaxEdge == Integer.MAX_VALUE;
167        }
168
169        public final void setSize(int size) {
170            mSize = size;
171        }
172
173        public final int getSize() {
174            return mSize;
175        }
176
177        public final void setPadding(int paddingMin, int paddingMax) {
178            mPaddingMin = paddingMin;
179            mPaddingMax = paddingMax;
180        }
181
182        public final int getPaddingMin() {
183            return mPaddingMin;
184        }
185
186        public final int getPaddingMax() {
187            return mPaddingMax;
188        }
189
190        public final int getClientSize() {
191            return mSize - mPaddingMin - mPaddingMax;
192        }
193
194        final int calculateKeyline() {
195            int keyLine;
196            if (!mReversedFlow) {
197                if (mWindowAlignmentOffset >= 0) {
198                    keyLine = mWindowAlignmentOffset;
199                } else {
200                    keyLine = mSize + mWindowAlignmentOffset;
201                }
202                if (mWindowAlignmentOffsetPercent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
203                    keyLine += (int) (mSize * mWindowAlignmentOffsetPercent / 100);
204                }
205            } else {
206                if (mWindowAlignmentOffset >= 0) {
207                    keyLine = mSize - mWindowAlignmentOffset;
208                } else {
209                    keyLine = -mWindowAlignmentOffset;
210                }
211                if (mWindowAlignmentOffsetPercent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
212                    keyLine -= (int) (mSize * mWindowAlignmentOffsetPercent / 100);
213                }
214            }
215            return keyLine;
216        }
217
218        /**
219         * Returns scroll distance to move viewCenterPosition to keyLine.
220         */
221        final int calculateScrollToKeyLine(int viewCenterPosition, int keyLine) {
222            return viewCenterPosition - keyLine;
223        }
224
225        /**
226         * Update {@link #getMinScroll()} and {@link #getMaxScroll()}
227         */
228        public final void updateMinMax(int minEdge, int maxEdge,
229                int minChildViewCenter, int maxChildViewCenter) {
230            mMinEdge = minEdge;
231            mMaxEdge = maxEdge;
232            final int clientSize = getClientSize();
233            final int keyLine = calculateKeyline();
234            final boolean isMinUnknown = isMinUnknown();
235            final boolean isMaxUnknown = isMaxUnknown();
236            if (!isMinUnknown) {
237                if (!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0
238                        : (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
239                    // calculate scroll distance to move current mMinEdge to padding at min edge
240                    mMinScroll = mMinEdge - mPaddingMin;
241                } else  {
242                    // calculate scroll distance to move min child center to key line
243                    mMinScroll = calculateScrollToKeyLine(minChildViewCenter, keyLine);
244                }
245            }
246            if (!isMaxUnknown) {
247                if (!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0
248                        : (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
249                    // calculate scroll distance to move current mMaxEdge to padding at max edge
250                    mMaxScroll = mMaxEdge - mPaddingMin - clientSize;
251                } else  {
252                    // calculate scroll distance to move max child center to key line
253                    mMaxScroll = calculateScrollToKeyLine(maxChildViewCenter, keyLine);
254                }
255            }
256            if (!isMaxUnknown && !isMinUnknown) {
257                if (!mReversedFlow) {
258                    if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
259                        if (isPreferKeylineOverLowEdge()) {
260                            // if we prefer key line, might align max child to key line for
261                            // minScroll
262                            mMinScroll = Math.min(mMinScroll,
263                                    calculateScrollToKeyLine(maxChildViewCenter, keyLine));
264                        }
265                        // don't over scroll max
266                        mMaxScroll = Math.max(mMinScroll, mMaxScroll);
267                    } else if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
268                        if (isPreferKeylineOverHighEdge()) {
269                            // if we prefer key line, might align min child to key line for
270                            // maxScroll
271                            mMaxScroll = Math.max(mMaxScroll,
272                                    calculateScrollToKeyLine(minChildViewCenter, keyLine));
273                        }
274                        // don't over scroll min
275                        mMinScroll = Math.min(mMinScroll, mMaxScroll);
276                    }
277                } else {
278                    if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
279                        if (isPreferKeylineOverLowEdge()) {
280                            // if we prefer key line, might align min child to key line for
281                            // maxScroll
282                            mMaxScroll = Math.max(mMaxScroll,
283                                    calculateScrollToKeyLine(minChildViewCenter, keyLine));
284                        }
285                        // don't over scroll min
286                        mMinScroll = Math.min(mMinScroll, mMaxScroll);
287                    } else if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
288                        if (isPreferKeylineOverHighEdge()) {
289                            // if we prefer key line, might align max child to key line for
290                            // minScroll
291                            mMinScroll = Math.min(mMinScroll,
292                                    calculateScrollToKeyLine(maxChildViewCenter, keyLine));
293                        }
294                        // don't over scroll max
295                        mMaxScroll = Math.max(mMinScroll, mMaxScroll);
296                    }
297                }
298            }
299        }
300
301        /**
302         * Get scroll distance of align an item (depends on ALIGN_LOW_EDGE, ALIGN_HIGH_EDGE or the
303         * item should be aligned to key line). The scroll distance will be capped by
304         * {@link #getMinScroll()} and {@link #getMaxScroll()}.
305         */
306        public final int getScroll(int viewCenter) {
307            final int size = getSize();
308            final int keyLine = calculateKeyline();
309            final boolean isMinUnknown = isMinUnknown();
310            final boolean isMaxUnknown = isMaxUnknown();
311            if (!isMinUnknown) {
312                final int keyLineToMinEdge = keyLine - mPaddingMin;
313                if ((!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0
314                     : (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0)
315                        && (viewCenter - mMinEdge <= keyLineToMinEdge)) {
316                    // view center is before key line: align the min edge (first child) to padding.
317                    int alignToMin = mMinEdge - mPaddingMin;
318                    // Also we need make sure don't over scroll
319                    if (!isMaxUnknown && alignToMin > mMaxScroll) {
320                        alignToMin = mMaxScroll;
321                    }
322                    return alignToMin;
323                }
324            }
325            if (!isMaxUnknown) {
326                final int keyLineToMaxEdge = size - keyLine - mPaddingMax;
327                if ((!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0
328                        : (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0)
329                        && (mMaxEdge - viewCenter <= keyLineToMaxEdge)) {
330                    // view center is after key line: align the max edge (last child) to padding.
331                    int alignToMax = mMaxEdge - (size - mPaddingMax);
332                    // Also we need make sure don't over scroll
333                    if (!isMinUnknown && alignToMax < mMinScroll) {
334                        alignToMax = mMinScroll;
335                    }
336                    return alignToMax;
337                }
338            }
339            // else put view center at key line.
340            return calculateScrollToKeyLine(viewCenter, keyLine);
341        }
342
343        public final void setReversedFlow(boolean reversedFlow) {
344            mReversedFlow = reversedFlow;
345        }
346
347        @Override
348        public String toString() {
349            return " min:" + mMinEdge + " " + mMinScroll + " max:" + mMaxEdge + " " + mMaxScroll;
350        }
351
352    }
353
354    private int mOrientation = HORIZONTAL;
355
356    public final Axis vertical = new Axis("vertical");
357
358    public final Axis horizontal = new Axis("horizontal");
359
360    private Axis mMainAxis = horizontal;
361
362    private Axis mSecondAxis = vertical;
363
364    public final Axis mainAxis() {
365        return mMainAxis;
366    }
367
368    public final Axis secondAxis() {
369        return mSecondAxis;
370    }
371
372    public final void setOrientation(int orientation) {
373        mOrientation = orientation;
374        if (mOrientation == HORIZONTAL) {
375            mMainAxis = horizontal;
376            mSecondAxis = vertical;
377        } else {
378            mMainAxis = vertical;
379            mSecondAxis = horizontal;
380        }
381    }
382
383    public final int getOrientation() {
384        return mOrientation;
385    }
386
387    public final void reset() {
388        mainAxis().reset();
389    }
390
391    @Override
392    public String toString() {
393        return "horizontal=" + horizontal + "; vertical=" + vertical;
394    }
395
396}
397