@rafaelaugusto/jolt 0.1.0-beta.6 → 0.1.0-beta.7
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/README.md +12 -2
- package/dist/image-effect-native.d.ts +26 -0
- package/dist/image-effect-native.d.ts.map +1 -0
- package/dist/image-effect-native.js +73 -0
- package/dist/image-effect-native.js.map +1 -0
- package/dist/image-effect.d.ts +50 -0
- package/dist/image-effect.d.ts.map +1 -0
- package/dist/image-effect.js +290 -0
- package/dist/image-effect.js.map +1 -0
- package/dist/image-owner.d.ts +3 -0
- package/dist/image-owner.d.ts.map +1 -0
- package/dist/image-owner.js +11 -0
- package/dist/image-owner.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/liquid.d.ts.map +1 -1
- package/dist/liquid.js +4 -0
- package/dist/liquid.js.map +1 -1
- package/dist/zoom-native.d.ts +30 -0
- package/dist/zoom-native.d.ts.map +1 -0
- package/dist/zoom-native.js +56 -0
- package/dist/zoom-native.js.map +1 -0
- package/dist/zoomable.d.ts +48 -0
- package/dist/zoomable.d.ts.map +1 -0
- package/dist/zoomable.js +249 -0
- package/dist/zoomable.js.map +1 -0
- package/package.json +2 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
- package/platforms/android/proguard-rules.pro +4 -0
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/JoltImageEffect.h +25 -0
- package/platforms/ios/src/JoltImageEffect.mm +242 -0
- package/platforms/ios/src/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +5 -0
- package/src/android-plugin.d.ts +78 -0
- package/src/image-effect-native.ts +129 -0
- package/src/image-effect.ts +336 -0
- package/src/image-owner.ts +12 -0
- package/src/index.ts +9 -0
- package/src/ios-plugin.d.ts +59 -0
- package/src/liquid.ts +4 -0
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.animation.Animator;
|
|
4
|
+
import android.animation.AnimatorListenerAdapter;
|
|
5
|
+
import android.animation.ValueAnimator;
|
|
6
|
+
import android.graphics.*;
|
|
7
|
+
import android.graphics.drawable.*;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
import android.view.MotionEvent;
|
|
10
|
+
import android.view.View;
|
|
11
|
+
import android.widget.ImageView;
|
|
12
|
+
|
|
13
|
+
/** Image-only GPU effects. All animation and pointer samples stay in Java. */
|
|
14
|
+
public final class ImageEffect {
|
|
15
|
+
public interface Listener { void onEnd(boolean completed, float progress); }
|
|
16
|
+
private static final String SHADER =
|
|
17
|
+
"uniform shader source; uniform shader destination; uniform float2 size;" +
|
|
18
|
+
"uniform float2 origin; uniform float2 displacement; uniform float progress;" +
|
|
19
|
+
"uniform float amplitude; uniform float radius; uniform float seed; uniform float mode;" +
|
|
20
|
+
"half4 main(float2 p) { float2 uv=p/size; float2 q=p;" +
|
|
21
|
+
"if(mode<1.5){ float2 v=p-origin; float d=length(v);" +
|
|
22
|
+
"float edge=clamp(min(min(p.x,size.x-p.x),min(p.y,size.y-p.y))/max(amplitude*2.,1.),0.,1.);" +
|
|
23
|
+
"if(mode<.5){float wave=d-progress*length(size);" +
|
|
24
|
+
"float a=sin(wave/max(radius*.12,1.))*exp(-pow(wave/max(radius,1.),2.))*amplitude*sin(progress*3.14159265);" +
|
|
25
|
+
"q-=v/max(d,1.)*a*edge;}else{q-=displacement*exp(-d*d/max(radius*radius,1.))*edge;}" +
|
|
26
|
+
"return source.eval(clamp(q,float2(.5),size-float2(.5)));}" +
|
|
27
|
+
"if(progress<=0.)return source.eval(p); if(progress>=1.)return destination.eval(p);" +
|
|
28
|
+
"float n=fract(sin(dot(floor(p/3.),float2(12.9898,78.233))+seed)*43758.5453);" +
|
|
29
|
+
"float a=smoothstep(n-.08,n+.08,progress*1.16-.08);" +
|
|
30
|
+
"return mix(source.eval(p),destination.eval(p),a);}";
|
|
31
|
+
|
|
32
|
+
private final ImageView view;
|
|
33
|
+
private final Listener listener;
|
|
34
|
+
private final int mode;
|
|
35
|
+
private final float amplitude, radius;
|
|
36
|
+
private final long duration;
|
|
37
|
+
private final Drawable original;
|
|
38
|
+
private final Bitmap originalBitmap;
|
|
39
|
+
private final ImageView.ScaleType originalScale;
|
|
40
|
+
private final View.OnTouchListener previousTouch;
|
|
41
|
+
private final boolean originalClickable;
|
|
42
|
+
private Bitmap source, destination, placeholderBitmap;
|
|
43
|
+
private RuntimeShader shader;
|
|
44
|
+
private EffectDrawable drawable;
|
|
45
|
+
private Drawable placeholder;
|
|
46
|
+
private ValueAnimator animator;
|
|
47
|
+
private boolean disposed, paused, suppressEnd, attached;
|
|
48
|
+
private float progress, goal = 1, x, y, dx, dy;
|
|
49
|
+
private int pointer = -1;
|
|
50
|
+
private float startX, startY, rawX, rawY;
|
|
51
|
+
private long frames, lastFrame, longestFrame;
|
|
52
|
+
private String failureReason="";
|
|
53
|
+
private final Path contentClip=new Path();
|
|
54
|
+
|
|
55
|
+
public ImageEffect(ImageView view, Bitmap from, Bitmap to, float[] geometry, int mode, float x, float y,
|
|
56
|
+
float amplitude, float radius, long duration, float seed,
|
|
57
|
+
View.OnTouchListener previousTouch, Listener listener) {
|
|
58
|
+
this.view=view; this.listener=listener; this.mode=mode;
|
|
59
|
+
this.amplitude=amplitude; this.radius=radius; this.duration=duration;
|
|
60
|
+
this.x=x; this.y=y; this.previousTouch=previousTouch;
|
|
61
|
+
original=view.getDrawable(); originalScale=view.getScaleType();
|
|
62
|
+
originalBitmap=from;
|
|
63
|
+
originalClickable=view.isClickable();
|
|
64
|
+
if(Build.VERSION.SDK_INT<33 || !view.isHardwareAccelerated() || from==null || original==null || original instanceof Animatable) {
|
|
65
|
+
failureReason="Requires API 33, hardware rendering and a static drawable ("+(original==null?"null":original.getClass().getName())+")";
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
int w=view.getWidth(), h=view.getHeight();
|
|
69
|
+
if(w<=0 || h<=0 || (long)w*h>4194304L) {failureReason="Image surface exceeds the pixel budget";return;}
|
|
70
|
+
try {
|
|
71
|
+
float left=view.getPaddingLeft(),top=view.getPaddingTop(),right=view.getPaddingRight(),bottom=view.getPaddingBottom();
|
|
72
|
+
float[] radii={
|
|
73
|
+
Math.max(0,geometry[2]-left),Math.max(0,geometry[2]-top),
|
|
74
|
+
Math.max(0,geometry[3]-right),Math.max(0,geometry[3]-top),
|
|
75
|
+
Math.max(0,geometry[4]-right),Math.max(0,geometry[4]-bottom),
|
|
76
|
+
Math.max(0,geometry[5]-left),Math.max(0,geometry[5]-bottom)
|
|
77
|
+
};
|
|
78
|
+
contentClip.addRoundRect(new RectF(left,top,w-right,h-bottom),radii,Path.Direction.CW);
|
|
79
|
+
source=raster(from,geometry[0],w,h);
|
|
80
|
+
if(mode==2) {
|
|
81
|
+
if(to==null) { releaseBitmaps(); return; }
|
|
82
|
+
destination=raster(to,geometry[1],w,h);
|
|
83
|
+
}
|
|
84
|
+
shader=new RuntimeShader(SHADER);
|
|
85
|
+
shader.setInputShader("source",new BitmapShader(source,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP));
|
|
86
|
+
shader.setInputShader("destination",new BitmapShader(destination==null?source:destination,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP));
|
|
87
|
+
shader.setFloatUniform("size",w,h); shader.setFloatUniform("mode",mode);
|
|
88
|
+
shader.setFloatUniform("amplitude",amplitude); shader.setFloatUniform("radius",radius);
|
|
89
|
+
shader.setFloatUniform("seed",seed);
|
|
90
|
+
drawable=new EffectDrawable(w,h);
|
|
91
|
+
drawable.setBounds(0,0,w,h);
|
|
92
|
+
update();
|
|
93
|
+
// NativeScript's ImageView rasterizes custom drawables on a software
|
|
94
|
+
// canvas. An overlay keeps RuntimeShader on the hardware canvas.
|
|
95
|
+
view.getOverlay().add(drawable);
|
|
96
|
+
attached=true;
|
|
97
|
+
// NativeScript's onDraw clips the canvas to its bitmap's mapped
|
|
98
|
+
// rectangle, including overlays. A full-content transparent alpha
|
|
99
|
+
// bitmap keeps that clip open in every stretch/rotation mode.
|
|
100
|
+
// Clearing the drawable leaves its cached bitmap shader invalid.
|
|
101
|
+
int pw=Math.max(1,w-view.getPaddingLeft()-view.getPaddingRight());
|
|
102
|
+
int ph=Math.max(1,h-view.getPaddingTop()-view.getPaddingBottom());
|
|
103
|
+
float angle=Math.abs(geometry[0]%180);
|
|
104
|
+
if(angle>45 && angle<135){int swap=pw;pw=ph;ph=swap;}
|
|
105
|
+
placeholderBitmap=Bitmap.createBitmap(pw,ph,Bitmap.Config.ALPHA_8);
|
|
106
|
+
view.setImageBitmap(placeholderBitmap);
|
|
107
|
+
placeholder=view.getDrawable();
|
|
108
|
+
if(mode==1) {
|
|
109
|
+
view.setClickable(true);
|
|
110
|
+
view.setOnTouchListener(this::touch);
|
|
111
|
+
}
|
|
112
|
+
} catch (RuntimeException | OutOfMemoryError error) {
|
|
113
|
+
failureReason=error.toString();
|
|
114
|
+
if(attached) restore(true);
|
|
115
|
+
releaseBitmaps(); shader=null; drawable=null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Match NativeScript's bitmap mapping without baking backgrounds/borders
|
|
120
|
+
* into the texture. The stationary inner corner mask is applied at draw time. */
|
|
121
|
+
private Bitmap raster(Bitmap bitmap,float rotation,int width,int height) {
|
|
122
|
+
Bitmap result=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
|
|
123
|
+
Canvas canvas=new Canvas(result);
|
|
124
|
+
float left=view.getPaddingLeft(),top=view.getPaddingTop();
|
|
125
|
+
float w=width-left-view.getPaddingRight(),h=height-top-view.getPaddingBottom();
|
|
126
|
+
float iw=bitmap.getWidth(),ih=bitmap.getHeight();
|
|
127
|
+
Matrix matrix=new Matrix();matrix.postRotate(rotation,iw/2,ih/2);
|
|
128
|
+
float angle=Math.abs(rotation%180);
|
|
129
|
+
if(angle>45 && angle<135){matrix.postTranslate((ih-iw)/2,(iw-ih)/2);float swap=iw;iw=ih;ih=swap;}
|
|
130
|
+
float sx=w/iw,sy=h/ih;
|
|
131
|
+
RectF visible=new RectF(left,top,left+w,top+h);
|
|
132
|
+
if(originalScale==ImageView.ScaleType.FIT_XY) matrix.postScale(sx,sy);
|
|
133
|
+
else if(originalScale==ImageView.ScaleType.MATRIX) visible.intersect(left,top,left+iw,top+ih);
|
|
134
|
+
else {
|
|
135
|
+
float scale=originalScale==ImageView.ScaleType.CENTER?1:originalScale==ImageView.ScaleType.CENTER_CROP?Math.max(sx,sy):Math.min(sx,sy);
|
|
136
|
+
matrix.postTranslate((w-iw)/2,(h-ih)/2);
|
|
137
|
+
matrix.postScale(scale,scale,w/2,h/2);
|
|
138
|
+
visible.intersect(left+(w-iw*scale)/2,top+(h-ih*scale)/2,left+(w+iw*scale)/2,top+(h+ih*scale)/2);
|
|
139
|
+
}
|
|
140
|
+
matrix.postTranslate(left,top);
|
|
141
|
+
BitmapShader pixels=new BitmapShader(bitmap,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP);
|
|
142
|
+
pixels.setLocalMatrix(matrix);
|
|
143
|
+
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG);
|
|
144
|
+
paint.setShader(pixels);paint.setColorFilter(view.getColorFilter());
|
|
145
|
+
canvas.drawRect(visible,paint);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public boolean isSupported() { return drawable!=null && !disposed; }
|
|
150
|
+
public String getFailureReason() { return failureReason; }
|
|
151
|
+
public float getProgress() { return progress; }
|
|
152
|
+
public long getFrames() { return frames; }
|
|
153
|
+
public long getLongestFrameNanos() { return longestFrame; }
|
|
154
|
+
public long getTextureBytes() { return source==null?0:(long)source.getAllocationByteCount()+(destination==null?0:destination.getAllocationByteCount())+(placeholderBitmap==null?0:placeholderBitmap.getAllocationByteCount()); }
|
|
155
|
+
public boolean isPaused() { return paused; }
|
|
156
|
+
|
|
157
|
+
public void play(float destinationProgress) {
|
|
158
|
+
if(disposed || drawable==null || mode==1) return;
|
|
159
|
+
goal=destinationProgress; paused=false;
|
|
160
|
+
animate(progress,goal,Math.max(1,(long)(duration*Math.abs(goal-progress))),false);
|
|
161
|
+
}
|
|
162
|
+
public void pause() {
|
|
163
|
+
paused=true;
|
|
164
|
+
if(animator!=null) animator.pause();
|
|
165
|
+
}
|
|
166
|
+
public void resume() {
|
|
167
|
+
if(disposed) return;
|
|
168
|
+
paused=false;
|
|
169
|
+
lastFrame=0;
|
|
170
|
+
if(animator!=null && animator.isPaused()) animator.resume();
|
|
171
|
+
}
|
|
172
|
+
public void seek(float value) {
|
|
173
|
+
if(disposed || mode==1) return;
|
|
174
|
+
boolean hold=paused;
|
|
175
|
+
cancelAnimator(); progress=value; update();
|
|
176
|
+
if(!hold) play(goal);
|
|
177
|
+
}
|
|
178
|
+
public void finish(float endpoint) {
|
|
179
|
+
if(disposed || mode==1) return;
|
|
180
|
+
cancelAnimator(); progress=endpoint; update();
|
|
181
|
+
listener.onEnd(true,progress);
|
|
182
|
+
}
|
|
183
|
+
public void stop() {
|
|
184
|
+
detach(true);
|
|
185
|
+
}
|
|
186
|
+
public void stopPreservingImage() {
|
|
187
|
+
detach(false);
|
|
188
|
+
}
|
|
189
|
+
private void detach(boolean restoreImage) {
|
|
190
|
+
if(disposed) return;
|
|
191
|
+
disposed=true; cancelAnimator();
|
|
192
|
+
restore(restoreImage); shader=null; drawable=null; releaseBitmaps();
|
|
193
|
+
}
|
|
194
|
+
private void restore(boolean restoreImage) {
|
|
195
|
+
if(attached && drawable!=null) view.getOverlay().remove(drawable);
|
|
196
|
+
if(restoreImage && attached && view.getDrawable()==placeholder) {
|
|
197
|
+
// NativeScript caches mBitmap separately from its drawable. Restoring
|
|
198
|
+
// only the drawable leaves the placeholder's geometry behind.
|
|
199
|
+
view.setImageBitmap(originalBitmap);
|
|
200
|
+
}
|
|
201
|
+
if(mode==1 && drawable!=null) {
|
|
202
|
+
view.setOnTouchListener(previousTouch); view.setClickable(originalClickable);
|
|
203
|
+
if(view.getParent()!=null) view.getParent().requestDisallowInterceptTouchEvent(false);
|
|
204
|
+
}
|
|
205
|
+
attached=false;
|
|
206
|
+
}
|
|
207
|
+
private void releaseBitmaps() {
|
|
208
|
+
// Draw commands may still reference them; let Android retire the backing storage safely.
|
|
209
|
+
source=null; destination=null; placeholderBitmap=null;
|
|
210
|
+
}
|
|
211
|
+
private void cancelAnimator() {
|
|
212
|
+
if(animator==null) return;
|
|
213
|
+
suppressEnd=true; animator.cancel(); animator=null; suppressEnd=false;
|
|
214
|
+
}
|
|
215
|
+
private void animate(float from,float to,long milliseconds,boolean settle) {
|
|
216
|
+
cancelAnimator();
|
|
217
|
+
lastFrame=0;
|
|
218
|
+
final float startDx=dx,startDy=dy;
|
|
219
|
+
animator=ValueAnimator.ofFloat(from,to);
|
|
220
|
+
animator.setDuration(milliseconds);
|
|
221
|
+
animator.setInterpolator(new android.view.animation.LinearInterpolator());
|
|
222
|
+
animator.addUpdateListener(a->{
|
|
223
|
+
float value=(float)a.getAnimatedValue();
|
|
224
|
+
if(settle) {float t=value; float decay=(float)(Math.exp(-8*t)*(1-t)); dx=startDx*decay; dy=startDy*decay;}
|
|
225
|
+
else progress=value;
|
|
226
|
+
update();
|
|
227
|
+
});
|
|
228
|
+
animator.addListener(new AnimatorListenerAdapter(){
|
|
229
|
+
@Override public void onAnimationEnd(Animator a){
|
|
230
|
+
if(suppressEnd || disposed) return;
|
|
231
|
+
animator=null;
|
|
232
|
+
if(settle) {dx=dy=0;update();}
|
|
233
|
+
else listener.onEnd(true,progress);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
animator.start();
|
|
237
|
+
}
|
|
238
|
+
private boolean touch(View target,MotionEvent event) {
|
|
239
|
+
if(disposed || paused) return false;
|
|
240
|
+
int action=event.getActionMasked();
|
|
241
|
+
if(action==MotionEvent.ACTION_DOWN) {
|
|
242
|
+
cancelAnimator(); pointer=event.getPointerId(0);
|
|
243
|
+
x=startX=event.getX(); y=startY=event.getY(); dx=dy=rawX=rawY=0;
|
|
244
|
+
update(); return true;
|
|
245
|
+
}
|
|
246
|
+
int index=event.findPointerIndex(pointer);
|
|
247
|
+
if(action==MotionEvent.ACTION_MOVE && index>=0) {
|
|
248
|
+
float vx=event.getX(index)-startX,vy=event.getY(index)-startY;
|
|
249
|
+
rawX=vx;rawY=vy;
|
|
250
|
+
float length=(float)Math.hypot(vx,vy);
|
|
251
|
+
float factor=length==0?0:(float)(amplitude*Math.tanh(length/Math.max(amplitude,1))/length);
|
|
252
|
+
dx=vx*factor; dy=vy*factor; update(); return true;
|
|
253
|
+
}
|
|
254
|
+
if(action==MotionEvent.ACTION_POINTER_UP && event.getPointerId(event.getActionIndex())==pointer) {
|
|
255
|
+
int next=event.getActionIndex()==0?1:0;
|
|
256
|
+
if(next<event.getPointerCount()) {
|
|
257
|
+
pointer=event.getPointerId(next); startX=event.getX(next)-rawX;startY=event.getY(next)-rawY;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if(action==MotionEvent.ACTION_UP || action==MotionEvent.ACTION_CANCEL) {
|
|
261
|
+
pointer=-1; animate(0,1,Math.min(600,duration),true);
|
|
262
|
+
if(action==MotionEvent.ACTION_UP) view.performClick();
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
private void update() {
|
|
267
|
+
if(shader==null || disposed) return;
|
|
268
|
+
shader.setFloatUniform("origin",x,y);
|
|
269
|
+
shader.setFloatUniform("displacement",dx,dy);
|
|
270
|
+
shader.setFloatUniform("progress",progress);
|
|
271
|
+
if(drawable!=null) drawable.invalidateSelf();
|
|
272
|
+
}
|
|
273
|
+
private final class EffectDrawable extends Drawable {
|
|
274
|
+
private final Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
275
|
+
private final int width,height;
|
|
276
|
+
EffectDrawable(int width,int height) {this.width=width;this.height=height;paint.setShader(shader);}
|
|
277
|
+
@Override public int getIntrinsicWidth(){return width;}
|
|
278
|
+
@Override public int getIntrinsicHeight(){return height;}
|
|
279
|
+
@Override public void draw(Canvas canvas){
|
|
280
|
+
long now=System.nanoTime(); if(lastFrame!=0 && animator!=null && !paused) longestFrame=Math.max(longestFrame,now-lastFrame);
|
|
281
|
+
lastFrame=now; frames++;
|
|
282
|
+
canvas.drawPath(contentClip,paint);
|
|
283
|
+
}
|
|
284
|
+
@Override public void setAlpha(int alpha){paint.setAlpha(alpha);invalidateSelf();}
|
|
285
|
+
@Override public void setColorFilter(ColorFilter filter){paint.setColorFilter(filter);invalidateSelf();}
|
|
286
|
+
@Override public int getOpacity(){return PixelFormat.TRANSLUCENT;}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.animation.ValueAnimator;
|
|
4
|
+
import android.graphics.RectF;
|
|
5
|
+
import android.view.*;
|
|
6
|
+
import android.widget.ImageView;
|
|
7
|
+
|
|
8
|
+
/** Focal zoom, bounds, pan and dismissal; no pointer/frame callback crosses to JS. */
|
|
9
|
+
public final class PhotoZoom implements View.OnTouchListener, View.OnLayoutChangeListener {
|
|
10
|
+
public interface Listener {
|
|
11
|
+
void onDismiss(float x,float y,float scale,float velocity,float progress);
|
|
12
|
+
void onSettled(float x,float y,float scale);
|
|
13
|
+
}
|
|
14
|
+
private final ImageView image;
|
|
15
|
+
private final View viewport,backdrop,chrome;
|
|
16
|
+
private final Listener listener;
|
|
17
|
+
private final View.OnTouchListener previousTouch,previousImageTouch;
|
|
18
|
+
private final ScaleGestureDetector pinch;
|
|
19
|
+
private final GestureDetector taps;
|
|
20
|
+
private final float minimum,maximum,doubleTap,density,slop;
|
|
21
|
+
private final float bitmapWidth,bitmapHeight;
|
|
22
|
+
private final boolean dismiss,originalClickable,originalImageClickable,originalClip,originalClipPadding;
|
|
23
|
+
private final float originalScaleX,originalScaleY,originalX,originalY,backdropAlpha,chromeAlpha;
|
|
24
|
+
private final RectF content=new RectF();
|
|
25
|
+
private float anchorX,anchorY,width,height,x,y,scale,progress;
|
|
26
|
+
private float lastX,lastY,downX,downY,lastFocusX,lastFocusY,vx,vy;
|
|
27
|
+
private float rawPanX,rawPanY,pinchFloor;
|
|
28
|
+
private int pointer=-1,mode; // 0 undecided, 1 pan, 2 dismiss, 3 handed to parent
|
|
29
|
+
private boolean disposed,pending,reduced,active=true;
|
|
30
|
+
private VelocityTracker velocity;
|
|
31
|
+
private ValueAnimator animation;
|
|
32
|
+
|
|
33
|
+
public PhotoZoom(ImageView image,View viewport,float bitmapWidth,float bitmapHeight,float minimum,float maximum,float doubleTap,
|
|
34
|
+
boolean dismiss,boolean reduced,View backdrop,View chrome,
|
|
35
|
+
View.OnTouchListener previousTouch,View.OnTouchListener previousImageTouch,Listener listener) {
|
|
36
|
+
this.image=image;this.viewport=viewport;this.minimum=minimum;this.maximum=maximum;
|
|
37
|
+
this.bitmapWidth=bitmapWidth;this.bitmapHeight=bitmapHeight;
|
|
38
|
+
this.doubleTap=doubleTap;this.dismiss=dismiss;this.reduced=reduced;
|
|
39
|
+
this.backdrop=backdrop;this.chrome=chrome;this.previousTouch=previousTouch;this.listener=listener;
|
|
40
|
+
this.previousImageTouch=previousImageTouch;
|
|
41
|
+
density=image.getResources().getDisplayMetrics().density;
|
|
42
|
+
slop=ViewConfiguration.get(image.getContext()).getScaledTouchSlop();
|
|
43
|
+
originalScaleX=image.getScaleX();originalScaleY=image.getScaleY();
|
|
44
|
+
originalX=image.getTranslationX();originalY=image.getTranslationY();
|
|
45
|
+
originalClickable=viewport.isClickable();
|
|
46
|
+
originalImageClickable=image.isClickable();
|
|
47
|
+
originalClip=!(viewport instanceof ViewGroup)||((ViewGroup)viewport).getClipChildren();
|
|
48
|
+
originalClipPadding=!(viewport instanceof ViewGroup)||((ViewGroup)viewport).getClipToPadding();
|
|
49
|
+
backdropAlpha=backdrop==null?1:backdrop.getAlpha();chromeAlpha=chrome==null?1:chrome.getAlpha();
|
|
50
|
+
scale=minimum;
|
|
51
|
+
pinch=new ScaleGestureDetector(image.getContext(),new ScaleGestureDetector.SimpleOnScaleGestureListener(){
|
|
52
|
+
@Override public boolean onScaleBegin(ScaleGestureDetector detector){
|
|
53
|
+
if(pending || disposed) return false;
|
|
54
|
+
cancelAnimation();mode=1;pinchFloor=Math.min(minimum,scale);
|
|
55
|
+
lastFocusX=detector.getFocusX();lastFocusY=detector.getFocusY();
|
|
56
|
+
disallow(true);return true;
|
|
57
|
+
}
|
|
58
|
+
@Override public boolean onScale(ScaleGestureDetector detector){
|
|
59
|
+
float next=clamp(scale*detector.getScaleFactor(),pinchFloor,maximum);
|
|
60
|
+
float fx=detector.getFocusX(),fy=detector.getFocusY();
|
|
61
|
+
x=fx-anchorX-(lastFocusX-anchorX-x)*next/scale;
|
|
62
|
+
y=fy-anchorY-(lastFocusY-anchorY-y)*next/scale;
|
|
63
|
+
scale=next;lastFocusX=fx;lastFocusY=fy;
|
|
64
|
+
progress=clamp((minimum-scale)/(minimum*.15f),0,1);
|
|
65
|
+
rebasePan();
|
|
66
|
+
apply();return true;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
taps=new GestureDetector(image.getContext(),new GestureDetector.SimpleOnGestureListener(){
|
|
70
|
+
@Override public boolean onDown(MotionEvent e){return true;}
|
|
71
|
+
@Override public boolean onDoubleTap(MotionEvent e){
|
|
72
|
+
if(!pending) zoom(scale>minimum+.01f?minimum:Math.min(maximum,doubleTap),e.getX(),e.getY(),true);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
@Override public boolean onSingleTapConfirmed(MotionEvent e){viewport.performClick();return true;}
|
|
76
|
+
});
|
|
77
|
+
viewport.setClickable(true);viewport.setOnTouchListener(this);
|
|
78
|
+
image.setClickable(true);image.setOnTouchListener((v,event)->{
|
|
79
|
+
MotionEvent local=MotionEvent.obtain(event);
|
|
80
|
+
local.transform(image.getMatrix());
|
|
81
|
+
local.offsetLocation(image.getLeft()-viewport.getScrollX(),image.getTop()-viewport.getScrollY());
|
|
82
|
+
boolean consumed=onTouch(viewport,local);local.recycle();return consumed;
|
|
83
|
+
});
|
|
84
|
+
if(viewport instanceof ViewGroup){((ViewGroup)viewport).setClipChildren(true);((ViewGroup)viewport).setClipToPadding(true);}
|
|
85
|
+
viewport.addOnLayoutChangeListener(this);image.addOnLayoutChangeListener(this);
|
|
86
|
+
measure();apply();
|
|
87
|
+
}
|
|
88
|
+
public float getX(){return x/density;}
|
|
89
|
+
public float getY(){return y/density;}
|
|
90
|
+
public float getScale(){return scale;}
|
|
91
|
+
public float getDismissProgress(){return progress;}
|
|
92
|
+
public boolean isDismissPending(){return pending;}
|
|
93
|
+
public void setReduced(boolean value){reduced=value;if(value){if(pending)reset(false);else settle(false);}}
|
|
94
|
+
public void setActive(boolean value){active=value;if(!value){cancelAnimation();endTouch();pending=false;settle(false);}}
|
|
95
|
+
public void zoom(float value,float focusX,float focusY,boolean animated){
|
|
96
|
+
if(disposed || pending) return;
|
|
97
|
+
cancelAnimation();
|
|
98
|
+
float next=clamp(value,minimum,maximum);
|
|
99
|
+
float tx=focusX-anchorX-(focusX-anchorX-x)*next/scale;
|
|
100
|
+
float ty=focusY-anchorY-(focusY-anchorY-y)*next/scale;
|
|
101
|
+
float[] bounds=bounds(next);
|
|
102
|
+
spring(clamp(tx,bounds[0],bounds[1]),clamp(ty,bounds[2],bounds[3]),next,0,0,animated,false);
|
|
103
|
+
}
|
|
104
|
+
public void reset(boolean animated){
|
|
105
|
+
if(disposed)return;
|
|
106
|
+
pending=false;mode=0;float[] b=bounds(minimum);
|
|
107
|
+
spring(clamp(0,b[0],b[1]),clamp(0,b[2],b[3]),minimum,0,0,animated,false);
|
|
108
|
+
}
|
|
109
|
+
public void requestDismiss(){
|
|
110
|
+
if(disposed || pending || !dismiss)return;
|
|
111
|
+
// Programmatic close normalizes zoom before emitting one route intent.
|
|
112
|
+
float[] b=bounds(minimum);
|
|
113
|
+
spring(clamp(0,b[0],b[1]),clamp(0,b[2],b[3]),minimum,0,0,true,true);
|
|
114
|
+
}
|
|
115
|
+
public void stop(){
|
|
116
|
+
if(disposed)return;
|
|
117
|
+
disposed=true;cancelAnimation();endTouch();
|
|
118
|
+
viewport.setOnTouchListener(previousTouch);viewport.setClickable(originalClickable);
|
|
119
|
+
image.setOnTouchListener(previousImageTouch);image.setClickable(originalImageClickable);
|
|
120
|
+
if(viewport instanceof ViewGroup){((ViewGroup)viewport).setClipChildren(originalClip);((ViewGroup)viewport).setClipToPadding(originalClipPadding);}
|
|
121
|
+
viewport.removeOnLayoutChangeListener(this);image.removeOnLayoutChangeListener(this);
|
|
122
|
+
image.setScaleX(originalScaleX);image.setScaleY(originalScaleY);
|
|
123
|
+
image.setTranslationX(originalX);image.setTranslationY(originalY);
|
|
124
|
+
if(backdrop!=null)backdrop.setAlpha(backdropAlpha);if(chrome!=null)chrome.setAlpha(chromeAlpha);
|
|
125
|
+
}
|
|
126
|
+
private void measure(){
|
|
127
|
+
width=viewport.getWidth();height=viewport.getHeight();
|
|
128
|
+
float iw=image.getWidth(),ih=image.getHeight();
|
|
129
|
+
float left=image.getPaddingLeft(),top=image.getPaddingTop();
|
|
130
|
+
float cw=iw-left-image.getPaddingRight(),ch=ih-top-image.getPaddingBottom();
|
|
131
|
+
float dw=Math.max(1,bitmapWidth),dh=Math.max(1,bitmapHeight);
|
|
132
|
+
ImageView.ScaleType fit=image.getScaleType();
|
|
133
|
+
float factor=fit==ImageView.ScaleType.CENTER_CROP?Math.max(cw/dw,ch/dh):Math.min(cw/dw,ch/dh);
|
|
134
|
+
if(fit==ImageView.ScaleType.FIT_XY){dw=cw;dh=ch;}
|
|
135
|
+
else if(fit!=ImageView.ScaleType.CENTER && fit!=ImageView.ScaleType.MATRIX){dw*=factor;dh*=factor;}
|
|
136
|
+
anchorX=image.getLeft()+image.getPivotX()+originalX;
|
|
137
|
+
anchorY=image.getTop()+image.getPivotY()+originalY;
|
|
138
|
+
// ImageView clips the raster before its outer transform is applied.
|
|
139
|
+
float ox=left+(fit==ImageView.ScaleType.MATRIX?0:(cw-dw)/2),oy=top+(fit==ImageView.ScaleType.MATRIX?0:(ch-dh)/2);
|
|
140
|
+
content.set((Math.max(left,ox)-image.getPivotX())*originalScaleX,
|
|
141
|
+
(Math.max(top,oy)-image.getPivotY())*originalScaleY,
|
|
142
|
+
(Math.min(left+cw,ox+dw)-image.getPivotX())*originalScaleX,
|
|
143
|
+
(Math.min(top+ch,oy+dh)-image.getPivotY())*originalScaleY);
|
|
144
|
+
}
|
|
145
|
+
private float[] bounds(float s){
|
|
146
|
+
float loX=width-anchorX-content.right*s,hiX=-anchorX-content.left*s;
|
|
147
|
+
float loY=height-anchorY-content.bottom*s,hiY=-anchorY-content.top*s;
|
|
148
|
+
if(loX>hiX)loX=hiX=(loX+hiX)/2;
|
|
149
|
+
if(loY>hiY)loY=hiY=(loY+hiY)/2;
|
|
150
|
+
return new float[]{loX,hiX,loY,hiY};
|
|
151
|
+
}
|
|
152
|
+
private void apply(){
|
|
153
|
+
image.setScaleX(originalScaleX*scale);image.setScaleY(originalScaleY*scale);
|
|
154
|
+
image.setTranslationX(originalX+x);image.setTranslationY(originalY+y);
|
|
155
|
+
if(backdrop!=null)backdrop.setAlpha(backdropAlpha*(1-progress));
|
|
156
|
+
if(chrome!=null)chrome.setAlpha(chromeAlpha*(1-progress));
|
|
157
|
+
}
|
|
158
|
+
private void settle(boolean animated){
|
|
159
|
+
if(disposed)return;
|
|
160
|
+
float s=clamp(scale,minimum,maximum),b[]=bounds(s);
|
|
161
|
+
float tx=clamp(x,b[0],b[1]),ty=clamp(y,b[2],b[3]);
|
|
162
|
+
if(mode==2){s=minimum;b=bounds(s);tx=clamp(0,b[0],b[1]);ty=clamp(0,b[2],b[3]);}
|
|
163
|
+
else {tx=clamp(tx+vx*.12f,b[0],b[1]);ty=clamp(ty+vy*.12f,b[2],b[3]);}
|
|
164
|
+
spring(tx,ty,s,vx,vy,animated,false);
|
|
165
|
+
}
|
|
166
|
+
private void cancelAnimation(){if(animation!=null){animation.cancel();animation=null;}}
|
|
167
|
+
private void spring(float tx,float ty,float targetScale,float velocityX,float velocityY,boolean animated,boolean close){
|
|
168
|
+
cancelAnimation();
|
|
169
|
+
final float sx=x,sy=y,ss=scale,sp=progress;
|
|
170
|
+
if(!animated || reduced){x=tx;y=ty;scale=targetScale;progress=0;apply();if(close)emitDismiss(0);else listener.onSettled(x/density,y/density,scale);return;}
|
|
171
|
+
animation=ValueAnimator.ofFloat(0,1);animation.setDuration(550);
|
|
172
|
+
animation.setInterpolator(new android.view.animation.LinearInterpolator());
|
|
173
|
+
animation.addUpdateListener(a->{
|
|
174
|
+
float fraction=(float)a.getAnimatedValue(),t=fraction*.55f;
|
|
175
|
+
float decay=(float)Math.exp(-16*t);
|
|
176
|
+
x=tx+((sx-tx)+(velocityX+16*(sx-tx))*t)*decay;
|
|
177
|
+
y=ty+((sy-ty)+(velocityY+16*(sy-ty))*t)*decay;
|
|
178
|
+
scale=targetScale+(ss-targetScale)*(1+16*t)*decay;
|
|
179
|
+
progress=sp*(1+16*t)*decay;
|
|
180
|
+
if(fraction>=1){x=tx;y=ty;scale=targetScale;progress=0;}
|
|
181
|
+
apply();
|
|
182
|
+
if(fraction>=1){animation=null;if(close)emitDismiss(0);else listener.onSettled(x/density,y/density,scale);}
|
|
183
|
+
});animation.start();
|
|
184
|
+
}
|
|
185
|
+
private void emitDismiss(float velocityY){
|
|
186
|
+
if(pending || disposed)return;
|
|
187
|
+
pending=true;listener.onDismiss(x/density,y/density,scale,velocityY/density,progress);
|
|
188
|
+
}
|
|
189
|
+
private void disallow(boolean value){if(viewport.getParent()!=null)viewport.getParent().requestDisallowInterceptTouchEvent(value);}
|
|
190
|
+
private void endTouch(){pointer=-1;if(velocity!=null){velocity.recycle();velocity=null;}disallow(false);}
|
|
191
|
+
@Override public boolean onTouch(View view,MotionEvent event){
|
|
192
|
+
if(disposed || !active || pending)return false;
|
|
193
|
+
int action=event.getActionMasked();
|
|
194
|
+
if(action==MotionEvent.ACTION_DOWN){
|
|
195
|
+
cancelAnimation();mode=0;vx=vy=0;
|
|
196
|
+
pointer=event.getPointerId(0);lastX=downX=event.getX();lastY=downY=event.getY();
|
|
197
|
+
rebasePan();
|
|
198
|
+
if(velocity!=null)velocity.recycle();velocity=VelocityTracker.obtain();disallow(true);
|
|
199
|
+
}
|
|
200
|
+
if(velocity!=null)velocity.addMovement(event);
|
|
201
|
+
pinch.onTouchEvent(event);taps.onTouchEvent(event);
|
|
202
|
+
int index=event.findPointerIndex(pointer);
|
|
203
|
+
if(action==MotionEvent.ACTION_MOVE && !pinch.isInProgress() && event.getPointerCount()==1 && index>=0){
|
|
204
|
+
float px=event.getX(index),py=event.getY(index),totalX=px-downX,totalY=py-downY;
|
|
205
|
+
if(mode==0 && Math.hypot(totalX,totalY)>slop){
|
|
206
|
+
if(scale<=minimum+.001f){
|
|
207
|
+
if(dismiss && Math.abs(totalY)>Math.abs(totalX)*1.2f)mode=2;
|
|
208
|
+
else {mode=3;disallow(false);return false;}
|
|
209
|
+
}else mode=1;
|
|
210
|
+
}
|
|
211
|
+
if(mode==1){
|
|
212
|
+
float[] b=bounds(scale);
|
|
213
|
+
rawPanX+=px-lastX;rawPanY+=py-lastY;
|
|
214
|
+
x=rubber(rawPanX,b[0],b[1],width);y=rubber(rawPanY,b[2],b[3],height);
|
|
215
|
+
}else if(mode==2){
|
|
216
|
+
x+= (px-lastX)*.4f;y+=py-lastY;
|
|
217
|
+
progress=clamp(Math.abs(y)/Math.max(height*.5f,1),0,1);
|
|
218
|
+
scale=minimum*(1-progress*.15f);
|
|
219
|
+
}
|
|
220
|
+
lastX=px;lastY=py;apply();
|
|
221
|
+
}
|
|
222
|
+
if(action==MotionEvent.ACTION_POINTER_UP){
|
|
223
|
+
int next=event.getActionIndex()==0?1:0;
|
|
224
|
+
if(next<event.getPointerCount()){pointer=event.getPointerId(next);lastX=downX=event.getX(next);lastY=downY=event.getY(next);rebasePan();}
|
|
225
|
+
}
|
|
226
|
+
if(action==MotionEvent.ACTION_UP || action==MotionEvent.ACTION_CANCEL){
|
|
227
|
+
if(velocity!=null){velocity.computeCurrentVelocity(1000);vx=velocity.getXVelocity(pointer);vy=velocity.getYVelocity(pointer);}
|
|
228
|
+
boolean commit=action==MotionEvent.ACTION_UP && mode==2 && (progress>.35f || (Math.abs(vy)>1000*density && y*vy>0));
|
|
229
|
+
boolean wasAnimation=animation!=null;
|
|
230
|
+
endTouch();
|
|
231
|
+
if(commit)emitDismiss(vy);
|
|
232
|
+
else if(mode!=3 && !wasAnimation)settle(true);
|
|
233
|
+
}
|
|
234
|
+
return mode!=3;
|
|
235
|
+
}
|
|
236
|
+
@Override public void onLayoutChange(View v,int l,int t,int r,int b,int ol,int ot,int or,int ob){
|
|
237
|
+
if(disposed || (l==ol&&t==ot&&r==or&&b==ob))return;
|
|
238
|
+
cancelAnimation();pending=false;measure();vx=vy=0;mode=0;settle(false);
|
|
239
|
+
}
|
|
240
|
+
private static float clamp(float v,float lo,float hi){return Math.max(lo,Math.min(hi,v));}
|
|
241
|
+
private void rebasePan(){float[] b=bounds(scale);rawPanX=unrubber(x,b[0],b[1],width);rawPanY=unrubber(y,b[2],b[3],height);}
|
|
242
|
+
private static float unrubber(float v,float lo,float hi,float dimension){
|
|
243
|
+
float edge=clamp(v,lo,hi),d=v-edge,size=Math.max(1,dimension);
|
|
244
|
+
return edge+d/(.55f*Math.max(.001f,1-Math.abs(d)/size));
|
|
245
|
+
}
|
|
246
|
+
private static float rubber(float value,float lo,float hi,float dimension){
|
|
247
|
+
float edge=clamp(value,lo,hi),d=value-edge;
|
|
248
|
+
return edge+Math.signum(d)*dimension*(1-1/(Math.abs(d)*.55f/Math.max(1,dimension)+1));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -11,3 +11,7 @@
|
|
|
11
11
|
-keep class org.rafaelaugusto.jolt.TextReveal$* { *; }
|
|
12
12
|
-keep class org.rafaelaugusto.jolt.NumberRoll { *; }
|
|
13
13
|
-keep class org.rafaelaugusto.jolt.NumberRoll$* { *; }
|
|
14
|
+
-keep class org.rafaelaugusto.jolt.ImageEffect { *; }
|
|
15
|
+
-keep class org.rafaelaugusto.jolt.ImageEffect$* { *; }
|
|
16
|
+
-keep class org.rafaelaugusto.jolt.PhotoZoom { *; }
|
|
17
|
+
-keep class org.rafaelaugusto.jolt.PhotoZoom$* { *; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
OTHER_LDFLAGS = $(inherited) -framework Metal -framework MetalKit
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
@interface JoltImageEffect : NSObject
|
|
5
|
+
+ (nullable instancetype)createWithView:(UIImageView *)view
|
|
6
|
+
destination:(nullable UIImage *)destination
|
|
7
|
+
mode:(NSInteger)mode
|
|
8
|
+
originX:(double)x originY:(double)y
|
|
9
|
+
amplitude:(double)amplitude radius:(double)radius
|
|
10
|
+
duration:(double)duration seed:(double)seed
|
|
11
|
+
listener:(void (^)(BOOL, double))listener;
|
|
12
|
+
@property(nonatomic, readonly) double progress;
|
|
13
|
+
@property(nonatomic, readonly) BOOL paused;
|
|
14
|
+
@property(nonatomic, readonly) NSUInteger frames;
|
|
15
|
+
@property(nonatomic, readonly) double longestFrame;
|
|
16
|
+
@property(nonatomic, readonly) NSUInteger textureBytes;
|
|
17
|
+
- (void)play:(double)endpoint;
|
|
18
|
+
- (void)pause;
|
|
19
|
+
- (void)resume;
|
|
20
|
+
- (void)seek:(double)progress;
|
|
21
|
+
- (void)finish:(double)endpoint;
|
|
22
|
+
- (void)stop;
|
|
23
|
+
- (void)stopPreservingImage;
|
|
24
|
+
@end
|
|
25
|
+
NS_ASSUME_NONNULL_END
|