1/*
2 * Copyright (C) 2015 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
17package android.support.percent;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.support.annotation.NonNull;
22import android.support.v4.view.MarginLayoutParamsCompat;
23import android.support.v4.view.ViewCompat;
24import android.util.AttributeSet;
25import android.util.Log;
26import android.view.View;
27import android.view.ViewGroup;
28
29import android.support.percent.R;
30
31/**
32 * Helper for layouts that want to support percentage based dimensions.
33 *
34 * <p>This class collects utility methods that are involved in extracting percentage based dimension
35 * attributes and applying them to ViewGroup's children. If you would like to implement a layout
36 * that supports percentage based dimensions, you need to take several steps:
37 *
38 * <ol>
39 * <li> You need a {@link ViewGroup.LayoutParams} subclass in your ViewGroup that implements
40 * {@link android.support.percent.PercentLayoutHelper.PercentLayoutParams}.
41 * <li> In your {@code LayoutParams(Context c, AttributeSet attrs)} constructor create an instance
42 * of {@link PercentLayoutHelper.PercentLayoutInfo} by calling
43 * {@link PercentLayoutHelper#getPercentLayoutInfo(Context, AttributeSet)}. Return this
44 * object from {@code public PercentLayoutHelper.PercentLayoutInfo getPercentLayoutInfo()}
45 * method that you implemented for {@link android.support.percent.PercentLayoutHelper.PercentLayoutParams} interface.
46 * <li> Override
47 * {@link ViewGroup.LayoutParams#setBaseAttributes(TypedArray, int, int)}
48 * with a single line implementation {@code PercentLayoutHelper.fetchWidthAndHeight(this, a,
49 * widthAttr, heightAttr);}
50 * <li> In your ViewGroup override {@link ViewGroup#generateLayoutParams(AttributeSet)} to return
51 * your LayoutParams.
52 * <li> In your {@link ViewGroup#onMeasure(int, int)} override, you need to implement following
53 * pattern:
54 * <pre class="prettyprint">
55 * protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
56 *     mHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec);
57 *     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
58 *     if (mHelper.handleMeasuredStateTooSmall()) {
59 *         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
60 *     }
61 * }
62 * </pre>
63 * <li>In your {@link ViewGroup#onLayout(boolean, int, int, int, int)} override, you need to
64 * implement following pattern:
65 * <pre class="prettyprint">
66 * protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
67 *     super.onLayout(changed, left, top, right, bottom);
68 *     mHelper.restoreOriginalParams();
69 * }
70 * </pre>
71 * </ol>
72 */
73public class PercentLayoutHelper {
74    private static final String TAG = "PercentLayout";
75
76    private static final boolean DEBUG = false;
77    private static final boolean VERBOSE = false;
78
79    private final ViewGroup mHost;
80
81    public PercentLayoutHelper(@NonNull ViewGroup host) {
82        if (host == null) {
83            throw new IllegalArgumentException("host must be non-null");
84        }
85        mHost = host;
86    }
87
88    /**
89     * Helper method to be called from {@link ViewGroup.LayoutParams#setBaseAttributes} override
90     * that reads layout_width and layout_height attribute values without throwing an exception if
91     * they aren't present.
92     */
93    public static void fetchWidthAndHeight(ViewGroup.LayoutParams params, TypedArray array,
94            int widthAttr, int heightAttr) {
95        params.width = array.getLayoutDimension(widthAttr, 0);
96        params.height = array.getLayoutDimension(heightAttr, 0);
97    }
98
99    /**
100     * Iterates over children and changes their width and height to one calculated from percentage
101     * values.
102     * @param widthMeasureSpec Width MeasureSpec of the parent ViewGroup.
103     * @param heightMeasureSpec Height MeasureSpec of the parent ViewGroup.
104     */
105    public void adjustChildren(int widthMeasureSpec, int heightMeasureSpec) {
106        if (DEBUG) {
107            Log.d(TAG, "adjustChildren: " + mHost + " widthMeasureSpec: "
108                    + View.MeasureSpec.toString(widthMeasureSpec) + " heightMeasureSpec: "
109                    + View.MeasureSpec.toString(heightMeasureSpec));
110        }
111
112        // Calculate available space, accounting for host's paddings
113        int widthHint = View.MeasureSpec.getSize(widthMeasureSpec) - mHost.getPaddingLeft()
114                - mHost.getPaddingRight();
115        int heightHint = View.MeasureSpec.getSize(heightMeasureSpec) - mHost.getPaddingTop()
116                - mHost.getPaddingBottom();
117        for (int i = 0, N = mHost.getChildCount(); i < N; i++) {
118            View view = mHost.getChildAt(i);
119            ViewGroup.LayoutParams params = view.getLayoutParams();
120            if (DEBUG) {
121                Log.d(TAG, "should adjust " + view + " " + params);
122            }
123            if (params instanceof PercentLayoutParams) {
124                PercentLayoutInfo info =
125                        ((PercentLayoutParams) params).getPercentLayoutInfo();
126                if (DEBUG) {
127                    Log.d(TAG, "using " + info);
128                }
129                if (info != null) {
130                    if (params instanceof ViewGroup.MarginLayoutParams) {
131                        info.fillMarginLayoutParams(view, (ViewGroup.MarginLayoutParams) params,
132                                widthHint, heightHint);
133                    } else {
134                        info.fillLayoutParams(params, widthHint, heightHint);
135                    }
136                }
137            }
138        }
139    }
140
141    /**
142     * Constructs a PercentLayoutInfo from attributes associated with a View. Call this method from
143     * {@code LayoutParams(Context c, AttributeSet attrs)} constructor.
144     */
145    public static PercentLayoutInfo getPercentLayoutInfo(Context context,
146            AttributeSet attrs) {
147        PercentLayoutInfo info = null;
148        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PercentLayout_Layout);
149        float value = array.getFraction(R.styleable.PercentLayout_Layout_layout_widthPercent, 1, 1,
150                -1f);
151        if (value != -1f) {
152            if (VERBOSE) {
153                Log.v(TAG, "percent width: " + value);
154            }
155            info = info != null ? info : new PercentLayoutInfo();
156            info.widthPercent = value;
157        }
158        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_heightPercent, 1, 1, -1f);
159        if (value != -1f) {
160            if (VERBOSE) {
161                Log.v(TAG, "percent height: " + value);
162            }
163            info = info != null ? info : new PercentLayoutInfo();
164            info.heightPercent = value;
165        }
166        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_marginPercent, 1, 1, -1f);
167        if (value != -1f) {
168            if (VERBOSE) {
169                Log.v(TAG, "percent margin: " + value);
170            }
171            info = info != null ? info : new PercentLayoutInfo();
172            info.leftMarginPercent = value;
173            info.topMarginPercent = value;
174            info.rightMarginPercent = value;
175            info.bottomMarginPercent = value;
176        }
177        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_marginLeftPercent, 1, 1,
178                -1f);
179        if (value != -1f) {
180            if (VERBOSE) {
181                Log.v(TAG, "percent left margin: " + value);
182            }
183            info = info != null ? info : new PercentLayoutInfo();
184            info.leftMarginPercent = value;
185        }
186        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_marginTopPercent, 1, 1,
187                -1f);
188        if (value != -1f) {
189            if (VERBOSE) {
190                Log.v(TAG, "percent top margin: " + value);
191            }
192            info = info != null ? info : new PercentLayoutInfo();
193            info.topMarginPercent = value;
194        }
195        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_marginRightPercent, 1, 1,
196                -1f);
197        if (value != -1f) {
198            if (VERBOSE) {
199                Log.v(TAG, "percent right margin: " + value);
200            }
201            info = info != null ? info : new PercentLayoutInfo();
202            info.rightMarginPercent = value;
203        }
204        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_marginBottomPercent, 1, 1,
205                -1f);
206        if (value != -1f) {
207            if (VERBOSE) {
208                Log.v(TAG, "percent bottom margin: " + value);
209            }
210            info = info != null ? info : new PercentLayoutInfo();
211            info.bottomMarginPercent = value;
212        }
213        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_marginStartPercent, 1, 1,
214                -1f);
215        if (value != -1f) {
216            if (VERBOSE) {
217                Log.v(TAG, "percent start margin: " + value);
218            }
219            info = info != null ? info : new PercentLayoutInfo();
220            info.startMarginPercent = value;
221        }
222        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_marginEndPercent, 1, 1,
223                -1f);
224        if (value != -1f) {
225            if (VERBOSE) {
226                Log.v(TAG, "percent end margin: " + value);
227            }
228            info = info != null ? info : new PercentLayoutInfo();
229            info.endMarginPercent = value;
230        }
231
232        value = array.getFraction(R.styleable.PercentLayout_Layout_layout_aspectRatio, 1, 1, -1f);
233        if (value != -1f) {
234            if (VERBOSE) {
235                Log.v(TAG, "aspect ratio: " + value);
236            }
237            info = info != null ? info : new PercentLayoutInfo();
238            info.aspectRatio = value;
239        }
240
241        array.recycle();
242        if (DEBUG) {
243            Log.d(TAG, "constructed: " + info);
244        }
245        return info;
246    }
247
248    /**
249     * Iterates over children and restores their original dimensions that were changed for
250     * percentage values. Calling this method only makes sense if you previously called
251     * {@link PercentLayoutHelper#adjustChildren(int, int)}.
252     */
253    public void restoreOriginalParams() {
254        for (int i = 0, N = mHost.getChildCount(); i < N; i++) {
255            View view = mHost.getChildAt(i);
256            ViewGroup.LayoutParams params = view.getLayoutParams();
257            if (DEBUG) {
258                Log.d(TAG, "should restore " + view + " " + params);
259            }
260            if (params instanceof PercentLayoutParams) {
261                PercentLayoutInfo info =
262                        ((PercentLayoutParams) params).getPercentLayoutInfo();
263                if (DEBUG) {
264                    Log.d(TAG, "using " + info);
265                }
266                if (info != null) {
267                    if (params instanceof ViewGroup.MarginLayoutParams) {
268                        info.restoreMarginLayoutParams((ViewGroup.MarginLayoutParams) params);
269                    } else {
270                        info.restoreLayoutParams(params);
271                    }
272                }
273            }
274        }
275    }
276
277    /**
278     * Iterates over children and checks if any of them would like to get more space than it
279     * received through the percentage dimension.
280     *
281     * If you are building a layout that supports percentage dimensions you are encouraged to take
282     * advantage of this method. The developer should be able to specify that a child should be
283     * remeasured by adding normal dimension attribute with {@code wrap_content} value. For example
284     * he might specify child's attributes as {@code app:layout_widthPercent="60%p"} and
285     * {@code android:layout_width="wrap_content"}. In this case if the child receives too little
286     * space, it will be remeasured with width set to {@code WRAP_CONTENT}.
287     *
288     * @return True if the measure phase needs to be rerun because one of the children would like
289     * to receive more space.
290     */
291    public boolean handleMeasuredStateTooSmall() {
292        boolean needsSecondMeasure = false;
293        for (int i = 0, N = mHost.getChildCount(); i < N; i++) {
294            View view = mHost.getChildAt(i);
295            ViewGroup.LayoutParams params = view.getLayoutParams();
296            if (DEBUG) {
297                Log.d(TAG, "should handle measured state too small " + view + " " + params);
298            }
299            if (params instanceof PercentLayoutParams) {
300                PercentLayoutInfo info =
301                        ((PercentLayoutParams) params).getPercentLayoutInfo();
302                if (info != null) {
303                    if (shouldHandleMeasuredWidthTooSmall(view, info)) {
304                        needsSecondMeasure = true;
305                        params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
306                    }
307                    if (shouldHandleMeasuredHeightTooSmall(view, info)) {
308                        needsSecondMeasure = true;
309                        params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
310                    }
311                }
312            }
313        }
314        if (DEBUG) {
315            Log.d(TAG, "should trigger second measure pass: " + needsSecondMeasure);
316        }
317        return needsSecondMeasure;
318    }
319
320    private static boolean shouldHandleMeasuredWidthTooSmall(View view, PercentLayoutInfo info) {
321        int state = ViewCompat.getMeasuredWidthAndState(view) & ViewCompat.MEASURED_STATE_MASK;
322        return state == ViewCompat.MEASURED_STATE_TOO_SMALL && info.widthPercent >= 0 &&
323                info.mPreservedParams.width == ViewGroup.LayoutParams.WRAP_CONTENT;
324    }
325
326    private static boolean shouldHandleMeasuredHeightTooSmall(View view, PercentLayoutInfo info) {
327        int state = ViewCompat.getMeasuredHeightAndState(view) & ViewCompat.MEASURED_STATE_MASK;
328        return state == ViewCompat.MEASURED_STATE_TOO_SMALL && info.heightPercent >= 0 &&
329                info.mPreservedParams.height == ViewGroup.LayoutParams.WRAP_CONTENT;
330    }
331
332    /* package */ static class PercentMarginLayoutParams extends ViewGroup.MarginLayoutParams {
333        // These two flags keep track of whether we're computing the LayoutParams width and height
334        // in the fill pass based on the aspect ratio. This allows the fill pass to be re-entrant
335        // as the framework code can call onMeasure() multiple times before the onLayout() is
336        // called. Those multiple invocations of onMeasure() are not guaranteed to be called with
337        // the same set of width / height.
338        private boolean mIsHeightComputedFromAspectRatio;
339        private boolean mIsWidthComputedFromAspectRatio;
340
341        public PercentMarginLayoutParams(int width, int height) {
342            super(width, height);
343        }
344    }
345
346    /**
347     * Container for information about percentage dimensions and margins. It acts as an extension
348     * for {@code LayoutParams}.
349     */
350    public static class PercentLayoutInfo {
351        /** The decimal value of the percentage-based width. */
352        public float widthPercent;
353
354        /** The decimal value of the percentage-based height. */
355        public float heightPercent;
356
357        /** The decimal value of the percentage-based left margin. */
358        public float leftMarginPercent;
359
360        /** The decimal value of the percentage-based top margin. */
361        public float topMarginPercent;
362
363        /** The decimal value of the percentage-based right margin. */
364        public float rightMarginPercent;
365
366        /** The decimal value of the percentage-based bottom margin. */
367        public float bottomMarginPercent;
368
369        /** The decimal value of the percentage-based start margin. */
370        public float startMarginPercent;
371
372        /** The decimal value of the percentage-based end margin. */
373        public float endMarginPercent;
374
375        /** The decimal value of the percentage-based aspect ratio. */
376        public float aspectRatio;
377
378        /* package */ final PercentMarginLayoutParams mPreservedParams;
379
380        public PercentLayoutInfo() {
381            widthPercent = -1f;
382            heightPercent = -1f;
383            leftMarginPercent = -1f;
384            topMarginPercent = -1f;
385            rightMarginPercent = -1f;
386            bottomMarginPercent = -1f;
387            startMarginPercent = -1f;
388            endMarginPercent = -1f;
389            mPreservedParams = new PercentMarginLayoutParams(0, 0);
390        }
391
392        /**
393         * Fills the {@link ViewGroup.LayoutParams#width} and {@link ViewGroup.LayoutParams#height}
394         * fields of the passed {@link ViewGroup.LayoutParams} object based on currently set
395         * percentage values.
396         */
397        public void fillLayoutParams(ViewGroup.LayoutParams params, int widthHint,
398                int heightHint) {
399            // Preserve the original layout params, so we can restore them after the measure step.
400            mPreservedParams.width = params.width;
401            mPreservedParams.height = params.height;
402
403            // We assume that width/height set to 0 means that value was unset. This might not
404            // necessarily be true, as the user might explicitly set it to 0. However, we use this
405            // information only for the aspect ratio. If the user set the aspect ratio attribute,
406            // it means they accept or soon discover that it will be disregarded.
407            final boolean widthNotSet =
408                    (mPreservedParams.mIsWidthComputedFromAspectRatio
409                            || mPreservedParams.width == 0) && (widthPercent < 0);
410            final boolean heightNotSet =
411                    (mPreservedParams.mIsHeightComputedFromAspectRatio
412                            || mPreservedParams.height == 0) && (heightPercent < 0);
413
414            if (widthPercent >= 0) {
415                params.width = (int) (widthHint * widthPercent);
416            }
417
418            if (heightPercent >= 0) {
419                params.height = (int) (heightHint * heightPercent);
420            }
421
422            if (aspectRatio >= 0) {
423                if (widthNotSet) {
424                    params.width = (int) (params.height * aspectRatio);
425                    // Keep track that we've filled the width based on the height and aspect ratio.
426                    mPreservedParams.mIsWidthComputedFromAspectRatio = true;
427                }
428                if (heightNotSet) {
429                    params.height = (int) (params.width / aspectRatio);
430                    // Keep track that we've filled the height based on the width and aspect ratio.
431                    mPreservedParams.mIsHeightComputedFromAspectRatio = true;
432                }
433            }
434
435            if (DEBUG) {
436                Log.d(TAG, "after fillLayoutParams: (" + params.width + ", " + params.height + ")");
437            }
438        }
439
440        /**
441         * @deprecated Use
442         * {@link #fillMarginLayoutParams(View, ViewGroup.MarginLayoutParams, int, int)}
443         * for proper RTL support.
444         */
445        @Deprecated
446        public void fillMarginLayoutParams(ViewGroup.MarginLayoutParams params,
447                int widthHint, int heightHint) {
448            fillMarginLayoutParams(null, params, widthHint, heightHint);
449        }
450
451        /**
452         * Fills the margin fields of the passed {@link ViewGroup.MarginLayoutParams} object based
453         * on currently set percentage values and the current layout direction of the passed
454         * {@link View}.
455         */
456        public void fillMarginLayoutParams(View view, ViewGroup.MarginLayoutParams params,
457                int widthHint, int heightHint) {
458            fillLayoutParams(params, widthHint, heightHint);
459
460            // Preserve the original margins, so we can restore them after the measure step.
461            mPreservedParams.leftMargin = params.leftMargin;
462            mPreservedParams.topMargin = params.topMargin;
463            mPreservedParams.rightMargin = params.rightMargin;
464            mPreservedParams.bottomMargin = params.bottomMargin;
465            MarginLayoutParamsCompat.setMarginStart(mPreservedParams,
466                    MarginLayoutParamsCompat.getMarginStart(params));
467            MarginLayoutParamsCompat.setMarginEnd(mPreservedParams,
468                    MarginLayoutParamsCompat.getMarginEnd(params));
469
470            if (leftMarginPercent >= 0) {
471                params.leftMargin = (int) (widthHint * leftMarginPercent);
472            }
473            if (topMarginPercent >= 0) {
474                params.topMargin = (int) (heightHint * topMarginPercent);
475            }
476            if (rightMarginPercent >= 0) {
477                params.rightMargin = (int) (widthHint * rightMarginPercent);
478            }
479            if (bottomMarginPercent >= 0) {
480                params.bottomMargin = (int) (heightHint * bottomMarginPercent);
481            }
482            boolean shouldResolveLayoutDirection = false;
483            if (startMarginPercent >= 0) {
484                MarginLayoutParamsCompat.setMarginStart(params,
485                        (int) (widthHint * startMarginPercent));
486                shouldResolveLayoutDirection = true;
487            }
488            if (endMarginPercent >= 0) {
489                MarginLayoutParamsCompat.setMarginEnd(params,
490                        (int) (widthHint * endMarginPercent));
491                shouldResolveLayoutDirection = true;
492            }
493            if (shouldResolveLayoutDirection && (view != null)) {
494                // Force the resolve pass so that start / end margins are propagated to the
495                // matching left / right fields
496                MarginLayoutParamsCompat.resolveLayoutDirection(params,
497                        ViewCompat.getLayoutDirection(view));
498            }
499            if (DEBUG) {
500                Log.d(TAG, "after fillMarginLayoutParams: (" + params.width + ", " + params.height
501                        + ")");
502            }
503        }
504
505        @Override
506        public String toString() {
507            return String.format("PercentLayoutInformation width: %f height %f, margins (%f, %f, "
508                            + " %f, %f, %f, %f)", widthPercent, heightPercent, leftMarginPercent,
509                    topMarginPercent, rightMarginPercent, bottomMarginPercent, startMarginPercent,
510                    endMarginPercent);
511
512        }
513
514        /**
515         * Restores the original dimensions and margins after they were changed for percentage based
516         * values. You should call this method only if you previously called
517         * {@link PercentLayoutHelper.PercentLayoutInfo#fillMarginLayoutParams(View, ViewGroup.MarginLayoutParams, int, int)}.
518         */
519        public void restoreMarginLayoutParams(ViewGroup.MarginLayoutParams params) {
520            restoreLayoutParams(params);
521            params.leftMargin = mPreservedParams.leftMargin;
522            params.topMargin = mPreservedParams.topMargin;
523            params.rightMargin = mPreservedParams.rightMargin;
524            params.bottomMargin = mPreservedParams.bottomMargin;
525            MarginLayoutParamsCompat.setMarginStart(params,
526                    MarginLayoutParamsCompat.getMarginStart(mPreservedParams));
527            MarginLayoutParamsCompat.setMarginEnd(params,
528                    MarginLayoutParamsCompat.getMarginEnd(mPreservedParams));
529        }
530
531        /**
532         * Restores original dimensions after they were changed for percentage based values.
533         * You should call this method only if you previously called
534         * {@link PercentLayoutHelper.PercentLayoutInfo#fillLayoutParams(ViewGroup.LayoutParams, int, int)}.
535         */
536        public void restoreLayoutParams(ViewGroup.LayoutParams params) {
537            if (!mPreservedParams.mIsWidthComputedFromAspectRatio) {
538                // Only restore the width if we didn't compute it based on the height and
539                // aspect ratio in the fill pass.
540                params.width = mPreservedParams.width;
541            }
542            if (!mPreservedParams.mIsHeightComputedFromAspectRatio) {
543                // Only restore the height if we didn't compute it based on the width and
544                // aspect ratio in the fill pass.
545                params.height = mPreservedParams.height;
546            }
547
548            // Reset the tracking flags.
549            mPreservedParams.mIsWidthComputedFromAspectRatio = false;
550            mPreservedParams.mIsHeightComputedFromAspectRatio = false;
551        }
552    }
553
554    /**
555     * If a layout wants to support percentage based dimensions and use this helper class, its
556     * {@code LayoutParams} subclass must implement this interface.
557     *
558     * Your {@code LayoutParams} subclass should contain an instance of {@code PercentLayoutInfo}
559     * and the implementation of this interface should be a simple accessor.
560     */
561    public interface PercentLayoutParams {
562        PercentLayoutInfo getPercentLayoutInfo();
563    }
564}
565