@nativescript-community/ui-image 4.6.6 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +4 -20
- package/index-common.d.ts +47 -18
- package/index-common.js +5 -69
- package/index-common.js.map +1 -1
- package/index.android.d.ts +33 -60
- package/index.android.js +597 -702
- package/index.android.js.map +1 -1
- package/index.d.ts +17 -92
- package/index.ios.d.ts +5 -10
- package/index.ios.js +72 -54
- package/index.ios.js.map +1 -1
- package/package.json +4 -4
- package/platforms/android/include.gradle +21 -16
- package/platforms/android/java/com/nativescript/image/CacheKeyStore.java +65 -0
- package/platforms/android/java/com/nativescript/image/CapturingEngineKeyFactory.java +43 -0
- package/platforms/android/java/com/nativescript/image/CompositeRequestListener.java +58 -0
- package/platforms/android/java/com/nativescript/image/ConditionalCrossFadeFactory.java +33 -0
- package/platforms/android/java/com/nativescript/image/CustomDataFetcher.java +124 -0
- package/platforms/android/java/com/nativescript/image/CustomGlideModule.java +220 -0
- package/platforms/android/java/com/nativescript/image/CustomGlideUrl.java +52 -0
- package/platforms/android/java/com/nativescript/image/CustomUrlLoader.java +74 -0
- package/platforms/android/java/com/nativescript/image/EvictionManager.java +735 -0
- package/platforms/android/java/com/nativescript/image/ExtractRequestOptions.java +109 -0
- package/platforms/android/java/com/nativescript/image/ImageLoadSourceCallback.java +5 -0
- package/platforms/android/java/com/nativescript/image/ImageProgressCallback.java +5 -0
- package/platforms/android/java/com/nativescript/image/LoadSourceInterceptor.java +28 -0
- package/platforms/android/java/com/nativescript/image/MatrixDrawable.java +200 -0
- package/platforms/android/java/com/nativescript/image/MatrixDrawableImageViewTarget.java +154 -0
- package/platforms/android/java/com/nativescript/image/MatrixImageView.java +696 -0
- package/platforms/android/java/com/nativescript/image/ProgressInterceptor.java +25 -0
- package/platforms/android/java/com/nativescript/image/ProgressResponseBody.java +70 -0
- package/platforms/android/java/com/nativescript/image/RecordingDigest.java +48 -0
- package/platforms/android/java/com/nativescript/image/RecreatedResourceKey.java +95 -0
- package/platforms/android/java/com/nativescript/image/SaveKeysRequestListener.java +145 -0
- package/platforms/android/java/com/nativescript/image/ScaleUtils.java +129 -0
- package/platforms/android/java/com/nativescript/image/SharedPrefCacheKeyStore.java +92 -0
- package/platforms/android/native-api-usage.json +39 -37
- package/platforms/ios/Podfile +1 -1
- package/references.d.ts +0 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/typings/android.d.ts +4 -27
- package/typings/glide.android.d.ts +9395 -0
- package/typings/glide.okhttp.android.d.ts +104 -0
- package/typings/glide.transform.android.d.ts +540 -0
- package/typings/ui_image.android.d.ts +517 -0
- package/platforms/android/java/com/nativescript/image/BaseDataSubscriber.java +0 -22
- package/platforms/android/java/com/nativescript/image/BaseDataSubscriberListener.java +0 -9
- package/platforms/android/java/com/nativescript/image/DraweeView.java +0 -371
- package/platforms/android/java/com/nativescript/image/NetworkImageRequest.java +0 -55
- package/platforms/android/java/com/nativescript/image/OkHttpNetworkFetcher.java +0 -56
- package/platforms/android/java/com/nativescript/image/ScalingBlurPostprocessor.java +0 -64
- package/platforms/android/java/com/nativescript/image/ScalingUtils.java +0 -519
- package/typings/fresco-processors.d.ts +0 -53
- package/typings/fresco.d.ts +0 -12070
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
package com.nativescript.image;
|
|
2
|
+
|
|
3
|
+
import android.animation.ValueAnimator;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.graphics.Matrix;
|
|
6
|
+
import android.graphics.drawable.Animatable;
|
|
7
|
+
import android.graphics.drawable.Drawable;
|
|
8
|
+
import android.util.AttributeSet;
|
|
9
|
+
import android.view.View;
|
|
10
|
+
import android.view.View.MeasureSpec;
|
|
11
|
+
import android.widget.ImageView;
|
|
12
|
+
|
|
13
|
+
import androidx.annotation.Nullable;
|
|
14
|
+
import androidx.appcompat.widget.AppCompatImageView;
|
|
15
|
+
|
|
16
|
+
import android.graphics.Canvas;
|
|
17
|
+
import android.graphics.Path;
|
|
18
|
+
import android.graphics.Color;
|
|
19
|
+
import android.graphics.Paint;
|
|
20
|
+
import android.graphics.Outline;
|
|
21
|
+
import android.graphics.RectF;
|
|
22
|
+
import android.graphics.Rect;
|
|
23
|
+
import android.graphics.PorterDuff;
|
|
24
|
+
import android.graphics.PorterDuffXfermode;
|
|
25
|
+
import android.view.ViewOutlineProvider;
|
|
26
|
+
|
|
27
|
+
import org.nativescript.widgets.BorderDrawable;
|
|
28
|
+
|
|
29
|
+
import android.util.Log;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ImageView that exposes setImageRotation(float) and coordinates rotation +
|
|
33
|
+
* scaleType.
|
|
34
|
+
*
|
|
35
|
+
* Enhancements:
|
|
36
|
+
* - Re-asserts MatrixDrawable -> wrapped drawable callback binding after the
|
|
37
|
+
* wrapper is set on the ImageView,
|
|
38
|
+
* to ensure the wrapped drawable's invalidation/schedule callbacks are received
|
|
39
|
+
* by the wrapper.
|
|
40
|
+
* - Starts/stops Animatable wrapped drawables consistently on
|
|
41
|
+
* attach/detach/visibility changes.
|
|
42
|
+
* - Aspect-ratio support (like MatrixImageView): setAspectRatio(float) to force
|
|
43
|
+
* measured width/height to respect ratio.
|
|
44
|
+
* - Supports rotation-aware measurement: when imageRotation is 90 or 270
|
|
45
|
+
* degrees, the enforced aspect ratio
|
|
46
|
+
* is inverted so measurement reflects the rotated content.
|
|
47
|
+
* - Adds a noRatioEnforce flag that disables all aspect-ratio measurement logic
|
|
48
|
+
* and simply calls super.onMeasure().
|
|
49
|
+
* - Auto-size support: when width or height measure is not finite, the view can
|
|
50
|
+
* size itself using
|
|
51
|
+
* explicit imageWidth/imageHeight (setImageSize) or the drawable intrinsic
|
|
52
|
+
* size.
|
|
53
|
+
*
|
|
54
|
+
* Protected fields allow subclasses (e.g. ZoomableMatrixImageView) to inspect
|
|
55
|
+
* base matrix for clamping.
|
|
56
|
+
*/
|
|
57
|
+
public class MatrixImageView extends AppCompatImageView {
|
|
58
|
+
// made protected so ZoomableMatrixImageView can compute clamped translations
|
|
59
|
+
// using base matrix.
|
|
60
|
+
protected final Matrix mBaseMatrix = new Matrix(); // rotation + scaleType fit (drawable->view)
|
|
61
|
+
protected final Matrix mExtraMatrix = new Matrix(); // user interaction (zoom/pan) in view coords, composed after
|
|
62
|
+
// base
|
|
63
|
+
private float mImageRotation = 0f;
|
|
64
|
+
private ImageView.ScaleType mAppliedScaleType = ImageView.ScaleType.FIT_CENTER;
|
|
65
|
+
|
|
66
|
+
private ValueAnimator mRotationAnimator;
|
|
67
|
+
|
|
68
|
+
// Aspect ratio support: value <= 0 means "disabled"
|
|
69
|
+
// When > 0, width / height == aspectRatio (same convention as MatrixImageView)
|
|
70
|
+
private float mAspectRatio = 0f;
|
|
71
|
+
|
|
72
|
+
// If true, bypass any aspect-ratio measurement (call super.onMeasure)
|
|
73
|
+
private boolean mNoRatioEnforce = false;
|
|
74
|
+
|
|
75
|
+
// Explicit image size that can be used for measuring when parent constraints
|
|
76
|
+
// are not finite.
|
|
77
|
+
// If zero, we fall back to drawable intrinsic size.
|
|
78
|
+
private int mImageWidth = 0;
|
|
79
|
+
private int mImageHeight = 0;
|
|
80
|
+
|
|
81
|
+
public MatrixImageView(Context context) {
|
|
82
|
+
super(context);
|
|
83
|
+
init();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public MatrixImageView(Context context, @Nullable AttributeSet attrs) {
|
|
87
|
+
super(context, attrs);
|
|
88
|
+
init();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public MatrixImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
|
92
|
+
super(context, attrs, defStyleAttr);
|
|
93
|
+
init();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private void init() {
|
|
97
|
+
// Keep ImageView in MATRIX mode so the system doesn't apply additional
|
|
98
|
+
// scaleType transforms.
|
|
99
|
+
super.setScaleType(ScaleType.MATRIX);
|
|
100
|
+
super.setImageMatrix(new Matrix());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Provide an explicit image size (in pixels) that will be used by onMeasure
|
|
105
|
+
* when
|
|
106
|
+
* the parent allows the view to size itself (i.e. width or height is not
|
|
107
|
+
* EXACTLY).
|
|
108
|
+
*
|
|
109
|
+
* Pass width=0 && height=0 to clear and fall back to drawable intrinsic size.
|
|
110
|
+
*/
|
|
111
|
+
public void setImageSize(int width, int height) {
|
|
112
|
+
if (mImageWidth == width && mImageHeight == height)
|
|
113
|
+
return;
|
|
114
|
+
mImageWidth = Math.max(0, width);
|
|
115
|
+
mImageHeight = Math.max(0, height);
|
|
116
|
+
// When the image size changes, we need to re-measure / layout so any AT_MOST/UNSPECIFIED
|
|
117
|
+
// parent dimensions are resolved correctly.
|
|
118
|
+
requestLayout();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public int getImageWidth() {
|
|
122
|
+
return mImageWidth;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public int getImageHeight() {
|
|
126
|
+
return mImageHeight;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Clear explicit image size; onMeasure will fall back to drawable intrinsic
|
|
131
|
+
* size.
|
|
132
|
+
*/
|
|
133
|
+
public void clearImageSize() {
|
|
134
|
+
setImageSize(0, 0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Aspect ratio API (same sign convention as MatrixImageView):
|
|
139
|
+
* - setAspectRatio(r) with r > 0 will cause the view to measure so that
|
|
140
|
+
* width/height == r
|
|
141
|
+
* - setAspectRatio(0) or negative disables aspect-ratio behavior and falls back
|
|
142
|
+
* to normal measuring
|
|
143
|
+
*/
|
|
144
|
+
public void setAspectRatio(float aspectRatio) {
|
|
145
|
+
if (aspectRatio < 0f)
|
|
146
|
+
aspectRatio = 0f;
|
|
147
|
+
if (Float.compare(mAspectRatio, aspectRatio) == 0)
|
|
148
|
+
return;
|
|
149
|
+
mAspectRatio = aspectRatio;
|
|
150
|
+
requestLayout();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public float getAspectRatio() {
|
|
154
|
+
return mAspectRatio;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* When true, onMeasure will bypass aspect-ratio enforcement and simply call
|
|
159
|
+
* super.onMeasure().
|
|
160
|
+
*/
|
|
161
|
+
public void setNoRatioEnforce(boolean noRatioEnforce) {
|
|
162
|
+
if (mNoRatioEnforce == noRatioEnforce)
|
|
163
|
+
return;
|
|
164
|
+
mNoRatioEnforce = noRatioEnforce;
|
|
165
|
+
requestLayout();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public boolean isNoRatioEnforce() {
|
|
169
|
+
return mNoRatioEnforce;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Override
|
|
173
|
+
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
174
|
+
// If aspect ratio not set or explicitly disabled, perform auto-size only if
|
|
175
|
+
// needed (parent not exact)
|
|
176
|
+
if (mNoRatioEnforce) {
|
|
177
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
|
182
|
+
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
|
183
|
+
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
|
184
|
+
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
|
185
|
+
|
|
186
|
+
// Log.d("JS", "onMeasure widthMode: " + widthMode + " heightMode: "+ heightMode + " widthSize: "+ widthSize+ " heightSize: "+ heightSize+ " mAspectRatio: "+ mAspectRatio+ " mImageWidth: "+ mImageWidth+ " mImageHeight: "+ mImageHeight);
|
|
187
|
+
|
|
188
|
+
// Determine whether parent constrained width/height exactly
|
|
189
|
+
final boolean finiteWidth = (widthMode == MeasureSpec.EXACTLY);
|
|
190
|
+
final boolean finiteHeight = (heightMode == MeasureSpec.EXACTLY);
|
|
191
|
+
|
|
192
|
+
// Consider paddings
|
|
193
|
+
final int paddingH = getPaddingLeft() + getPaddingRight();
|
|
194
|
+
final int paddingV = getPaddingTop() + getPaddingBottom();
|
|
195
|
+
|
|
196
|
+
// Work with available inner space
|
|
197
|
+
int availableWidth = Math.max(0, widthSize - paddingH);
|
|
198
|
+
int availableHeight = Math.max(0, heightSize - paddingV);
|
|
199
|
+
|
|
200
|
+
// Determine the source aspect ratio to use:
|
|
201
|
+
// Priority:
|
|
202
|
+
// 1) explicit mAspectRatio if > 0
|
|
203
|
+
// 2) computed from imageWidth/imageHeight if known
|
|
204
|
+
// 3) computed from drawable intrinsic size if available
|
|
205
|
+
float aspect = mAspectRatio;
|
|
206
|
+
int srcW = mImageWidth;
|
|
207
|
+
int srcH = mImageHeight;
|
|
208
|
+
|
|
209
|
+
if (aspect <= 0f) {
|
|
210
|
+
// no explicit aspect ratio, try to derive one from sizes
|
|
211
|
+
if (srcW <= 0 || srcH <= 0) {
|
|
212
|
+
Drawable d = getDrawable();
|
|
213
|
+
if (d != null) {
|
|
214
|
+
int iw = d.getIntrinsicWidth();
|
|
215
|
+
int ih = d.getIntrinsicHeight();
|
|
216
|
+
if (iw > 0 && ih > 0) {
|
|
217
|
+
srcW = iw;
|
|
218
|
+
srcH = ih;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (srcW > 0 && srcH > 0) {
|
|
223
|
+
aspect = srcW / (float) srcH;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// If no usable aspect ratio found, fall back to default measurement
|
|
228
|
+
if (aspect <= 0f) {
|
|
229
|
+
// No usable aspect. As a convenience, when width is EXACT and height is AT_MOST
|
|
230
|
+
// prefer the available height (fill vertical constraint) instead of returning
|
|
231
|
+
// zero (wrap_content with no drawable).
|
|
232
|
+
if ((finiteWidth && !finiteHeight) || (!finiteWidth && finiteHeight)) {
|
|
233
|
+
int measuredWidth = availableWidth + paddingH;
|
|
234
|
+
int measuredHeight = availableHeight + paddingV;
|
|
235
|
+
setMeasuredDimension(resolveSizeAndState(measuredWidth, widthMeasureSpec, 0),
|
|
236
|
+
resolveSizeAndState(measuredHeight, heightMeasureSpec, 0));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Fallback to default ImageView behavior otherwise
|
|
240
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Effective aspect ratio must take into account 90/270 rotation which swaps
|
|
245
|
+
// drawable axes.
|
|
246
|
+
float effectiveAspect = aspect;
|
|
247
|
+
float normalizedRotation = mImageRotation % 360f;
|
|
248
|
+
if (normalizedRotation < 0f)
|
|
249
|
+
normalizedRotation += 360f;
|
|
250
|
+
// If rotation is near 90 or 270 degrees, invert the aspect.
|
|
251
|
+
int rot = Math.round(normalizedRotation) % 360;
|
|
252
|
+
if ((rot == 90) || (rot == 270)) {
|
|
253
|
+
effectiveAspect = 1f / aspect;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
int measuredWidth;
|
|
257
|
+
int measuredHeight;
|
|
258
|
+
|
|
259
|
+
// If aspect ratio is set (explicit) or derived, apply MatrixImageView-like logic
|
|
260
|
+
// with auto size when parent is not exact.
|
|
261
|
+
// If width is exact and height is not exact -> compute height from
|
|
262
|
+
// width/aspectRatio
|
|
263
|
+
if (finiteWidth && !finiteHeight) {
|
|
264
|
+
measuredWidth = availableWidth;
|
|
265
|
+
measuredHeight = Math.round(measuredWidth / effectiveAspect);
|
|
266
|
+
if (heightMode == MeasureSpec.AT_MOST && measuredHeight > availableHeight) {
|
|
267
|
+
measuredHeight = availableHeight;
|
|
268
|
+
}
|
|
269
|
+
measuredWidth += paddingH;
|
|
270
|
+
measuredHeight += paddingV;
|
|
271
|
+
setMeasuredDimension(resolveSizeAndState(measuredWidth, widthMeasureSpec, 0),
|
|
272
|
+
resolveSizeAndState(measuredHeight, heightMeasureSpec, 0));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// If height is exact and width is not exact -> compute width from
|
|
276
|
+
// height*aspectRatio
|
|
277
|
+
else if (finiteHeight && !finiteWidth) {
|
|
278
|
+
measuredHeight = availableHeight;
|
|
279
|
+
measuredWidth = Math.round(measuredHeight * effectiveAspect);
|
|
280
|
+
if (widthMode == MeasureSpec.AT_MOST && measuredWidth > availableWidth) {
|
|
281
|
+
measuredWidth = availableWidth;
|
|
282
|
+
}
|
|
283
|
+
measuredWidth += paddingH;
|
|
284
|
+
measuredHeight += paddingV;
|
|
285
|
+
setMeasuredDimension(resolveSizeAndState(measuredWidth, widthMeasureSpec, 0),
|
|
286
|
+
resolveSizeAndState(measuredHeight, heightMeasureSpec, 0));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// If neither dimension is exact, try to size based on image intrinsic/explicit
|
|
290
|
+
// size
|
|
291
|
+
else if (!finiteWidth && !finiteHeight) {
|
|
292
|
+
// If we have a source size (srcW/srcH), use it as a hint to size the view
|
|
293
|
+
// (respect aspect)
|
|
294
|
+
if (srcW > 0 && srcH > 0) {
|
|
295
|
+
// Scale the source size to fit available constraints (AT_MOST) while preserving
|
|
296
|
+
// aspect
|
|
297
|
+
// If parent constraints are UNSPECIFIED, use source size directly.
|
|
298
|
+
int desiredInnerW = srcW;
|
|
299
|
+
int desiredInnerH = srcH;
|
|
300
|
+
|
|
301
|
+
// If width has an AT_MOST constraint, limit by availableWidth
|
|
302
|
+
if (widthMode == MeasureSpec.AT_MOST && desiredInnerW > availableWidth) {
|
|
303
|
+
desiredInnerW = availableWidth;
|
|
304
|
+
desiredInnerH = Math.round(desiredInnerW / effectiveAspect);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// If height has an AT_MOST constraint, limit by availableHeight
|
|
308
|
+
if (heightMode == MeasureSpec.AT_MOST && desiredInnerH > availableHeight) {
|
|
309
|
+
desiredInnerH = availableHeight;
|
|
310
|
+
desiredInnerW = Math.round(desiredInnerH * effectiveAspect);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
measuredWidth = desiredInnerW + paddingH;
|
|
314
|
+
measuredHeight = desiredInnerH + paddingV;
|
|
315
|
+
setMeasuredDimension(resolveSizeAndState(measuredWidth, widthMeasureSpec, 0),
|
|
316
|
+
resolveSizeAndState(measuredHeight, heightMeasureSpec, 0));
|
|
317
|
+
return;
|
|
318
|
+
} else {
|
|
319
|
+
// No source size available -> fall back to default
|
|
320
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Both dimensions are exact (or other cases) -> default measurement
|
|
326
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@Override
|
|
330
|
+
public void setScaleType(ScaleType scaleType) {
|
|
331
|
+
if (scaleType == null) {
|
|
332
|
+
scaleType = ScaleType.FIT_CENTER;
|
|
333
|
+
}
|
|
334
|
+
mAppliedScaleType = scaleType;
|
|
335
|
+
updateBaseMatrix();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Set rotation (degrees) applied to the drawable before fitting to view.
|
|
340
|
+
* This is an immediate set (no animation).
|
|
341
|
+
*
|
|
342
|
+
* Note: changing rotation may change measured dimensions when aspect-ratio
|
|
343
|
+
* enforcement is active.
|
|
344
|
+
* We therefore requestLayout() so measurement reflects the rotated aspect.
|
|
345
|
+
*/
|
|
346
|
+
public void setImageRotation(float degrees) {
|
|
347
|
+
cancelRotationAnimation();
|
|
348
|
+
if (Float.compare(mImageRotation, degrees) == 0)
|
|
349
|
+
return;
|
|
350
|
+
mImageRotation = degrees;
|
|
351
|
+
// rotation can affect measurement when aspect-ratio is enforced (90/270 swap)
|
|
352
|
+
requestLayout();
|
|
353
|
+
updateBaseMatrix();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get current image rotation in degrees.
|
|
358
|
+
*/
|
|
359
|
+
public float getImageRotation() {
|
|
360
|
+
return mImageRotation;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Animate rotation from current rotation to target degrees.
|
|
365
|
+
*
|
|
366
|
+
* @param toDegrees target rotation value in degrees
|
|
367
|
+
* @param durationMs duration in milliseconds (use 0 for immediate)
|
|
368
|
+
*
|
|
369
|
+
* Note: the animation will call requestLayout() on each
|
|
370
|
+
* update so the view can adjust layout
|
|
371
|
+
* if the rotation crosses axis swap thresholds or if you want
|
|
372
|
+
* continuous relayout during animation.
|
|
373
|
+
* This is potentially expensive; disable aspect-ratio or
|
|
374
|
+
* setNoRatioEnforce(true) if you prefer no layout churn.
|
|
375
|
+
*/
|
|
376
|
+
public void animateImageRotation(float toDegrees, long durationMs) {
|
|
377
|
+
cancelRotationAnimation();
|
|
378
|
+
if (durationMs <= 0) {
|
|
379
|
+
setImageRotation(toDegrees);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
mRotationAnimator = ValueAnimator.ofFloat(mImageRotation, toDegrees);
|
|
383
|
+
mRotationAnimator.setDuration(durationMs);
|
|
384
|
+
mRotationAnimator.addUpdateListener(animation -> {
|
|
385
|
+
float value = (float) animation.getAnimatedValue();
|
|
386
|
+
mImageRotation = value;
|
|
387
|
+
// rotation may affect measurement; request layout to update sizing while
|
|
388
|
+
// animating.
|
|
389
|
+
requestLayout();
|
|
390
|
+
updateBaseMatrix();
|
|
391
|
+
});
|
|
392
|
+
mRotationAnimator.start();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Cancel any running rotation animation.
|
|
397
|
+
*/
|
|
398
|
+
public void cancelRotationAnimation() {
|
|
399
|
+
if (mRotationAnimator != null) {
|
|
400
|
+
mRotationAnimator.cancel();
|
|
401
|
+
mRotationAnimator = null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@Override
|
|
406
|
+
public void setImageDrawable(@Nullable Drawable drawable) {
|
|
407
|
+
if (drawable == null) {
|
|
408
|
+
// stop any wrapped animatable and clear
|
|
409
|
+
Drawable old = getDrawable();
|
|
410
|
+
if (old instanceof Animatable) {
|
|
411
|
+
((Animatable) old).stop();
|
|
412
|
+
}
|
|
413
|
+
setImageSize(0, 0);
|
|
414
|
+
super.setImageDrawable(null);
|
|
415
|
+
// clear implicit image size when drawable removed
|
|
416
|
+
// (do not clear explicit mImageWidth/mImageHeight set by caller)
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
setImageSize(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
|
420
|
+
MatrixDrawable md = new MatrixDrawable(drawable);
|
|
421
|
+
// set the wrapper as the ImageView drawable - AppCompatImageView will set the
|
|
422
|
+
// wrapper's callback to the view.
|
|
423
|
+
super.setImageDrawable(md);
|
|
424
|
+
|
|
425
|
+
// Re-assert the wrapped drawable's callback -> wrapper relationship (safe no-op
|
|
426
|
+
// if already set).
|
|
427
|
+
md.refreshWrappedCallback();
|
|
428
|
+
|
|
429
|
+
// If caller hasn't set an explicit image size, capture drawable intrinsic size
|
|
430
|
+
// as a hint for measuring
|
|
431
|
+
if (mImageWidth <= 0 || mImageHeight <= 0) {
|
|
432
|
+
int iw = drawable.getIntrinsicWidth();
|
|
433
|
+
int ih = drawable.getIntrinsicHeight();
|
|
434
|
+
if (iw > 0 && ih > 0) {
|
|
435
|
+
mImageWidth = iw;
|
|
436
|
+
mImageHeight = ih;
|
|
437
|
+
// we captured a size that may affect measurement
|
|
438
|
+
requestLayout();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Ensure the next updateBaseMatrix applies the matrix to the new wrapper instance.
|
|
443
|
+
mLastComposedDrawable = null;
|
|
444
|
+
updateBaseMatrix();
|
|
445
|
+
|
|
446
|
+
// If underlying drawable is animatable and ImageView is visible, start it
|
|
447
|
+
if (drawable instanceof Animatable && getVisibility() == VISIBLE) {
|
|
448
|
+
((Animatable) drawable).start();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// cache last composed matrix so we can no-op identical updates (avoid repeated invalidates)
|
|
453
|
+
private final Matrix mLastComposedMatrix = new Matrix();
|
|
454
|
+
// The drawable instance we last applied mLastComposedMatrix to. If a new drawable is
|
|
455
|
+
// installed (wrapper changed), we must still apply the composed matrix even if values match.
|
|
456
|
+
private Drawable mLastComposedDrawable = null;
|
|
457
|
+
|
|
458
|
+
private static boolean matricesApproxEqual(Matrix a, Matrix b, float eps) {
|
|
459
|
+
float[] va = new float[9];
|
|
460
|
+
float[] vb = new float[9];
|
|
461
|
+
a.getValues(va);
|
|
462
|
+
b.getValues(vb);
|
|
463
|
+
for (int i = 0; i < 9; ++i) {
|
|
464
|
+
if (Math.abs(va[i] - vb[i]) > eps) return false;
|
|
465
|
+
}
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private void updateBaseMatrix() {
|
|
470
|
+
Drawable d = getDrawable();
|
|
471
|
+
if (d == null)
|
|
472
|
+
return;
|
|
473
|
+
|
|
474
|
+
int vw = getWidth() - getPaddingLeft() - getPaddingRight();
|
|
475
|
+
int vh = getHeight() - getPaddingTop() - getPaddingBottom();
|
|
476
|
+
|
|
477
|
+
Matrix base = new Matrix();
|
|
478
|
+
if (vw > 0 && vh > 0) {
|
|
479
|
+
ScaleUtils.getImageMatrix(d, vw, vh, mImageRotation, mAppliedScaleType, base);
|
|
480
|
+
}
|
|
481
|
+
mBaseMatrix.set(base);
|
|
482
|
+
|
|
483
|
+
// Compose base + extra into one matrix and apply to the MatrixDrawable wrapper
|
|
484
|
+
Matrix composed = new Matrix(mBaseMatrix);
|
|
485
|
+
composed.postConcat(mExtraMatrix);
|
|
486
|
+
|
|
487
|
+
// No-op only if composed matrix hasn't changed AND the drawable instance is the same.
|
|
488
|
+
// If the wrapper changed, we must explicitly set the composed matrix on the new wrapper.
|
|
489
|
+
if (d == mLastComposedDrawable && matricesApproxEqual(mLastComposedMatrix, composed, 1e-6f)) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
mLastComposedMatrix.set(composed);
|
|
493
|
+
mLastComposedDrawable = d;
|
|
494
|
+
|
|
495
|
+
if (d instanceof MatrixDrawable) {
|
|
496
|
+
((MatrixDrawable) d).setMatrix(composed);
|
|
497
|
+
} else {
|
|
498
|
+
// Fallback: if drawable is not wrapped, fall back on ImageView's matrix
|
|
499
|
+
super.setImageMatrix(composed);
|
|
500
|
+
}
|
|
501
|
+
// Use VSync-friendly invalidation.
|
|
502
|
+
postInvalidateOnAnimation();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Apply additional transform on top of rotation+scaleType fit (e.g. zoom/pan).
|
|
507
|
+
* The passed matrix is interpreted in view coordinates and will be
|
|
508
|
+
* post-concatenated after base.
|
|
509
|
+
* If extra is null, the base matrix alone is used.
|
|
510
|
+
*/
|
|
511
|
+
public void setExtraTransform(@Nullable Matrix extra) {
|
|
512
|
+
mExtraMatrix.reset();
|
|
513
|
+
if (extra != null) {
|
|
514
|
+
mExtraMatrix.set(extra);
|
|
515
|
+
}
|
|
516
|
+
updateBaseMatrix();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
@Override
|
|
520
|
+
protected void onAttachedToWindow() {
|
|
521
|
+
super.onAttachedToWindow();
|
|
522
|
+
// If drawable is animatable, start it when attached and visible
|
|
523
|
+
Drawable d = getDrawable();
|
|
524
|
+
if (d instanceof Animatable && getVisibility() == VISIBLE) {
|
|
525
|
+
((Animatable) d).start();
|
|
526
|
+
}
|
|
527
|
+
// If we have a MatrixDrawable, re-assert wrapped callback relationship
|
|
528
|
+
if (d instanceof MatrixDrawable) {
|
|
529
|
+
((MatrixDrawable) d).refreshWrappedCallback();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
@Override
|
|
534
|
+
protected void onDetachedFromWindow() {
|
|
535
|
+
// stop any animatables to avoid leaks
|
|
536
|
+
Drawable d = getDrawable();
|
|
537
|
+
if (d instanceof Animatable) {
|
|
538
|
+
((Animatable) d).stop();
|
|
539
|
+
}
|
|
540
|
+
cancelRotationAnimation();
|
|
541
|
+
super.onDetachedFromWindow();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@Override
|
|
545
|
+
protected void onVisibilityChanged(View changedView, int visibility) {
|
|
546
|
+
super.onVisibilityChanged(changedView, visibility);
|
|
547
|
+
Drawable d = getDrawable();
|
|
548
|
+
if (d instanceof Animatable) {
|
|
549
|
+
if (visibility == VISIBLE) {
|
|
550
|
+
((Animatable) d).start();
|
|
551
|
+
} else {
|
|
552
|
+
((Animatable) d).stop();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// If drawable exists and is MatrixDrawable, re-assert wrapped callback
|
|
556
|
+
// relationship
|
|
557
|
+
if (d instanceof MatrixDrawable) {
|
|
558
|
+
((MatrixDrawable) d).refreshWrappedCallback();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
@Override
|
|
563
|
+
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
564
|
+
updateBaseMatrix();
|
|
565
|
+
super.onSizeChanged(w, h, oldw, oldh);
|
|
566
|
+
updateOutlineProvider();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Add this method to MatrixImageView (insert into the class body)
|
|
570
|
+
public void resetInteraction() {
|
|
571
|
+
// Reset any extra interaction transform (zoom/pan) and update the view.
|
|
572
|
+
mExtraMatrix.reset();
|
|
573
|
+
// Apply the cleared extra transform
|
|
574
|
+
setExtraTransform(null);
|
|
575
|
+
// Notify layout/state if necessary (no-op here)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Hook for subclasses to validate & clamp current scale (no-op for non-zoomable base view)
|
|
579
|
+
protected void ensureCurrentScaleInBounds() { /* no-op for base */ }
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
public boolean isUsingOutlineProvider = false;
|
|
583
|
+
private static Paint clipPaint;
|
|
584
|
+
public void updateOutlineProvider() {
|
|
585
|
+
Drawable drawable = getBackground();
|
|
586
|
+
if (android.os.Build.VERSION.SDK_INT >= 21) {
|
|
587
|
+
// we try to support N setting outline provider now
|
|
588
|
+
if (!isUsingOutlineProvider && getOutlineProvider() != null) {
|
|
589
|
+
// already handled somewhere else
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (drawable instanceof BorderDrawable && (android.os.Build.VERSION.SDK_INT >= 33 || ((BorderDrawable)drawable).hasUniformBorderRadius())) {
|
|
593
|
+
setOutlineProvider(new ViewOutlineProvider() {
|
|
594
|
+
@Override
|
|
595
|
+
public void getOutline(View view, Outline outline) {
|
|
596
|
+
Drawable drawable = getBackground();
|
|
597
|
+
if (drawable instanceof BorderDrawable) {
|
|
598
|
+
BorderDrawable borderDrawable = (BorderDrawable) drawable;
|
|
599
|
+
// that if test is only needed until N BorderDrawable is updated to do it
|
|
600
|
+
if (borderDrawable.hasUniformBorderRadius()) {
|
|
601
|
+
// outlineRect.set(borderDrawable.getBounds());
|
|
602
|
+
outline.setRoundRect(borderDrawable.getBounds(), borderDrawable.getBorderBottomLeftRadius());
|
|
603
|
+
} else {
|
|
604
|
+
drawable.getOutline(outline);
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
outline.setRect(100, 100, view.getWidth() - 200, view.getHeight() - 200);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
setClipToOutline(true);
|
|
612
|
+
isUsingOutlineProvider = true;
|
|
613
|
+
// } else if (android.os.Build.VERSION.SDK_INT >= 21) {
|
|
614
|
+
// isUsingOutlineProvider = false;
|
|
615
|
+
// setOutlineProvider(null);
|
|
616
|
+
// setClipToOutline(false);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
@Override
|
|
622
|
+
public void setBackground(Drawable background) {
|
|
623
|
+
super.setBackground(background);
|
|
624
|
+
updateOutlineProvider();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
Path innerBorderPath;
|
|
628
|
+
Path innerBorderTempPath;
|
|
629
|
+
|
|
630
|
+
private Path generateInnerBorderPath(BorderDrawable borderDrawable) {
|
|
631
|
+
|
|
632
|
+
float borderTopLeftRadius = borderDrawable.getBorderTopLeftRadius();
|
|
633
|
+
float borderTopRightRadius = borderDrawable.getBorderTopRightRadius();
|
|
634
|
+
float borderBottomRightRadius = borderDrawable.getBorderBottomRightRadius();
|
|
635
|
+
float borderBottomLeftRadius = borderDrawable.getBorderBottomLeftRadius();
|
|
636
|
+
float borderLeftWidth = borderDrawable.getBorderLeftWidth();
|
|
637
|
+
float borderBottomWidth = borderDrawable.getBorderBottomWidth();
|
|
638
|
+
float borderTopWidth = borderDrawable.getBorderTopWidth();
|
|
639
|
+
float borderRightWidth = borderDrawable.getBorderRightWidth();
|
|
640
|
+
if (innerBorderPath == null) {
|
|
641
|
+
innerBorderPath = new Path();
|
|
642
|
+
} else {
|
|
643
|
+
innerBorderPath.reset();
|
|
644
|
+
}
|
|
645
|
+
if (innerBorderTempPath == null) {
|
|
646
|
+
innerBorderTempPath = new Path();
|
|
647
|
+
} else {
|
|
648
|
+
innerBorderTempPath.reset();
|
|
649
|
+
}
|
|
650
|
+
Rect bounds = borderDrawable.getBounds();
|
|
651
|
+
float width = (float) borderDrawable.getBounds().width();
|
|
652
|
+
float height = (float) borderDrawable.getBounds().height();
|
|
653
|
+
|
|
654
|
+
RectF borderInnerRect = new RectF(borderLeftWidth, borderTopWidth, width - borderRightWidth,
|
|
655
|
+
height - borderBottomWidth);
|
|
656
|
+
float[] borderInnerRadii = { Math.max(0, borderTopLeftRadius - borderLeftWidth),
|
|
657
|
+
Math.max(0, borderTopLeftRadius - borderTopWidth), Math.max(0, borderTopRightRadius - borderRightWidth),
|
|
658
|
+
Math.max(0, borderTopRightRadius - borderTopWidth),
|
|
659
|
+
Math.max(0, borderBottomRightRadius - borderRightWidth),
|
|
660
|
+
Math.max(0, borderBottomRightRadius - borderBottomWidth),
|
|
661
|
+
Math.max(0, borderBottomLeftRadius - borderLeftWidth),
|
|
662
|
+
Math.max(0, borderBottomLeftRadius - borderBottomWidth) };
|
|
663
|
+
innerBorderTempPath.addRoundRect(borderInnerRect, borderInnerRadii, Path.Direction.CW);
|
|
664
|
+
innerBorderPath.addRect(new RectF(bounds), Path.Direction.CW);
|
|
665
|
+
innerBorderPath.op(innerBorderTempPath, Path.Op.DIFFERENCE);
|
|
666
|
+
return innerBorderPath;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
@Override
|
|
670
|
+
protected void onDraw(Canvas canvas) {
|
|
671
|
+
Drawable drawable = getBackground();
|
|
672
|
+
if (!isUsingOutlineProvider && drawable instanceof BorderDrawable) {
|
|
673
|
+
BorderDrawable borderDrawable = (BorderDrawable) drawable;
|
|
674
|
+
Path clipPath = generateInnerBorderPath(borderDrawable);
|
|
675
|
+
if (clipPath != null) {
|
|
676
|
+
if (MatrixImageView.clipPaint == null) {
|
|
677
|
+
MatrixImageView.clipPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
678
|
+
MatrixImageView.clipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
|
679
|
+
}
|
|
680
|
+
int saveCount;
|
|
681
|
+
int width = getWidth();
|
|
682
|
+
int height = getHeight();
|
|
683
|
+
if (android.os.Build.VERSION.SDK_INT >= 21) {
|
|
684
|
+
saveCount = canvas.saveLayer(new android.graphics.RectF(0.0f, 0.0f, width, height), null);
|
|
685
|
+
} else {
|
|
686
|
+
saveCount = canvas.saveLayer(0.0f, 0.0f, width, height, null, Canvas.ALL_SAVE_FLAG);
|
|
687
|
+
}
|
|
688
|
+
super.onDraw(canvas);
|
|
689
|
+
canvas.drawPath(clipPath, MatrixImageView.clipPaint);
|
|
690
|
+
canvas.restoreToCount(saveCount);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
super.onDraw(canvas);
|
|
695
|
+
}
|
|
696
|
+
}
|