@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +4 -20
  3. package/index-common.d.ts +47 -18
  4. package/index-common.js +5 -69
  5. package/index-common.js.map +1 -1
  6. package/index.android.d.ts +33 -60
  7. package/index.android.js +597 -702
  8. package/index.android.js.map +1 -1
  9. package/index.d.ts +17 -92
  10. package/index.ios.d.ts +5 -10
  11. package/index.ios.js +72 -54
  12. package/index.ios.js.map +1 -1
  13. package/package.json +4 -4
  14. package/platforms/android/include.gradle +21 -16
  15. package/platforms/android/java/com/nativescript/image/CacheKeyStore.java +65 -0
  16. package/platforms/android/java/com/nativescript/image/CapturingEngineKeyFactory.java +43 -0
  17. package/platforms/android/java/com/nativescript/image/CompositeRequestListener.java +58 -0
  18. package/platforms/android/java/com/nativescript/image/ConditionalCrossFadeFactory.java +33 -0
  19. package/platforms/android/java/com/nativescript/image/CustomDataFetcher.java +124 -0
  20. package/platforms/android/java/com/nativescript/image/CustomGlideModule.java +220 -0
  21. package/platforms/android/java/com/nativescript/image/CustomGlideUrl.java +52 -0
  22. package/platforms/android/java/com/nativescript/image/CustomUrlLoader.java +74 -0
  23. package/platforms/android/java/com/nativescript/image/EvictionManager.java +735 -0
  24. package/platforms/android/java/com/nativescript/image/ExtractRequestOptions.java +109 -0
  25. package/platforms/android/java/com/nativescript/image/ImageLoadSourceCallback.java +5 -0
  26. package/platforms/android/java/com/nativescript/image/ImageProgressCallback.java +5 -0
  27. package/platforms/android/java/com/nativescript/image/LoadSourceInterceptor.java +28 -0
  28. package/platforms/android/java/com/nativescript/image/MatrixDrawable.java +200 -0
  29. package/platforms/android/java/com/nativescript/image/MatrixDrawableImageViewTarget.java +154 -0
  30. package/platforms/android/java/com/nativescript/image/MatrixImageView.java +696 -0
  31. package/platforms/android/java/com/nativescript/image/ProgressInterceptor.java +25 -0
  32. package/platforms/android/java/com/nativescript/image/ProgressResponseBody.java +70 -0
  33. package/platforms/android/java/com/nativescript/image/RecordingDigest.java +48 -0
  34. package/platforms/android/java/com/nativescript/image/RecreatedResourceKey.java +95 -0
  35. package/platforms/android/java/com/nativescript/image/SaveKeysRequestListener.java +145 -0
  36. package/platforms/android/java/com/nativescript/image/ScaleUtils.java +129 -0
  37. package/platforms/android/java/com/nativescript/image/SharedPrefCacheKeyStore.java +92 -0
  38. package/platforms/android/native-api-usage.json +39 -37
  39. package/platforms/ios/Podfile +1 -1
  40. package/references.d.ts +0 -1
  41. package/tsconfig.tsbuildinfo +1 -1
  42. package/typings/android.d.ts +4 -27
  43. package/typings/glide.android.d.ts +9395 -0
  44. package/typings/glide.okhttp.android.d.ts +104 -0
  45. package/typings/glide.transform.android.d.ts +540 -0
  46. package/typings/ui_image.android.d.ts +517 -0
  47. package/platforms/android/java/com/nativescript/image/BaseDataSubscriber.java +0 -22
  48. package/platforms/android/java/com/nativescript/image/BaseDataSubscriberListener.java +0 -9
  49. package/platforms/android/java/com/nativescript/image/DraweeView.java +0 -371
  50. package/platforms/android/java/com/nativescript/image/NetworkImageRequest.java +0 -55
  51. package/platforms/android/java/com/nativescript/image/OkHttpNetworkFetcher.java +0 -56
  52. package/platforms/android/java/com/nativescript/image/ScalingBlurPostprocessor.java +0 -64
  53. package/platforms/android/java/com/nativescript/image/ScalingUtils.java +0 -519
  54. package/typings/fresco-processors.d.ts +0 -53
  55. 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
+ }