public class

RatioViewWrapper

extends ViewGroup

 java.lang.Object

↳ViewGroup

↳androidx.wear.protolayout.renderer.inflater.RatioViewWrapper

Gradle dependencies

compile group: 'androidx.wear.protolayout', name: 'protolayout-renderer', version: '1.2.0'

  • groupId: androidx.wear.protolayout
  • artifactId: protolayout-renderer
  • version: 1.2.0

Artifact androidx.wear.protolayout:protolayout-renderer:1.2.0 it located at Google repository (https://maven.google.com/)

Overview

A wrapper for a view, which enforces that its dimensions adhere to a set ratio if possible. Note that while multiple children can be added, only the first child will be measured, laid out, and drawn.

This will measure the child as normal, given the width/height MeasureSpecs assigned to this object. If either (or both) the width and the height for the child are inexact (i.e. WRAP_CONTENT), this wrapper will size those dimensions to be proportional to any known dimension.

As an example, say we add this wrapper to a FrameView, with width = MATCH_PARENT and height = WRAP_CONTENT, with a ratio of 2 (i.e. width is double height). In this case, it will measure its first child in the parent's bounds, as normal, then enforce that the height must be parentWidth / 2.

Note that if both axes are exact, this container does nothing; it will simply size the child and itself according to the exact MeasureSpecs.

Summary

Fields
public static final floatUNDEFINED_ASPECT_RATIO

An undefined aspect ratio.

Constructors
publicRatioViewWrapper(Context context)

publicRatioViewWrapper(Context context, AttributeSet attributeSet)

publicRatioViewWrapper(Context context, AttributeSet attributeSet, int defStyleAttr)

publicRatioViewWrapper(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)

Methods
protected voidonLayout(boolean changed, int l, int t, int r, int b)

protected voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)

public voidsetAspectRatio(float aspectRatio)

Sets the aspect ratio that this RatioViewWrapper should conform to.

public voidsetPadding(int left, int top, int right, int bottom)

public voidsetPaddingRelative(int start, int top, int end, int bottom)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final float UNDEFINED_ASPECT_RATIO

An undefined aspect ratio. If RatioViewWrapper.setAspectRatio(float) is called with this value, or never called, this wrapper may only be used with child views with MeasureSpec.EXACTLY for both dimensions.

Constructors

public RatioViewWrapper(Context context)

public RatioViewWrapper(Context context, AttributeSet attributeSet)

public RatioViewWrapper(Context context, AttributeSet attributeSet, int defStyleAttr)

public RatioViewWrapper(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)

Methods

public void setAspectRatio(float aspectRatio)

Sets the aspect ratio that this RatioViewWrapper should conform to. This will force the view to have the dimensions width = aspect * height

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

protected void onLayout(boolean changed, int l, int t, int r, int b)

public void setPadding(int left, int top, int right, int bottom)

public void setPaddingRelative(int start, int top, int end, int bottom)

Source

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.wear.protolayout.renderer.inflater;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;

/**
 * A wrapper for a view, which enforces that its dimensions adhere to a set ratio if possible. Note
 * that while multiple children can be added, only the first child will be measured, laid out, and
 * drawn.
 *
 * <p>This will measure the child as normal, given the width/height MeasureSpecs assigned to this
 * object. If either (or both) the width and the height for the child are inexact (i.e.
 * WRAP_CONTENT), this wrapper will size those dimensions to be proportional to any known dimension.
 *
 * <p>As an example, say we add this wrapper to a FrameView, with width = MATCH_PARENT and height =
 * WRAP_CONTENT, with a ratio of 2 (i.e. width is double height). In this case, it will measure its
 * first child in the parent's bounds, as normal, then enforce that the height must be parentWidth /
 * 2.
 *
 * <p>Note that if both axes are exact, this container does nothing; it will simply size the child
 * and itself according to the exact MeasureSpecs.
 */
public class RatioViewWrapper extends ViewGroup {
    /**
     * An undefined aspect ratio. If {@link #setAspectRatio} is called with this value, or never
     * called, this wrapper may only be used with child views with {@code MeasureSpec.EXACTLY} for
     * both dimensions.
     */
    public static final float UNDEFINED_ASPECT_RATIO = -1;

    private static final float EPSILON = 0.00000000001f;
    private float mAspectRatio = UNDEFINED_ASPECT_RATIO;

    public RatioViewWrapper(@NonNull Context context) {
        this(context, null);
    }

    public RatioViewWrapper(@NonNull Context context, @Nullable AttributeSet attributeSet) {
        this(context, attributeSet, 0);
    }

    public RatioViewWrapper(
            @NonNull Context context,
            @Nullable AttributeSet attributeSet,
            @AttrRes int defStyleAttr) {
        this(context, attributeSet, defStyleAttr, 0);
    }

    public RatioViewWrapper(
            @NonNull Context context,
            @Nullable AttributeSet attrs,
            @AttrRes int defStyleAttr,
            @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * Sets the aspect ratio that this RatioViewWrapper should conform to. This will force the view
     * to have the dimensions width = aspect * height
     */
    public void setAspectRatio(float aspectRatio) {
        this.mAspectRatio = aspectRatio;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (getChildCount() != 1) {
            throw new IllegalStateException("RatioViewWrapper must contain a single child");
        }

        int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);

        View childView = getChildAt(0);

        // Measure the child within the given bounds.
        childView.measure(widthMeasureSpec, heightMeasureSpec);

        // No aspect ratio. Trust the child and hope for the best.
        if (mAspectRatio == UNDEFINED_ASPECT_RATIO) {
            setMeasuredDimension(childView.getMeasuredWidth(), childView.getMeasuredHeight());
            combineMeasuredStates(getMeasuredState(), childView.getMeasuredState());
            return;
        }

        // If both are MeasureSpec.EXACTLY, we can't do anything else. Set our dimensions to be the
        // same and exit.
        if (widthMeasureMode == MeasureSpec.EXACTLY && heightMeasureMode == MeasureSpec.EXACTLY) {
            setMeasuredDimension(childView.getMeasuredWidth(), childView.getMeasuredHeight());
            return;
        }

        // 1. If measure gives zero size, exit. This happens when the child view is an
        // ImageViewWithoutIntrinsicSize and gets a MeasureSpec which allows it to pick its own
        // size.
        // 2. If we've already hit our aspect ratio, exit.
        if (childView.getMeasuredWidth() == 0
                || childView.getMeasuredHeight() == 0
                || Math.abs(
                                (float) childView.getMeasuredWidth() / childView.getMeasuredHeight()
                                        - mAspectRatio)
                        <= EPSILON) {
            setMeasuredDimension(childView.getMeasuredWidth(), childView.getMeasuredHeight());
            return;
        }

        if ((widthMeasureMode == MeasureSpec.AT_MOST || widthMeasureMode == MeasureSpec.UNSPECIFIED)
                && (heightMeasureMode == MeasureSpec.AT_MOST
                        || heightMeasureMode == MeasureSpec.UNSPECIFIED)) {
            // Generally, this happens if this view has both width/height=WRAP_CONTENT. This can
            // also happen though if this view has both dimensions as MATCH_CONTENT, but the parent
            // view is WRAP_CONTENT. In that case, the parent will run a first view pass to get the
            // size of the children, then calculate its size and re-size this widget with EXACTLY
            // MeasureSpecs.
            //
            // In this case, let's just assume that the child has reached the maximum size that it
            // wants, so rescale the dimension that will make it _smaller_.
            float targetWidth = childView.getMeasuredHeight() * mAspectRatio;
            float targetHeight = childView.getMeasuredWidth() / mAspectRatio;

            if (targetWidth < childView.getMeasuredWidth()) {
                // Resize the width down
                int childWidth =
                        MeasureSpec.makeMeasureSpec((int) targetWidth, MeasureSpec.EXACTLY);
                int childHeight =
                        MeasureSpec.makeMeasureSpec(
                                childView.getMeasuredHeight(), MeasureSpec.EXACTLY);

                childView.measure(childWidth, childHeight);
                setMeasuredDimension(childView.getMeasuredWidth(), childView.getMeasuredHeight());
            } else if (targetHeight < childView.getMeasuredHeight()) {
                // Resize the height down
                int childWidth =
                        MeasureSpec.makeMeasureSpec(
                                childView.getMeasuredWidth(), MeasureSpec.EXACTLY);
                int childHeight =
                        MeasureSpec.makeMeasureSpec((int) targetHeight, MeasureSpec.EXACTLY);

                childView.measure(childWidth, childHeight);
                setMeasuredDimension(childView.getMeasuredWidth(), childView.getMeasuredHeight());
            } else {
                // This should have been picked up by the aspect ratio check above...
                throw new IllegalStateException(
                        "Neither target width nor target height was smaller than measured"
                                + " width/height");
            }
        } else if (widthMeasureMode == MeasureSpec.EXACTLY) {
            // Can't change the width, but can change height.
            float targetHeight = childView.getMeasuredWidth() / mAspectRatio;

            int childWidth =
                    MeasureSpec.makeMeasureSpec(childView.getMeasuredWidth(), MeasureSpec.EXACTLY);
            int childHeight = MeasureSpec.makeMeasureSpec((int) targetHeight, MeasureSpec.EXACTLY);

            childView.measure(childWidth, childHeight);

            // We're pulling some hacks here. We get an AT_MOST constraint, but if we oversize
            // ourselves, the parent container should do appropriate clipping.
            setMeasuredDimension(childView.getMeasuredWidth(), childView.getMeasuredHeight());
        } else if (heightMeasureMode == MeasureSpec.EXACTLY) {
            // Can't change height, change width.
            float targetWidth = childView.getMeasuredHeight() * mAspectRatio;

            int childWidth = MeasureSpec.makeMeasureSpec((int) targetWidth, MeasureSpec.EXACTLY);
            int childHeight =
                    MeasureSpec.makeMeasureSpec(childView.getMeasuredHeight(), MeasureSpec.EXACTLY);

            childView.measure(childWidth, childHeight);

            setMeasuredDimension(childView.getMeasuredWidth(), childView.getMeasuredHeight());
        } else {
            // This should never happen; the first if checks that both MeasureSpecs are either
            // AT_MOST or UNSPECIFIED. If that branch isn't taken, one of the MeasureSpecs must be
            // EXACTLY. It's technically possible to smash the flag bits though (mode == 3 is
            // invalid), so if we get here, that must have happened.
            throw new IllegalArgumentException("Unknown measure mode bits in given MeasureSpecs");
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        View childView = getChildAt(0);

        // Place the child view within the bounds. If the child is greater than the bounds (i.e. one
        // of the constraints was MATCH_PARENT, and the other was free), then just align the
        // top-left for now.
        childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
    }

    // setPadding(Relative) should just pass straight through to the child; this View should just be
    // a wrapper, so should not itself introduce any extra spacing.
    //
    // We don't override the getters, since nothing in the layout tree should actually use them.
    @Override
    public void setPadding(int left, int top, int right, int bottom) {
        View childView = getChildAt(0);
        childView.setPadding(left, top, right, bottom);
    }

    @Override
    public void setPaddingRelative(int start, int top, int end, int bottom) {
        View childView = getChildAt(0);
        childView.setPaddingRelative(start, top, end, bottom);
    }
}