content: /*
   * Copyright (C) 2008 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 android.graphics.drawable;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UnsupportedAppUsage;
  import android.content.res.Resources;
  import android.content.res.Resources.Theme;
  import android.content.res.TypedArray;
  import android.graphics.Bitmap;
  import android.graphics.Insets;
  import android.graphics.Outline;
  import android.graphics.PixelFormat;
  import android.graphics.Rect;
  import android.util.AttributeSet;
  import android.util.DisplayMetrics;
  import android.util.TypedValue;
  
  import com.android.internal.R;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  
  /**
   * A Drawable that insets another Drawable by a specified distance or fraction of the content bounds.
   * This is used when a View needs a background that is smaller than
   * the View's actual bounds.
   *
   * <p>It can be defined in an XML file with the <code>&lt;inset></code> element. For more
   * information, see the guide to <a
   * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
   *
   * @attr ref android.R.styleable#InsetDrawable_visible
   * @attr ref android.R.styleable#InsetDrawable_drawable
   * @attr ref android.R.styleable#InsetDrawable_insetLeft
   * @attr ref android.R.styleable#InsetDrawable_insetRight
   * @attr ref android.R.styleable#InsetDrawable_insetTop
   * @attr ref android.R.styleable#InsetDrawable_insetBottom
   */
  public class InsetDrawable extends DrawableWrapper {
      private final Rect mTmpRect = new Rect();
      private final Rect mTmpInsetRect = new Rect();
  
      @UnsupportedAppUsage
      private InsetState mState;
  
      /**
       * No-arg constructor used by drawable inflation.
       */
      InsetDrawable() {
          this(new InsetState(null, null), null);
      }
  
      /**
       * Creates a new inset drawable with the specified inset.
       *
       * @param drawable The drawable to inset.
       * @param inset Inset in pixels around the drawable.
       */
      public InsetDrawable(@Nullable Drawable drawable, int inset) {
          this(drawable, inset, inset, inset, inset);
      }
  
      /**
       * Creates a new inset drawable with the specified inset.
       *
       * @param drawable The drawable to inset.
       * @param inset Inset in fraction (range: [0, 1)) of the inset content bounds.
       */
      public InsetDrawable(@Nullable Drawable drawable, float inset) {
          this(drawable, inset, inset, inset, inset);
      }
  
      /**
       * Creates a new inset drawable with the specified insets in pixels.
       *
       * @param drawable The drawable to inset.
       * @param insetLeft Left inset in pixels.
       * @param insetTop Top inset in pixels.
       * @param insetRight Right inset in pixels.
       * @param insetBottom Bottom inset in pixels.
       */
      public InsetDrawable(@Nullable Drawable drawable, int insetLeft, int insetTop,
              int insetRight, int insetBottom) {
          this(new InsetState(null, null), null);
  
          mState.mInsetLeft = new InsetValue(0f, insetLeft);
          mState.mInsetTop = new InsetValue(0f, insetTop);
          mState.mInsetRight = new InsetValue(0f, insetRight);
          mState.mInsetBottom = new InsetValue(0f, insetBottom);
  
          setDrawable(drawable);
      }
  
      /**
       * Creates a new inset drawable with the specified insets in fraction of the view bounds.
       *
       * @param drawable The drawable to inset.
       * @param insetLeftFraction Left inset in fraction (range: [0, 1)) of the inset content bounds.
       * @param insetTopFraction Top inset in fraction (range: [0, 1)) of the inset content bounds.
       * @param insetRightFraction Right inset in fraction (range: [0, 1)) of the inset content bounds.
       * @param insetBottomFraction Bottom inset in fraction (range: [0, 1)) of the inset content bounds.
       */
      public InsetDrawable(@Nullable Drawable drawable, float insetLeftFraction,
          float insetTopFraction, float insetRightFraction, float insetBottomFraction) {
          this(new InsetState(null, null), null);
  
          mState.mInsetLeft = new InsetValue(insetLeftFraction, 0);
          mState.mInsetTop = new InsetValue(insetTopFraction, 0);
          mState.mInsetRight = new InsetValue(insetRightFraction, 0);
          mState.mInsetBottom = new InsetValue(insetBottomFraction, 0);
  
          setDrawable(drawable);
      }
  
      @Override
      public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
              @NonNull AttributeSet attrs, @Nullable Theme theme)
              throws XmlPullParserException, IOException {
          final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.InsetDrawable);
  
          // Inflation will advance the XmlPullParser and AttributeSet.
          super.inflate(r, parser, attrs, theme);
  
          updateStateFromTypedArray(a);
          verifyRequiredAttributes(a);
          a.recycle();
      }
  
      @Override
      public void applyTheme(@NonNull Theme t) {
          super.applyTheme(t);
  
          final InsetState state = mState;
          if (state == null) {
              return;
          }
  
          if (state.mThemeAttrs != null) {
              final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.InsetDrawable);
              try {
                  updateStateFromTypedArray(a);
                  verifyRequiredAttributes(a);
              } catch (XmlPullParserException e) {
                  rethrowAsRuntimeException(e);
              } finally {
                  a.recycle();
              }
          }
      }
  
      private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
          // If we're not waiting on a theme, verify required attributes.
          if (getDrawable() == null && (mState.mThemeAttrs == null
                  || mState.mThemeAttrs[R.styleable.InsetDrawable_drawable] == 0)) {
              throw new XmlPullParserException(a.getPositionDescription()
                      + ": <inset> tag requires a 'drawable' attribute or "
                      + "child tag defining a drawable");
          }
      }
  
      private void updateStateFromTypedArray(@NonNull TypedArray a) {
          final InsetState state = mState;
          if (state == null) {
              return;
          }
  
          // Account for any configuration changes.
          state.mChangingConfigurations |= a.getChangingConfigurations();
  
          // Extract the theme attributes, if any.
          state.mThemeAttrs = a.extractThemeAttrs();
  
          // Inset attribute may be overridden by more specific attributes.
          if (a.hasValue(R.styleable.InsetDrawable_inset)) {
              final InsetValue inset = getInset(a, R.styleable.InsetDrawable_inset, new InsetValue());
              state.mInsetLeft = inset;
              state.mInsetTop = inset;
              state.mInsetRight = inset;
              state.mInsetBottom = inset;
          }
          state.mInsetLeft = getInset(a, R.styleable.InsetDrawable_insetLeft, state.mInsetLeft);
          state.mInsetTop = getInset(a, R.styleable.InsetDrawable_insetTop, state.mInsetTop);
          state.mInsetRight = getInset(a, R.styleable.InsetDrawable_insetRight, state.mInsetRight);
          state.mInsetBottom = getInset(a, R.styleable.InsetDrawable_insetBottom, state.mInsetBottom);
      }
  
      private InsetValue getInset(@NonNull TypedArray a, int index, InsetValue defaultValue) {
          if (a.hasValue(index)) {
              TypedValue tv = a.peekValue(index);
              if (tv.type == TypedValue.TYPE_FRACTION) {
                  float f = tv.getFraction(1.0f, 1.0f);
                  if (f >= 1f) {
                      throw new IllegalStateException("Fraction cannot be larger than 1");
                  }
                  return new InsetValue(f, 0);
              } else {
                  int dimension = a.getDimensionPixelOffset(index, 0);
                  if (dimension != 0) {
                      return new InsetValue(0, dimension);
                  }
              }
          }
          return defaultValue;
      }
  
      private void getInsets(Rect out) {
          final Rect b = getBounds();
          out.left = mState.mInsetLeft.getDimension(b.width());
          out.right = mState.mInsetRight.getDimension(b.width());
          out.top = mState.mInsetTop.getDimension(b.height());
          out.bottom = mState.mInsetBottom.getDimension(b.height());
      }
  
      @Override
      public boolean getPadding(Rect padding) {
          final boolean pad = super.getPadding(padding);
          getInsets(mTmpInsetRect);
          padding.left += mTmpInsetRect.left;
          padding.right += mTmpInsetRect.right;
          padding.top += mTmpInsetRect.top;
          padding.bottom += mTmpInsetRect.bottom;
  
          return pad || (mTmpInsetRect.left | mTmpInsetRect.right
                  | mTmpInsetRect.top | mTmpInsetRect.bottom) != 0;
      }
  
      @Override
      public Insets getOpticalInsets() {
          final Insets contentInsets = super.getOpticalInsets();
          getInsets(mTmpInsetRect);
          return Insets.of(
                  contentInsets.left + mTmpInsetRect.left,
                  contentInsets.top + mTmpInsetRect.top,
                  contentInsets.right + mTmpInsetRect.right,
                  contentInsets.bottom + mTmpInsetRect.bottom);
      }
  
      @Override
      public int getOpacity() {
          final InsetState state = mState;
          final int opacity = getDrawable().getOpacity();
          getInsets(mTmpInsetRect);
          if (opacity == PixelFormat.OPAQUE &&
              (mTmpInsetRect.left > 0 || mTmpInsetRect.top > 0 || mTmpInsetRect.right > 0
                  || mTmpInsetRect.bottom > 0)) {
              return PixelFormat.TRANSLUCENT;
          }
          return opacity;
      }
  
      @Override
      protected void onBoundsChange(Rect bounds) {
          final Rect r = mTmpRect;
          r.set(bounds);
  
          r.left += mState.mInsetLeft.getDimension(bounds.width());
          r.top += mState.mInsetTop.getDimension(bounds.height());
          r.right -= mState.mInsetRight.getDimension(bounds.width());
          r.bottom -= mState.mInsetBottom.getDimension(bounds.height());
  
          // Apply inset bounds to the wrapped drawable.
          super.onBoundsChange(r);
      }
  
      @Override
      public int getIntrinsicWidth() {
          final int childWidth = getDrawable().getIntrinsicWidth();
          final float fraction = mState.mInsetLeft.mFraction + mState.mInsetRight.mFraction;
          if (childWidth < 0 || fraction >= 1) {
              return -1;
          }
          return (int) (childWidth / (1 - fraction)) + mState.mInsetLeft.mDimension
              + mState.mInsetRight.mDimension;
      }
  
      @Override
      public int getIntrinsicHeight() {
          final int childHeight = getDrawable().getIntrinsicHeight();
          final float fraction = mState.mInsetTop.mFraction + mState.mInsetBottom.mFraction;
          if (childHeight < 0 || fraction >= 1) {
              return -1;
          }
          return (int) (childHeight / (1 - fraction)) + mState.mInsetTop.mDimension
              + mState.mInsetBottom.mDimension;
      }
  
      @Override
      public void getOutline(@NonNull Outline outline) {
          getDrawable().getOutline(outline);
      }
  
      @Override
      DrawableWrapperState mutateConstantState() {
          mState = new InsetState(mState, null);
          return mState;
      }
  
      static final class InsetState extends DrawableWrapper.DrawableWrapperState {
          private int[] mThemeAttrs;
  
          InsetValue mInsetLeft;
          InsetValue mInsetTop;
          InsetValue mInsetRight;
          InsetValue mInsetBottom;
  
          InsetState(@Nullable InsetState orig, @Nullable Resources res) {
              super(orig, res);
  
              if (orig != null) {
                  mInsetLeft = orig.mInsetLeft.clone();
                  mInsetTop = orig.mInsetTop.clone();
                  mInsetRight = orig.mInsetRight.clone();
                  mInsetBottom = orig.mInsetBottom.clone();
  
                  if (orig.mDensity != mDensity) {
                      applyDensityScaling(orig.mDensity, mDensity);
                  }
              } else {
                  mInsetLeft = new InsetValue();
                  mInsetTop = new InsetValue();
                  mInsetRight = new InsetValue();
                  mInsetBottom = new InsetValue();
              }
          }
  
          @Override
          void onDensityChanged(int sourceDensity, int targetDensity) {
              super.onDensityChanged(sourceDensity, targetDensity);
  
              applyDensityScaling(sourceDensity, targetDensity);
          }
  
          /**
           * Called when the constant state density changes to scale
           * density-dependent properties specific to insets.
           *
           * @param sourceDensity the previous constant state density
           * @param targetDensity the new constant state density
           */
          private void applyDensityScaling(int sourceDensity, int targetDensity) {
              mInsetLeft.scaleFromDensity(sourceDensity, targetDensity);
              mInsetTop.scaleFromDensity(sourceDensity, targetDensity);
              mInsetRight.scaleFromDensity(sourceDensity, targetDensity);
              mInsetBottom.scaleFromDensity(sourceDensity, targetDensity);
          }
  
          @Override
          public Drawable newDrawable(@Nullable Resources res) {
              // If this drawable is being created for a different density,
              // just create a new constant state and call it a day.
              final InsetState state;
              if (res != null) {
                  final int densityDpi = res.getDisplayMetrics().densityDpi;
                  final int density = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
                  if (density != mDensity) {
                      state = new InsetState(this, res);
                  } else {
                      state = this;
                  }
              } else {
                  state = this;
              }
  
              return new InsetDrawable(state, res);
          }
      }
  
      static final class InsetValue implements Cloneable {
          final float mFraction;
          int mDimension;
  
          public InsetValue() {
              this(0f, 0);
          }
  
          public InsetValue(float fraction, int dimension) {
              mFraction = fraction;
              mDimension = dimension;
          }
          int getDimension(int boundSize) {
              return (int) (boundSize * mFraction) + mDimension;
          }
  
          void scaleFromDensity(int sourceDensity, int targetDensity) {
              if (mDimension != 0) {
                  mDimension = Bitmap.scaleFromDensity(mDimension, sourceDensity, targetDensity);
              }
          }
  
          @Override
          public InsetValue clone() {
              return new InsetValue(mFraction, mDimension);
          }
      }
  
      /**
       * The one constructor to rule them all. This is called by all public
       * constructors to set the state and initialize local properties.
       */
      private InsetDrawable(@NonNull InsetState state, @Nullable Resources res) {
          super(state, res);
  
          mState = state;
      }
  }
  
  