diff --git a/core/src/main/java/icyllis/modernui/graphics/drawable/Drawable.java b/core/src/main/java/icyllis/modernui/graphics/drawable/Drawable.java index 68cb25fd..82500ca5 100644 --- a/core/src/main/java/icyllis/modernui/graphics/drawable/Drawable.java +++ b/core/src/main/java/icyllis/modernui/graphics/drawable/Drawable.java @@ -28,6 +28,7 @@ import icyllis.modernui.view.View; import icyllis.modernui.widget.ImageView; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.UnmodifiableView; import java.lang.ref.WeakReference; import java.util.Arrays; @@ -193,6 +194,7 @@ public final Rect copyBounds() { * @see #copyBounds() * @see #copyBounds(Rect) */ + @UnmodifiableView @NonNull public final Rect getBounds() { if (mBounds == ZERO_BOUNDS_RECT) { @@ -391,7 +393,7 @@ protected boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDir * Specify an alpha value for the drawable. 0 means fully transparent, and * 255 means fully opaque. */ - public void setAlpha(int alpha) { + public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { } /** @@ -409,8 +411,16 @@ public int getAlpha() { /** * Specifies tint color for this drawable. *
+ * A Drawable's drawing content will be blended together with its tint + * before it is drawn to the screen. + *
+ ** To clear the tint, pass {@code null} to * {@link #setTintList(ColorStateList)}. + *
+ *Note: Setting a color filter via + * {@link #setColorFilter(ColorFilter)} overrides tint. + *
* * @param tintColor Color to use for tinting this drawable * @see #setTintList(ColorStateList) @@ -422,6 +432,13 @@ public void setTint(@ColorInt int tintColor) { /** * Specifies tint color for this drawable as a color state list. + *+ * A Drawable's drawing content will be blended together with its tint + * before it is drawn to the screen. + *
+ *Note: Setting a color filter via + * {@link #setColorFilter(ColorFilter)} overrides tint. + *
* * @param tint Color state list to use for tinting this drawable, or * {@code null} to clear the tint @@ -436,14 +453,49 @@ public void setTintList(@Nullable ColorStateList tint) { ** Defines how this drawable's tint color should be blended into the drawable * before it is drawn to screen. Default tint mode is {@link BlendMode#SRC_IN}. + *
+ *Note: Setting a color filter via + * {@link #setColorFilter(ColorFilter)} + *
* - * @param blendMode BlendMode to apply to the drawable + * @param blendMode BlendMode to apply to the drawable, a value of null sets the default + * blend mode value of {@link BlendMode#SRC_IN} * @see #setTint(int) * @see #setTintList(ColorStateList) */ public void setTintBlendMode(@NonNull BlendMode blendMode) { } + /** + * Specify an optional color filter for the drawable. + *+ * If a Drawable has a ColorFilter, each output pixel of the Drawable's + * drawing contents will be modified by the color filter before it is + * blended onto the render target of a Canvas. + *
+ *+ * Pass {@code null} to remove any existing color filter. + *
+ *Note: Setting a non-{@code null} color + * filter disables {@link #setTintList(ColorStateList) tint}. + *
+ * + * @param colorFilter The color filter to apply, or {@code null} to remove the + * existing color filter + */ + public void setColorFilter(@Nullable ColorFilter colorFilter) { + } + + /** + * Returns the current color filter, or {@code null} if none set. + * + * @return the current color filter, or {@code null} if none set + */ + @Nullable + public ColorFilter getColorFilter() { + return null; + } + /** * Specifies the hotspot's location within the drawable. * diff --git a/core/src/main/java/icyllis/modernui/graphics/drawable/ImageDrawable.java b/core/src/main/java/icyllis/modernui/graphics/drawable/ImageDrawable.java index b5cb45c6..5d257c44 100644 --- a/core/src/main/java/icyllis/modernui/graphics/drawable/ImageDrawable.java +++ b/core/src/main/java/icyllis/modernui/graphics/drawable/ImageDrawable.java @@ -99,11 +99,16 @@ public final Image getImage() { } /** - * Switch to a new Image object. + * Switch to a new Image object. Calling this method will also reset + * the subset to the full image, see {@link #setSrcRect(Rect)}. */ public void setImage(@Nullable Image image) { if (mImageState.mImage != image) { mImageState.mImage = image; + if (mSrcRect != null && image != null) { + mSrcRect.set(0, 0, image.getWidth(), image.getWidth()); + } + mFullImage = true; invalidateSelf(); } } @@ -134,43 +139,83 @@ public void setGravity(int gravity) { * Specifies the subset of the image to draw. To draw the full image, * call {@link #setSrcRect(Rect)} with null. *- * Caveat: this method is marked experimental since 3.11 and may be redesigned - * in the future releases. + * Calling this method when there's no image has no effect. Next call + * to {@link #setImage(Image)} will reset the subset to the full image. */ - @ApiStatus.Experimental public void setSrcRect(int left, int top, int right, int bottom) { - if (mSrcRect == null) { - mSrcRect = new Rect(left, top, right, bottom); - invalidateSelf(); + final Image image = mImageState.mImage; + if (image == null) { + return; + } + int imageWidth = image.getWidth(); + int imageHeight = image.getHeight(); + if (left <= 0 && top <= 0 && + right >= imageWidth && bottom >= imageHeight) { + if (!mFullImage) { + invalidateSelf(); + } + mFullImage = true; } else { - Rect oldBounds = mSrcRect; - if (oldBounds.left != left || oldBounds.top != top || - oldBounds.right != right || oldBounds.bottom != bottom) { - mSrcRect.set(left, top, right, bottom); + if (mSrcRect == null) { + mSrcRect = new Rect(0, 0, imageWidth, imageHeight); + if (!mSrcRect.intersect(left, top, right, bottom)) { + mSrcRect.setEmpty(); + } invalidateSelf(); + } else { + Rect oldBounds = mSrcRect; + if (oldBounds.left != left || oldBounds.top != top || + oldBounds.right != right || oldBounds.bottom != bottom) { + mSrcRect.set(0, 0, imageWidth, imageHeight); + if (!mSrcRect.intersect(left, top, right, bottom)) { + mSrcRect.setEmpty(); + } + invalidateSelf(); + } } + mFullImage = false; } - mFullImage = false; } /** * Specifies the subset of the image to draw. Null for the full image. + *
+ * Calling this method when there's no image has no effect. Next call
+ * to {@link #setImage(Image)} will reset the subset to the full image.
*
* @param srcRect the subset of the image
*/
- @ApiStatus.Experimental
public void setSrcRect(@Nullable Rect srcRect) {
- if (srcRect == null) {
+ final Image image = mImageState.mImage;
+ if (image == null) {
+ return;
+ }
+ int imageWidth = image.getWidth();
+ int imageHeight = image.getHeight();
+ if (srcRect == null || (srcRect.left <= 0 && srcRect.top <= 0 &&
+ srcRect.right >= imageWidth && srcRect.bottom >= imageHeight)) {
+ if (!mFullImage) {
+ invalidateSelf();
+ }
mFullImage = true;
} else {
if (mSrcRect == null) {
- mSrcRect = srcRect.copy();
+ mSrcRect = new Rect(0, 0, imageWidth, imageHeight);
+ if (!mSrcRect.intersect(srcRect)) {
+ mSrcRect.setEmpty();
+ }
+ invalidateSelf();
} else {
- mSrcRect.set(srcRect);
+ if (!mSrcRect.equals(srcRect)) {
+ mSrcRect.set(0, 0, imageWidth, imageHeight);
+ if (!mSrcRect.intersect(srcRect)) {
+ mSrcRect.setEmpty();
+ }
+ invalidateSelf();
+ }
}
mFullImage = false;
}
- invalidateSelf();
}
/**
@@ -181,6 +226,7 @@ public void setSrcRect(@Nullable Rect srcRect) {
* @param mipmap True if the image should use mipmaps, false otherwise.
* @see #hasMipmap()
*/
+ @ApiStatus.Experimental
public void setMipmap(boolean mipmap) {
}
@@ -191,6 +237,7 @@ public void setMipmap(boolean mipmap) {
* is null, this method always returns false.
* @see #setMipmap(boolean)
*/
+ @ApiStatus.Experimental
public boolean hasMipmap() {
return true;
}
@@ -503,6 +550,18 @@ public void setTintBlendMode(@NonNull BlendMode blendMode) {
}
}
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ mImageState.mPaint.setColorFilter(colorFilter);
+ invalidateSelf();
+ }
+
+ @Nullable
+ @Override
+ public ColorFilter getColorFilter() {
+ return mImageState.mPaint.getColorFilter();
+ }
+
/**
* A mutable ImageDrawable still shares its Image with any other Drawable
* that comes from the same resource.
diff --git a/core/src/main/java/icyllis/modernui/widget/ImageView.java b/core/src/main/java/icyllis/modernui/widget/ImageView.java
index 6a280dca..fdbd0d2a 100644
--- a/core/src/main/java/icyllis/modernui/widget/ImageView.java
+++ b/core/src/main/java/icyllis/modernui/widget/ImageView.java
@@ -18,18 +18,15 @@
package icyllis.modernui.widget;
+import icyllis.modernui.annotation.NonNull;
+import icyllis.modernui.annotation.Nullable;
import icyllis.modernui.core.Context;
import icyllis.modernui.graphics.*;
-import icyllis.modernui.graphics.drawable.Drawable;
-import icyllis.modernui.graphics.drawable.ImageDrawable;
-import icyllis.modernui.graphics.drawable.LevelListDrawable;
+import icyllis.modernui.graphics.drawable.*;
import icyllis.modernui.util.ColorStateList;
import icyllis.modernui.view.MeasureSpec;
import icyllis.modernui.view.View;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
/**
* Displays image resources, for example {@link icyllis.modernui.graphics.Image}
* or {@link icyllis.modernui.graphics.drawable.Drawable} resources.
@@ -52,16 +49,21 @@ public class ImageView extends View {
private int mMaxHeight = Integer.MAX_VALUE;
// these are applied to the drawable
+ private ColorFilter mImageColorFilter = null;
+ private boolean mHasImageColorFilter = false;
private int mImageAlpha = 255;
private boolean mHasImageAlpha = false;
private Drawable mDrawable = null;
private ImageDrawable mRecycleImageDrawable = null;
private ColorStateList mDrawableTintList = null;
+ private BlendMode mDrawableBlendMode = null;
private boolean mHasDrawableTint = false;
+ private boolean mHasDrawableBlendMode = false;
private int[] mState = null;
private boolean mMergeState = false;
+ private boolean mHasLevelSet = false;
private int mLevel = 0;
private int mDrawableWidth;
private int mDrawableHeight;
@@ -77,7 +79,7 @@ public ImageView(Context context) {
}
@Override
- protected boolean verifyDrawable(@Nonnull Drawable dr) {
+ protected boolean verifyDrawable(@NonNull Drawable dr) {
return mDrawable == dr || super.verifyDrawable(dr);
}
@@ -90,7 +92,7 @@ public void jumpDrawablesToCurrentState() {
}
@Override
- public void invalidateDrawable(@Nonnull Drawable dr) {
+ public void invalidateDrawable(@NonNull Drawable dr) {
if (dr == mDrawable) {
// update cached drawable dimensions if they've changed
final int w = dr.getIntrinsicWidth();
@@ -246,7 +248,7 @@ public void setImageDrawable(@Nullable Drawable drawable) {
/**
* Applies a tint to the image drawable. Does not modify the current tint
- * mode, which is SRC_IN
by default.
+ * mode, which is {@link BlendMode#SRC_IN} by default.
*
* Subsequent calls to {@link #setImageDrawable(Drawable)} will automatically * mutate the drawable and apply the specified tint and tint mode using @@ -275,14 +277,46 @@ public ColorStateList getImageTintList() { return mDrawableTintList; } + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setImageTintList(ColorStateList)}} to the image drawable. The default + * mode is {@link BlendMode#SRC_IN}. + * + * @param blendMode the blending mode used to apply the tint, may be + * {@code null} to clear tint + * @see #getImageTintBlendMode() + * @see Drawable#setTintBlendMode(BlendMode) + */ + public void setImageTintBlendMode(@Nullable BlendMode blendMode) { + mDrawableBlendMode = blendMode; + mHasDrawableBlendMode = true; + + applyImageTint(); + } + + /** + * Gets the blending mode used to apply the tint to the image Drawable + * + * @return the blending mode used to apply the tint to the image Drawable + * @see #setImageTintBlendMode(BlendMode) + */ + @Nullable + public BlendMode getImageTintBlendMode() { + return mDrawableBlendMode; + } + private void applyImageTint() { - if (mDrawable != null && (mHasDrawableTint)) { + if (mDrawable != null && (mHasDrawableTint || mHasDrawableBlendMode)) { mDrawable = mDrawable.mutate(); if (mHasDrawableTint) { mDrawable.setTintList(mDrawableTintList); } + if (mHasDrawableBlendMode) { + mDrawable.setTintBlendMode(mDrawableBlendMode); + } + // The drawable (or one of its children) may not have been // stateful before applying the tint, so let's try again. if (mDrawable.isStateful()) { @@ -340,6 +374,7 @@ public void setSelected(boolean selected) { */ public void setImageLevel(int level) { mLevel = level; + mHasLevelSet = true; if (mDrawable != null) { mDrawable.setLevel(level); resizeFromDrawable(); @@ -352,7 +387,7 @@ public void setImageLevel(int level) { * * @param scaleType The desired scaling mode. */ - public void setScaleType(@Nonnull ScaleType scaleType) { + public void setScaleType(@NonNull ScaleType scaleType) { if (mScaleType != scaleType) { mScaleType = scaleType; @@ -367,11 +402,26 @@ public void setScaleType(@Nonnull ScaleType scaleType) { * @return The ScaleType used to scale the image. * @see ImageView.ScaleType */ - @Nonnull + @NonNull public ScaleType getScaleType() { return mScaleType; } + /** + * Returns the view's optional matrix. This is applied to the + * view's drawable when it is drawn. If there is no matrix, + * this method will return an identity matrix. + * Do not change this matrix in place but make a copy. + * If you want a different matrix applied to the drawable, + * be sure to call setImageMatrix(). + */ + public Matrix getImageMatrix() { + if (mDrawMatrix == null) { + return new Matrix(); + } + return mDrawMatrix; + } + /** * Adds a transformation {@link Matrix} that is applied * to the view's drawable when it is drawn. Allows custom scaling, @@ -388,11 +438,7 @@ public void setImageMatrix(@Nullable Matrix matrix) { // don't invalidate unless we're actually changing our matrix if (matrix == null && !mMatrix.isIdentity() || matrix != null && !mMatrix.equals(matrix)) { - if (matrix == null) { - mMatrix.setIdentity(); - } else { - mMatrix.set(matrix); - } + mMatrix.set(matrix); configureBounds(); invalidate(); } @@ -422,7 +468,7 @@ public void setCropToPadding(boolean cropToPadding) { } } - @Nonnull + @NonNull @Override public int[] onCreateDrawableState(int extraSpace) { if (mState == null) { @@ -463,10 +509,13 @@ private void updateDrawable(@Nullable Drawable d) { final boolean visible = isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown(); d.setVisible(visible, true); } - d.setLevel(mLevel); + if (mHasLevelSet) { + d.setLevel(mLevel); + } mDrawableWidth = d.getIntrinsicWidth(); mDrawableHeight = d.getIntrinsicHeight(); applyImageTint(); + applyColorFilter(); applyAlpha(); configureBounds(); @@ -624,16 +673,16 @@ private int resolveAdjustedSize(int desiredSize, int maxSize, final int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED -> - // Parent says we can be as big as we want. Just don't be larger - // than max size imposed on ourselves. + // Parent says we can be as big as we want. Just don't be larger + // than max size imposed on ourselves. result = Math.min(desiredSize, maxSize); case MeasureSpec.AT_MOST -> - // Parent says we can be as big as we want, up to specSize. - // Don't be larger than specSize, and don't be larger than - // the max size imposed on ourselves. + // Parent says we can be as big as we want, up to specSize. + // Don't be larger than specSize, and don't be larger than + // the max size imposed on ourselves. result = Math.min(Math.min(desiredSize, specSize), maxSize); case MeasureSpec.EXACTLY -> - // No choice. Do what we are told. + // No choice. Do what we are told. result = specSize; } return result; @@ -699,8 +748,7 @@ private void configureBounds() { dy = (vheight - dheight * scale) * 0.5f; } - mDrawMatrix.setTranslate(Math.round(dx), Math.round(dy)); - mDrawMatrix.preScale(scale, scale); + mDrawMatrix.setScaleTranslate(scale, scale, Math.round(dx), Math.round(dy)); } else if (ScaleType.CENTER_INSIDE == mScaleType) { mDrawMatrix = mMatrix; float scale; @@ -717,13 +765,13 @@ private void configureBounds() { dx = Math.round((vwidth - dwidth * scale) * 0.5f); dy = Math.round((vheight - dheight * scale) * 0.5f); - mDrawMatrix.setTranslate(dx, dy); - mDrawMatrix.preScale(scale, scale); + mDrawMatrix.setScaleTranslate(scale, scale, dx, dy); } else { mDrawMatrix = mMatrix; float tx = 0, sx = (float) vwidth / dwidth; float ty = 0, sy = (float) vheight / dheight; boolean xLarger = false; + if (mScaleType != ScaleType.FIT_XY) { if (sx > sy) { xLarger = true; @@ -753,8 +801,7 @@ private void configureBounds() { } } - mDrawMatrix.setTranslate(tx, ty); - mDrawMatrix.preScale(sx, sy); + mDrawMatrix.setScaleTranslate(sx, sy, tx, ty); } } } @@ -809,7 +856,7 @@ public void animateTransform(@Nullable Matrix matrix) { } @Override - protected void onDraw(@Nonnull Canvas canvas) { + protected void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); if (mDrawable == null) { @@ -897,6 +944,32 @@ public boolean getBaselineAlignBottom() { return mBaselineAlignBottom; } + /** + * Returns the active color filter for this ImageView. + * + * @return the active color filter for this ImageView + * @see #setColorFilter(ColorFilter) + */ + @Nullable + public ColorFilter getColorFilter() { + return mImageColorFilter; + } + + /** + * Apply an arbitrary color filter to the image. + * + * @param colorFilter the color filter to apply (can be null) + * @see #getColorFilter() + */ + public void setColorFilter(@Nullable ColorFilter colorFilter) { + if (mImageColorFilter != colorFilter) { + mImageColorFilter = colorFilter; + mHasImageColorFilter = true; + applyColorFilter(); + invalidate(); + } + } + /** * Returns the alpha that will be applied to the drawable of this ImageView. * @@ -926,6 +999,13 @@ public void setImageAlpha(int alpha) { } } + private void applyColorFilter() { + if (mDrawable != null && mHasImageColorFilter) { + mDrawable = mDrawable.mutate(); + mDrawable.setColorFilter(mImageColorFilter); + } + } + private void applyAlpha() { if (mDrawable != null && mHasImageAlpha) { mDrawable = mDrawable.mutate();