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 android.support.v17.leanback.widget;
16
17import static android.support.v17.leanback.widget.BaseGridView.WINDOW_ALIGN_BOTH_EDGE;
18import static android.support.v17.leanback.widget.BaseGridView.WINDOW_ALIGN_HIGH_EDGE;
19import static android.support.v17.leanback.widget.BaseGridView.WINDOW_ALIGN_LOW_EDGE;
20import static android.support.v17.leanback.widget.BaseGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED;
21import static android.support.v7.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 ? (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0
258                        : (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
259                    if (!mReversedFlow ? isPreferKeylineOverLowEdge()
260                            : isPreferKeylineOverHighEdge()) {
261                        // if we prefer key line, might align max child to key line for minScroll
262                        mMinScroll = Math.min(mMinScroll,
263                                calculateScrollToKeyLine(maxChildViewCenter, keyLine));
264                    } else {
265                        // don't over scroll max
266                        mMaxScroll = Math.max(mMinScroll, mMaxScroll);
267                    }
268                } else if (!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0
269                        : (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
270                    if (!mReversedFlow ? isPreferKeylineOverHighEdge()
271                            : isPreferKeylineOverLowEdge()) {
272                        // if we prefer key line, might align min child to key line for maxScroll
273                        mMaxScroll = Math.max(mMaxScroll,
274                                calculateScrollToKeyLine(minChildViewCenter, keyLine));
275                    } else {
276                        // don't over scroll min
277                        mMinScroll = Math.min(mMinScroll, mMaxScroll);
278                    }
279                }
280            }
281        }
282
283        /**
284         * Get scroll distance of align an item (depends on ALIGN_LOW_EDGE, ALIGN_HIGH_EDGE or the
285         * item should be aligned to key line). The scroll distance will be capped by
286         * {@link #getMinScroll()} and {@link #getMaxScroll()}.
287         */
288        public final int getScroll(int viewCenter) {
289            final int size = getSize();
290            final int keyLine = calculateKeyline();
291            final boolean isMinUnknown = isMinUnknown();
292            final boolean isMaxUnknown = isMaxUnknown();
293            if (!isMinUnknown) {
294                final int keyLineToMinEdge = keyLine - mPaddingMin;
295                if ((!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0
296                     : (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0)
297                        && (viewCenter - mMinEdge <= keyLineToMinEdge)) {
298                    // view center is before key line: align the min edge (first child) to padding.
299                    int alignToMin = mMinEdge - mPaddingMin;
300                    // Also we need make sure don't over scroll
301                    if (!isMaxUnknown && alignToMin > mMaxScroll) {
302                        alignToMin = mMaxScroll;
303                    }
304                    return alignToMin;
305                }
306            }
307            if (!isMaxUnknown) {
308                final int keyLineToMaxEdge = size - keyLine - mPaddingMax;
309                if ((!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0
310                        : (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0)
311                        && (mMaxEdge - viewCenter <= keyLineToMaxEdge)) {
312                    // view center is after key line: align the max edge (last child) to padding.
313                    int alignToMax = mMaxEdge - (size - mPaddingMax);
314                    // Also we need make sure don't over scroll
315                    if (!isMinUnknown && alignToMax < mMinScroll) {
316                        alignToMax = mMinScroll;
317                    }
318                    return alignToMax;
319                }
320            }
321            // else put view center at key line.
322            return calculateScrollToKeyLine(viewCenter, keyLine);
323        }
324
325        public final void setReversedFlow(boolean reversedFlow) {
326            mReversedFlow = reversedFlow;
327        }
328
329        @Override
330        public String toString() {
331            return " min:" + mMinEdge + " " + mMinScroll + " max:" + mMaxEdge + " " + mMaxScroll;
332        }
333
334    }
335
336    private int mOrientation = HORIZONTAL;
337
338    public final Axis vertical = new Axis("vertical");
339
340    public final Axis horizontal = new Axis("horizontal");
341
342    private Axis mMainAxis = horizontal;
343
344    private Axis mSecondAxis = vertical;
345
346    public final Axis mainAxis() {
347        return mMainAxis;
348    }
349
350    public final Axis secondAxis() {
351        return mSecondAxis;
352    }
353
354    public final void setOrientation(int orientation) {
355        mOrientation = orientation;
356        if (mOrientation == HORIZONTAL) {
357            mMainAxis = horizontal;
358            mSecondAxis = vertical;
359        } else {
360            mMainAxis = vertical;
361            mSecondAxis = horizontal;
362        }
363    }
364
365    public final int getOrientation() {
366        return mOrientation;
367    }
368
369    public final void reset() {
370        mainAxis().reset();
371    }
372
373    @Override
374    public String toString() {
375        return new StringBuffer().append("horizontal=")
376                .append(horizontal.toString())
377                .append("; vertical=")
378                .append(vertical.toString())
379                .toString();
380    }
381
382}
383