content: /*
   * Copyright (C) 2015 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 dev.dworks.apps.anexplorer.ui;
  
  import android.animation.ArgbEvaluator;
  import android.animation.ValueAnimator;
  import android.animation.ValueAnimator.AnimatorUpdateListener;
  import android.annotation.TargetApi;
  import android.content.Context;
  import android.content.res.ColorStateList;
  import android.content.res.TypedArray;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Paint;
  import android.graphics.Paint.Style;
  import android.graphics.RadialGradient;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.graphics.Shader;
  import android.graphics.drawable.Drawable;
  import android.os.Build;
  import android.util.AttributeSet;
  import android.view.View;
  
  import java.util.Objects;
  
  import dev.dworks.apps.anexplorer.R;
  
  /**
   * An image view surrounded by a circle.
   */
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public class CircledImageView extends View {
  
      private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator();
  
      private Drawable mDrawable;
  
      private final RectF mOval;
      private final Paint mPaint;
  
      private ColorStateList mCircleColor;
  
      private float mCircleRadius;
      private float mCircleRadiusPercent;
  
      private float mCircleRadiusPressed;
      private float mCircleRadiusPressedPercent;
  
      private float mRadiusInset;
  
      private int mCircleBorderColor;
  
      private float mCircleBorderWidth;
      private float mProgress = 1f;
      private final float mShadowWidth;
  
      private float mShadowVisibility;
      private boolean mCircleHidden = false;
  
      private float mInitialCircleRadius;
  
      private boolean mPressed = false;
  
      private boolean mProgressIndeterminate;
      private ProgressDrawable mIndeterminateDrawable;
      private Rect mIndeterminateBounds = new Rect();
      private long mColorChangeAnimationDurationMs = 0;
  
      private float mImageCirclePercentage = 1f;
      private float mImageHorizontalOffcenterPercentage = 0f;
      private Integer mImageTint;
  
      private final Drawable.Callback mDrawableCallback = new Drawable.Callback() {
          @Override
          public void invalidateDrawable(Drawable drawable) {
              invalidate();
          }
  
          @Override
          public void scheduleDrawable(Drawable drawable, Runnable runnable, long l) {
              // Not needed.
          }
  
          @Override
          public void unscheduleDrawable(Drawable drawable, Runnable runnable) {
              // Not needed.
          }
      };
  
      private int mCurrentColor;
  
      private final AnimatorUpdateListener mAnimationListener = new AnimatorUpdateListener() {
          @Override
          public void onAnimationUpdate(ValueAnimator animation) {
              int color = (int) animation.getAnimatedValue();
              if (color != CircledImageView.this.mCurrentColor) {
                  CircledImageView.this.mCurrentColor = color;
                  CircledImageView.this.invalidate();
              }
          }
      };
  
      private ValueAnimator mColorAnimator;
  
      public CircledImageView(Context context) {
          this(context, null);
      }
  
      public CircledImageView(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public CircledImageView(Context context, AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
  
          TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CircledImageView);
          mDrawable = a.getDrawable(R.styleable.CircledImageView_android_src);
  
          mCircleColor = a.getColorStateList(R.styleable.CircledImageView_circle_color);
          if (mCircleColor == null) {
              mCircleColor = ColorStateList.valueOf(context.getColor(android.R.color.darker_gray));
          }
  
          mCircleRadius = a.getDimension(
                  R.styleable.CircledImageView_circle_radius, 0);
          mInitialCircleRadius = mCircleRadius;
          mCircleRadiusPressed = a.getDimension(
                  R.styleable.CircledImageView_circle_radius_pressed, mCircleRadius);
          mCircleBorderColor = a.getColor(
                  R.styleable.CircledImageView_circle_border_color, Color.BLACK);
          mCircleBorderWidth = a.getDimension(R.styleable.CircledImageView_circle_border_width, 0);
  
          if (mCircleBorderWidth > 0) {
              mRadiusInset += mCircleBorderWidth;
          }
  
          float circlePadding = a.getDimension(R.styleable.CircledImageView_circle_padding, 0);
          if (circlePadding > 0) {
              mRadiusInset += circlePadding;
          }
          mShadowWidth = a.getDimension(R.styleable.CircledImageView_shadow_width, 0);
  
          mImageCirclePercentage = a.getFloat(
                  R.styleable.CircledImageView_image_circle_percentage, 0f);
  
          mImageHorizontalOffcenterPercentage = a.getFloat(
                  R.styleable.CircledImageView_image_horizontal_offcenter_percentage, 0f);
  
          if (a.hasValue(R.styleable.CircledImageView_image_tint)) {
              mImageTint = a.getColor(R.styleable.CircledImageView_image_tint, 0);
          }
  
          mCircleRadiusPercent = a.getFraction(R.styleable.CircledImageView_circle_radius_percent,
                  1, 1, 0f);
  
          mCircleRadiusPressedPercent = a.getFraction(
                  R.styleable.CircledImageView_circle_radius_pressed_percent, 1, 1,
                  mCircleRadiusPercent);
  
          a.recycle();
  
          mOval = new RectF();
          mPaint = new Paint();
          mPaint.setAntiAlias(true);
  
          mIndeterminateDrawable = new ProgressDrawable();
          // {@link #mDrawableCallback} must be retained as a member, as Drawable callback
          // is held by weak reference, we must retain it for it to continue to be called.
          mIndeterminateDrawable.setCallback(mDrawableCallback);
  
          setWillNotDraw(false);
  
          setColorForCurrentState();
      }
  
      public void setCircleHidden(boolean circleHidden) {
          if (circleHidden != mCircleHidden) {
              mCircleHidden = circleHidden;
              invalidate();
          }
      }
  
  
      @Override
      protected boolean onSetAlpha(int alpha) {
          return true;
      }
  
      @Override
      protected void onDraw(Canvas canvas) {
          int paddingLeft = getPaddingLeft();
          int paddingTop = getPaddingTop();
  
  
          float circleRadius = mPressed ? getCircleRadiusPressed() : getCircleRadius();
          if (mShadowWidth > 0 && mShadowVisibility > 0) {
              // First let's find the center of the view.
              mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(),
                      getHeight() - getPaddingBottom());
              // Having the center, lets make the shadow start beyond the circled and possibly the
              // border.
              final float radius = circleRadius + mCircleBorderWidth +
                      mShadowWidth * mShadowVisibility;
              mPaint.setColor(Color.BLACK);
              mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha()));
              mPaint.setStyle(Style.FILL);
              // TODO: precalc and pre-allocate this
              mPaint.setShader(new RadialGradient(mOval.centerX(), mOval.centerY(), radius,
                      new int[]{Color.BLACK, Color.TRANSPARENT}, new float[]{0.6f, 1f},
                      Shader.TileMode.MIRROR));
              canvas.drawCircle(mOval.centerX(), mOval.centerY(), radius, mPaint);
              mPaint.setShader(null);
          }
          if (mCircleBorderWidth > 0) {
              // First let's find the center of the view.
              mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(),
                      getHeight() - getPaddingBottom());
              // Having the center, lets make the border meet the circle.
              mOval.set(mOval.centerX() - circleRadius, mOval.centerY() - circleRadius,
                      mOval.centerX() + circleRadius, mOval.centerY() + circleRadius);
              mPaint.setColor(mCircleBorderColor);
              // {@link #Paint.setAlpha} is a helper method that just sets the alpha portion of the
              // color. {@link #Paint.setPaint} will clear any previously set alpha value.
              mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha()));
              mPaint.setStyle(Style.STROKE);
              mPaint.setStrokeWidth(mCircleBorderWidth);
  
              if (mProgressIndeterminate) {
                  mOval.roundOut(mIndeterminateBounds);
                  mIndeterminateDrawable.setBounds(mIndeterminateBounds);
                  mIndeterminateDrawable.setRingColor(mCircleBorderColor);
                  mIndeterminateDrawable.setRingWidth(mCircleBorderWidth);
                  mIndeterminateDrawable.draw(canvas);
              } else {
                  canvas.drawArc(mOval, -90, 360 * mProgress, false, mPaint);
              }
          }
          if (!mCircleHidden) {
              mOval.set(paddingLeft, paddingTop, getWidth() - getPaddingRight(),
                      getHeight() - getPaddingBottom());
              // {@link #Paint.setAlpha} is a helper method that just sets the alpha portion of the
              // color. {@link #Paint.setPaint} will clear any previously set alpha value.
              mPaint.setColor(mCurrentColor);
              mPaint.setAlpha(Math.round(mPaint.getAlpha() * getAlpha()));
  
              mPaint.setStyle(Style.FILL);
              float centerX = mOval.centerX();
              float centerY = mOval.centerY();
  
              canvas.drawCircle(centerX, centerY, circleRadius, mPaint);
          }
  
          if (mDrawable != null) {
              mDrawable.setAlpha(Math.round(getAlpha() * 255));
  
              if (mImageTint != null) {
                  mDrawable.setTint(mImageTint);
              }
              mDrawable.draw(canvas);
          }
  
          super.onDraw(canvas);
      }
  
      private void setColorForCurrentState() {
          int newColor = mCircleColor.getColorForState(getDrawableState(),
                  mCircleColor.getDefaultColor());
          if (mColorChangeAnimationDurationMs > 0) {
              if (mColorAnimator != null) {
                  mColorAnimator.cancel();
              } else {
                  mColorAnimator = new ValueAnimator();
              }
              mColorAnimator.setIntValues(new int[] {
                      mCurrentColor, newColor });
              mColorAnimator.setEvaluator(ARGB_EVALUATOR);
              mColorAnimator.setDuration(mColorChangeAnimationDurationMs);
              mColorAnimator.addUpdateListener(this.mAnimationListener);
              mColorAnimator.start();
          } else {
              if (newColor != mCurrentColor) {
                  mCurrentColor = newColor;
                  invalidate();
              }
          }
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  
          final float radius = getCircleRadius() + mCircleBorderWidth +
                  mShadowWidth * mShadowVisibility;
          float desiredWidth = radius * 2;
          float desiredHeight = radius * 2;
  
          int widthMode = MeasureSpec.getMode(widthMeasureSpec);
          int widthSize = MeasureSpec.getSize(widthMeasureSpec);
          int heightMode = MeasureSpec.getMode(heightMeasureSpec);
          int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  
          int width;
          int height;
  
          if (widthMode == MeasureSpec.EXACTLY) {
              width = widthSize;
          } else if (widthMode == MeasureSpec.AT_MOST) {
              width = (int) Math.min(desiredWidth, widthSize);
          } else {
              width = (int) desiredWidth;
          }
  
          if (heightMode == MeasureSpec.EXACTLY) {
              height = heightSize;
          } else if (heightMode == MeasureSpec.AT_MOST) {
              height = (int) Math.min(desiredHeight, heightSize);
          } else {
              height = (int) desiredHeight;
          }
  
          super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                  MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          if (mDrawable != null) {
              // Retrieve the sizes of the drawable and the view.
              final int nativeDrawableWidth = mDrawable.getIntrinsicWidth();
              final int nativeDrawableHeight = mDrawable.getIntrinsicHeight();
              final int viewWidth = getMeasuredWidth();
              final int viewHeight = getMeasuredHeight();
              final float imageCirclePercentage = mImageCirclePercentage > 0
                      ? mImageCirclePercentage : 1;
  
              final float scaleFactor = Math.min(1f,
                      Math.min(
                              (float) nativeDrawableWidth != 0
                                      ? imageCirclePercentage * viewWidth / nativeDrawableWidth : 1,
                              (float) nativeDrawableHeight != 0
                                      ? imageCirclePercentage
                                          * viewHeight / nativeDrawableHeight : 1));
  
              // Scale the drawable down to fit the view, if needed.
              final int drawableWidth = Math.round(scaleFactor * nativeDrawableWidth);
              final int drawableHeight = Math.round(scaleFactor * nativeDrawableHeight);
  
              // Center the drawable within the view.
              final int drawableLeft = (viewWidth - drawableWidth) / 2
                      + Math.round(mImageHorizontalOffcenterPercentage * drawableWidth);
              final int drawableTop = (viewHeight - drawableHeight) / 2;
  
              mDrawable.setBounds(drawableLeft, drawableTop, drawableLeft + drawableWidth,
                      drawableTop + drawableHeight);
          }
  
          super.onLayout(changed, left, top, right, bottom);
      }
  
      public void setImageDrawable(Drawable drawable) {
          if (drawable != mDrawable) {
              final Drawable existingDrawable = mDrawable;
              mDrawable = drawable;
  
              final boolean skipLayout = drawable != null
                      && existingDrawable != null
                      && existingDrawable.getIntrinsicHeight() == drawable.getIntrinsicHeight()
                      && existingDrawable.getIntrinsicWidth() == drawable.getIntrinsicWidth();
  
              if (skipLayout) {
                  mDrawable.setBounds(existingDrawable.getBounds());
              } else {
                  requestLayout();
              }
  
              invalidate();
          }
      }
  
      public void setImageResource(int resId) {
          setImageDrawable(resId == 0 ? null : getContext().getDrawable(resId));
      }
  
      public void setImageCirclePercentage(float percentage) {
          float clamped = Math.max(0, Math.min(1, percentage));
          if (clamped != mImageCirclePercentage) {
              mImageCirclePercentage = clamped;
              invalidate();
          }
      }
  
      public void setImageHorizontalOffcenterPercentage(float percentage) {
          if (percentage != mImageHorizontalOffcenterPercentage) {
              mImageHorizontalOffcenterPercentage = percentage;
              invalidate();
          }
      }
  
      public void setImageTint(int tint) {
          if (tint != mImageTint) {
              mImageTint = tint;
              invalidate();
          }
      }
  
      public float getCircleRadius() {
          float radius = mCircleRadius;
          if (mCircleRadius <= 0 && mCircleRadiusPercent > 0) {
              radius = Math.max(getMeasuredHeight(), getMeasuredWidth()) * mCircleRadiusPercent;
          }
  
          return radius - mRadiusInset;
      }
  
      public float getCircleRadiusPercent() {
          return mCircleRadiusPercent;
      }
  
      public float getCircleRadiusPressed() {
          float radius = mCircleRadiusPressed;
  
          if (mCircleRadiusPressed <= 0 && mCircleRadiusPressedPercent > 0) {
              radius = Math.max(getMeasuredHeight(), getMeasuredWidth())
                      * mCircleRadiusPressedPercent;
          }
  
          return radius - mRadiusInset;
      }
  
      public float getCircleRadiusPressedPercent() {
          return mCircleRadiusPressedPercent;
      }
  
      public void setCircleRadius(float circleRadius) {
          if (circleRadius != mCircleRadius) {
              mCircleRadius = circleRadius;
              invalidate();
          }
      }
  
      /**
       * Sets the radius of the circle to be a percentage of the largest dimension of the view.
       * @param circleRadiusPercent A {@code float} from 0 to 1 representing the radius percentage.
       */
      public void setCircleRadiusPercent(float circleRadiusPercent) {
          if (circleRadiusPercent != mCircleRadiusPercent) {
              mCircleRadiusPercent = circleRadiusPercent;
              invalidate();
          }
      }
  
      public void setCircleRadiusPressed(float circleRadiusPressed) {
          if (circleRadiusPressed != mCircleRadiusPressed) {
              mCircleRadiusPressed = circleRadiusPressed;
              invalidate();
          }
      }
  
      /**
       * Sets the radius of the circle to be a percentage of the largest dimension of the view when
       * pressed.
       * @param circleRadiusPressedPercent A {@code float} from 0 to 1 representing the radius
       *                                   percentage.
       */
      public void setCircleRadiusPressedPercent(float circleRadiusPressedPercent) {
          if (circleRadiusPressedPercent  != mCircleRadiusPressedPercent) {
              mCircleRadiusPressedPercent = circleRadiusPressedPercent;
              invalidate();
          }
      }
  
      @Override
      protected void drawableStateChanged() {
          super.drawableStateChanged();
          setColorForCurrentState();
      }
  
      public void setCircleColor(int circleColor) {
          setCircleColorStateList(ColorStateList.valueOf(circleColor));
      }
  
      public void setCircleColorStateList(ColorStateList circleColor) {
          if (!Objects.equals(circleColor, mCircleColor)) {
              mCircleColor = circleColor;
              setColorForCurrentState();
              invalidate();
          }
      }
  
      public ColorStateList getCircleColorStateList() {
          return mCircleColor;
      }
  
      public int getDefaultCircleColor() {
          return mCircleColor.getDefaultColor();
      }
  
      /**
       * Show the circle border as an indeterminate progress spinner.
       * The views circle border width and color must be set for this to have an effect.
       *
       * @param show true if the progress spinner is shown, false to hide it.
       */
      public void showIndeterminateProgress(boolean show) {
          mProgressIndeterminate = show;
          if (show) {
              mIndeterminateDrawable.startAnimation();
          } else {
              mIndeterminateDrawable.stopAnimation();
          }
      }
  
      @Override
      protected void onVisibilityChanged(View changedView, int visibility) {
          super.onVisibilityChanged(changedView, visibility);
          if (visibility != View.VISIBLE) {
              showIndeterminateProgress(false);
          } else if (mProgressIndeterminate) {
              showIndeterminateProgress(true);
          }
      }
  
      public void setProgress(float progress) {
          if (progress != mProgress) {
              mProgress = progress;
              invalidate();
          }
      }
  
      /**
       * Set how much of the shadow should be shown.
       * @param shadowVisibility Value between 0 and 1.
       */
      public void setShadowVisibility(float shadowVisibility) {
          if (shadowVisibility != mShadowVisibility) {
              mShadowVisibility = shadowVisibility;
              invalidate();
          }
      }
  
      public float getInitialCircleRadius() {
          return mInitialCircleRadius;
      }
  
      public void setCircleBorderColor(int circleBorderColor) {
          mCircleBorderColor = circleBorderColor;
      }
  
      /**
       * Set the border around the circle.
       * @param circleBorderWidth Width of the border around the circle.
       */
      public void setCircleBorderWidth(float circleBorderWidth) {
          if (circleBorderWidth != mCircleBorderWidth) {
              mCircleBorderWidth = circleBorderWidth;
              invalidate();
          }
      }
  
      @Override
      public void setPressed(boolean pressed) {
          super.setPressed(pressed);
          if (pressed != mPressed) {
              mPressed = pressed;
              invalidate();
          }
      }
  
      public Drawable getImageDrawable() {
          return mDrawable;
      }
  
      /**
       * @return the milliseconds duration of the transition animation when the color changes.
       */
      public long getColorChangeAnimationDuration() {
          return mColorChangeAnimationDurationMs;
      }
  
      /**
       * @param mColorChangeAnimationDurationMs the milliseconds duration of the color change
       *            animation. The color change animation will run if the color changes with {@link #setCircleColor}
       *            or as a result of the active state changing.
       */
      public void setColorChangeAnimationDuration(long mColorChangeAnimationDurationMs) {
          this.mColorChangeAnimationDurationMs = mColorChangeAnimationDurationMs;
      }
  }