@rafaelaugusto/jolt 0.1.0-beta.6 → 0.1.0-beta.8
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 +21 -4
- package/dist/animate.d.ts +2 -0
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +7 -2
- package/dist/animate.js.map +1 -1
- package/dist/drag-input.d.ts +15 -0
- package/dist/drag-input.d.ts.map +1 -0
- package/dist/drag-input.js +66 -0
- package/dist/drag-input.js.map +1 -0
- package/dist/drag-physics.d.ts +20 -0
- package/dist/drag-physics.d.ts.map +1 -0
- package/dist/drag-physics.js +75 -0
- package/dist/drag-physics.js.map +1 -0
- package/dist/drag.d.ts +58 -0
- package/dist/drag.d.ts.map +1 -0
- package/dist/drag.js +324 -0
- package/dist/drag.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +38 -6
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +9 -0
- package/dist/engine.js.map +1 -1
- 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 +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/interaction-surface.d.ts +12 -0
- package/dist/interaction-surface.d.ts.map +1 -0
- package/dist/interaction-surface.js +36 -0
- package/dist/interaction-surface.js.map +1 -0
- package/dist/liquid.d.ts.map +1 -1
- package/dist/liquid.js +4 -0
- package/dist/liquid.js.map +1 -1
- package/dist/motion-scope.d.ts +37 -0
- package/dist/motion-scope.d.ts.map +1 -0
- package/dist/motion-scope.js +232 -0
- package/dist/motion-scope.js.map +1 -0
- package/dist/motion-states.d.ts +36 -0
- package/dist/motion-states.d.ts.map +1 -0
- package/dist/motion-states.js +165 -0
- package/dist/motion-states.js.map +1 -0
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +14 -3
- package/dist/press.js.map +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +21 -4
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +2 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js.map +1 -1
- package/dist/vue/index.d.ts +3 -1
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +17 -4
- package/dist/vue/index.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/JoltInteractiveAnimation.h +18 -0
- package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
- package/platforms/ios/src/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +6 -0
- package/src/android-plugin.d.ts +78 -0
- package/src/animate.ts +15 -8
- package/src/drag-input.ts +74 -0
- package/src/drag-physics.ts +110 -0
- package/src/drag.ts +395 -0
- package/src/driver/ios.ts +57 -8
- package/src/engine.ts +11 -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 +23 -0
- package/src/interaction-surface.ts +41 -0
- package/src/ios-plugin.d.ts +81 -0
- package/src/liquid.ts +4 -0
- package/src/motion-scope.ts +265 -0
- package/src/motion-states.ts +215 -0
- package/src/press.ts +15 -3
- package/src/track.ts +25 -4
- package/src/transition.ts +2 -0
- package/src/vue/index.ts +20 -3
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
|
@@ -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
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#import "JoltImageEffect.h"
|
|
2
|
+
#import <MetalKit/MetalKit.h>
|
|
3
|
+
#import <simd/simd.h>
|
|
4
|
+
|
|
5
|
+
static const char *JoltImageShader = R"METAL(
|
|
6
|
+
#include <metal_stdlib>
|
|
7
|
+
using namespace metal;
|
|
8
|
+
struct Vertex {float4 position [[position]]; float2 uv;};
|
|
9
|
+
struct Params {float2 size; float2 origin; float2 displacement; float progress; float amplitude; float radius; float seed; float mode;};
|
|
10
|
+
vertex Vertex joltVertex(uint id [[vertex_id]]) {
|
|
11
|
+
float2 p[4]={float2(-1,-1),float2(1,-1),float2(-1,1),float2(1,1)};
|
|
12
|
+
float2 uv[4]={float2(0,1),float2(1,1),float2(0,0),float2(1,0)};
|
|
13
|
+
return {float4(p[id],0,1),uv[id]};
|
|
14
|
+
}
|
|
15
|
+
fragment float4 joltImage(Vertex in [[stage_in]], constant Params &u [[buffer(0)]],
|
|
16
|
+
texture2d<float> source [[texture(0)]], texture2d<float> destination [[texture(1)]]) {
|
|
17
|
+
constexpr sampler s(coord::normalized,address::clamp_to_edge,filter::linear);
|
|
18
|
+
float2 p=in.uv*u.size,q=p;
|
|
19
|
+
if(u.mode<1.5) {
|
|
20
|
+
float2 v=p-u.origin; float d=length(v);
|
|
21
|
+
float edge=clamp(min(min(p.x,u.size.x-p.x),min(p.y,u.size.y-p.y))/max(u.amplitude*2.,1.),0.,1.);
|
|
22
|
+
if(u.mode<.5) {
|
|
23
|
+
float wave=d-u.progress*length(u.size);
|
|
24
|
+
float a=sin(wave/max(u.radius*.12,1.))*exp(-pow(wave/max(u.radius,1.),2.))*u.amplitude*sin(u.progress*3.14159265);
|
|
25
|
+
q-=v/max(d,1.)*a*edge;
|
|
26
|
+
} else q-=u.displacement*exp(-d*d/max(u.radius*u.radius,1.))*edge;
|
|
27
|
+
return source.sample(s,q/u.size);
|
|
28
|
+
}
|
|
29
|
+
if(u.progress<=0.) return source.sample(s,in.uv);
|
|
30
|
+
if(u.progress>=1.) return destination.sample(s,in.uv);
|
|
31
|
+
float n=fract(sin(dot(floor(p/3.),float2(12.9898,78.233))+u.seed)*43758.5453);
|
|
32
|
+
float a=smoothstep(n-.08,n+.08,u.progress*1.16-.08);
|
|
33
|
+
return mix(source.sample(s,in.uv),destination.sample(s,in.uv),a);
|
|
34
|
+
}
|
|
35
|
+
)METAL";
|
|
36
|
+
|
|
37
|
+
typedef struct {
|
|
38
|
+
vector_float2 size, origin, displacement;
|
|
39
|
+
float progress, amplitude, radius, seed, mode;
|
|
40
|
+
} JoltImageParams;
|
|
41
|
+
|
|
42
|
+
@interface JoltImageEffect () <MTKViewDelegate, UIGestureRecognizerDelegate>
|
|
43
|
+
@property(nonatomic, weak) UIImageView *image;
|
|
44
|
+
@property(nonatomic, strong) UIImage *original;
|
|
45
|
+
@property(nonatomic, strong) MTKView *surface;
|
|
46
|
+
@property(nonatomic, strong) id<MTLCommandQueue> queue;
|
|
47
|
+
@property(nonatomic, strong) id<MTLRenderPipelineState> pipeline;
|
|
48
|
+
@property(nonatomic, strong) id<MTLTexture> sourceTexture, destinationTexture;
|
|
49
|
+
@property(nonatomic, strong) UIPanGestureRecognizer *pan;
|
|
50
|
+
@property(nonatomic, copy) void (^listener)(BOOL,double);
|
|
51
|
+
@property(nonatomic) BOOL originalInteraction, disposed, playing, settling;
|
|
52
|
+
@property(nonatomic, readwrite) BOOL paused;
|
|
53
|
+
@property(nonatomic, readwrite) double progress;
|
|
54
|
+
@property(nonatomic, readwrite) NSUInteger frames;
|
|
55
|
+
@property(nonatomic, readwrite) double longestFrame;
|
|
56
|
+
@property(nonatomic, readwrite) NSUInteger textureBytes;
|
|
57
|
+
@property(nonatomic) double duration, goal, anchor, startProgress, remaining, previousFrame;
|
|
58
|
+
@property(nonatomic) vector_float2 settleStart;
|
|
59
|
+
@property(nonatomic) JoltImageParams params;
|
|
60
|
+
@end
|
|
61
|
+
|
|
62
|
+
@implementation JoltImageEffect
|
|
63
|
+
+ (instancetype)createWithView:(UIImageView *)view destination:(UIImage *)destination mode:(NSInteger)mode
|
|
64
|
+
originX:(double)x originY:(double)y amplitude:(double)amplitude radius:(double)radius
|
|
65
|
+
duration:(double)duration seed:(double)seed listener:(void (^)(BOOL,double))listener {
|
|
66
|
+
if(!view.image.CGImage || view.image.images.count || view.image.renderingMode==UIImageRenderingModeAlwaysTemplate || !view.window) return nil;
|
|
67
|
+
CGFloat scale=view.window.screen.scale;
|
|
68
|
+
NSUInteger w=(NSUInteger)ceil(view.bounds.size.width*scale),h=(NSUInteger)ceil(view.bounds.size.height*scale);
|
|
69
|
+
if(!w || !h || w*h>4194304 || (mode==2 && !destination.CGImage)) return nil;
|
|
70
|
+
id<MTLDevice> device=MTLCreateSystemDefaultDevice();
|
|
71
|
+
if(!device) return nil;
|
|
72
|
+
static id<MTLRenderPipelineState> cached;
|
|
73
|
+
static dispatch_once_t once;
|
|
74
|
+
dispatch_once(&once, ^{
|
|
75
|
+
NSError *error=nil;
|
|
76
|
+
id<MTLLibrary> library=[device newLibraryWithSource:[NSString stringWithUTF8String:JoltImageShader] options:nil error:&error];
|
|
77
|
+
MTLRenderPipelineDescriptor *descriptor=[MTLRenderPipelineDescriptor new];
|
|
78
|
+
descriptor.vertexFunction=[library newFunctionWithName:@"joltVertex"];
|
|
79
|
+
descriptor.fragmentFunction=[library newFunctionWithName:@"joltImage"];
|
|
80
|
+
descriptor.colorAttachments[0].pixelFormat=MTLPixelFormatBGRA8Unorm;
|
|
81
|
+
if(library) cached=[device newRenderPipelineStateWithDescriptor:descriptor error:&error];
|
|
82
|
+
if(error) NSLog(@"Jolt image shader unavailable: %@",error.localizedDescription);
|
|
83
|
+
});
|
|
84
|
+
if(!cached) return nil;
|
|
85
|
+
JoltImageEffect *effect=[JoltImageEffect new];
|
|
86
|
+
effect.image=view; effect.original=view.image; effect.originalInteraction=view.userInteractionEnabled;
|
|
87
|
+
effect.listener=listener; effect.pipeline=cached; effect.queue=[device newCommandQueue];
|
|
88
|
+
effect.duration=duration/1000.; effect.goal=1; effect.paused=YES;
|
|
89
|
+
effect.params=(JoltImageParams){ {(float)w,(float)h}, {(float)(x*scale),(float)(y*scale)}, {0,0}, 0,
|
|
90
|
+
(float)(amplitude*scale),(float)(radius*scale),(float)seed,(float)mode };
|
|
91
|
+
effect.sourceTexture=[effect texture:view.image device:device width:w height:h];
|
|
92
|
+
effect.destinationTexture=mode==2?[effect texture:destination device:device width:w height:h]:effect.sourceTexture;
|
|
93
|
+
if(!effect.sourceTexture || !effect.destinationTexture) return nil;
|
|
94
|
+
effect.textureBytes=w*h*4*(mode==2?2:1);
|
|
95
|
+
MTKView *surface=[[MTKView alloc] initWithFrame:view.bounds device:device];
|
|
96
|
+
surface.opaque=NO; surface.backgroundColor=UIColor.clearColor;
|
|
97
|
+
surface.clearColor=MTLClearColorMake(0,0,0,0);
|
|
98
|
+
surface.userInteractionEnabled=NO; surface.accessibilityElementsHidden=YES;
|
|
99
|
+
surface.colorPixelFormat=MTLPixelFormatBGRA8Unorm;
|
|
100
|
+
surface.autoResizeDrawable=NO; surface.drawableSize=CGSizeMake(w,h);
|
|
101
|
+
surface.preferredFramesPerSecond=MIN(120,view.window.screen.maximumFramesPerSecond);
|
|
102
|
+
surface.paused=YES; surface.enableSetNeedsDisplay=YES;
|
|
103
|
+
surface.delegate=effect; effect.surface=surface;
|
|
104
|
+
[view addSubview:surface];
|
|
105
|
+
// Draw the identity frame before suppressing the original raster.
|
|
106
|
+
[surface draw]; view.image=nil;
|
|
107
|
+
if(mode==1) {
|
|
108
|
+
effect.pan=[[UIPanGestureRecognizer alloc] initWithTarget:effect action:@selector(drag:)];
|
|
109
|
+
effect.pan.delegate=effect; effect.pan.cancelsTouchesInView=NO;
|
|
110
|
+
view.userInteractionEnabled=YES; [view addGestureRecognizer:effect.pan];
|
|
111
|
+
}
|
|
112
|
+
return effect;
|
|
113
|
+
}
|
|
114
|
+
- (id<MTLTexture>)texture:(UIImage *)image device:(id<MTLDevice>)device width:(NSUInteger)w height:(NSUInteger)h {
|
|
115
|
+
if(!image.CGImage) return nil;
|
|
116
|
+
NSMutableData *data=[NSMutableData dataWithLength:w*h*4];
|
|
117
|
+
CGColorSpaceRef space=CGColorSpaceCreateDeviceRGB();
|
|
118
|
+
CGContextRef context=CGBitmapContextCreate(data.mutableBytes,w,h,8,w*4,space,kCGImageAlphaPremultipliedLast|kCGBitmapByteOrder32Big);
|
|
119
|
+
CGColorSpaceRelease(space);
|
|
120
|
+
if(!context) return nil;
|
|
121
|
+
CGContextTranslateCTM(context,0,h); CGContextScaleCTM(context,1,-1);
|
|
122
|
+
CGFloat iw=image.size.width,ih=image.size.height;
|
|
123
|
+
UIViewContentMode mode=self.image.contentMode;
|
|
124
|
+
CGFloat factor=mode==UIViewContentModeScaleAspectFill?MAX(w/iw,h/ih):MIN(w/iw,h/ih);
|
|
125
|
+
BOOL unscaled=mode==UIViewContentModeCenter || mode==UIViewContentModeTopLeft;
|
|
126
|
+
CGFloat dw=mode==UIViewContentModeScaleToFill?w:unscaled?iw*self.image.window.screen.scale:iw*factor;
|
|
127
|
+
CGFloat dh=mode==UIViewContentModeScaleToFill?h:unscaled?ih*self.image.window.screen.scale:ih*factor;
|
|
128
|
+
CGFloat left=mode==UIViewContentModeTopLeft?0:(w-dw)/2,top=mode==UIViewContentModeTopLeft?0:(h-dh)/2;
|
|
129
|
+
UIGraphicsPushContext(context); [image drawInRect:CGRectMake(left,top,dw,dh)]; UIGraphicsPopContext();
|
|
130
|
+
CGContextRelease(context);
|
|
131
|
+
MTLTextureDescriptor *descriptor=[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:w height:h mipmapped:NO];
|
|
132
|
+
descriptor.usage=MTLTextureUsageShaderRead;
|
|
133
|
+
id<MTLTexture> texture=[device newTextureWithDescriptor:descriptor];
|
|
134
|
+
[texture replaceRegion:MTLRegionMake2D(0,0,w,h) mipmapLevel:0 withBytes:data.bytes bytesPerRow:w*4];
|
|
135
|
+
return texture;
|
|
136
|
+
}
|
|
137
|
+
- (void)play:(double)endpoint {
|
|
138
|
+
if(self.disposed || self.params.mode==1) return;
|
|
139
|
+
self.goal=endpoint; self.startProgress=self.progress; self.remaining=MAX(.001,self.duration*fabs(endpoint-self.progress));
|
|
140
|
+
self.anchor=CACurrentMediaTime(); self.playing=YES; self.settling=NO; self.paused=NO;
|
|
141
|
+
self.previousFrame=0; self.surface.enableSetNeedsDisplay=NO; self.surface.paused=NO;
|
|
142
|
+
}
|
|
143
|
+
- (void)pause {
|
|
144
|
+
if(self.disposed) return;
|
|
145
|
+
[self advance:CACurrentMediaTime()]; self.paused=YES;
|
|
146
|
+
self.surface.paused=YES; self.surface.enableSetNeedsDisplay=YES;
|
|
147
|
+
}
|
|
148
|
+
- (void)resume {
|
|
149
|
+
if(self.disposed || !self.paused) return;
|
|
150
|
+
if(self.params.mode==1) {
|
|
151
|
+
self.paused=NO;
|
|
152
|
+
if(self.settling && self.playing) {
|
|
153
|
+
self.settleStart=self.params.displacement;self.anchor=CACurrentMediaTime();
|
|
154
|
+
self.remaining=MIN(.6,self.duration);self.previousFrame=0;
|
|
155
|
+
self.surface.enableSetNeedsDisplay=NO;self.surface.paused=NO;
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
[self play:self.goal];
|
|
160
|
+
}
|
|
161
|
+
- (void)seek:(double)value {
|
|
162
|
+
if(self.disposed || self.params.mode==1) return;
|
|
163
|
+
BOOL hold=self.paused; self.progress=value;
|
|
164
|
+
JoltImageParams p=self.params;p.progress=value;self.params=p;
|
|
165
|
+
[self.surface draw];
|
|
166
|
+
if(!hold) [self play:self.goal];
|
|
167
|
+
}
|
|
168
|
+
- (void)finish:(double)endpoint {
|
|
169
|
+
if(self.disposed || self.params.mode==1) return;
|
|
170
|
+
self.progress=endpoint; self.playing=NO;
|
|
171
|
+
self.surface.paused=YES;
|
|
172
|
+
if(self.listener) self.listener(YES,endpoint);
|
|
173
|
+
}
|
|
174
|
+
- (void)stop {
|
|
175
|
+
[self detach:YES];
|
|
176
|
+
}
|
|
177
|
+
- (void)stopPreservingImage {
|
|
178
|
+
[self detach:NO];
|
|
179
|
+
}
|
|
180
|
+
- (void)detach:(BOOL)restoreImage {
|
|
181
|
+
if(self.disposed) return;
|
|
182
|
+
self.disposed=YES; self.surface.paused=YES; self.surface.delegate=nil;
|
|
183
|
+
[self.surface removeFromSuperview]; self.surface=nil;
|
|
184
|
+
if(self.pan) { [self.image removeGestureRecognizer:self.pan];self.image.userInteractionEnabled=self.originalInteraction;self.pan=nil; }
|
|
185
|
+
if(restoreImage && !self.image.image) self.image.image=self.original;
|
|
186
|
+
self.sourceTexture=nil;self.destinationTexture=nil;self.original=nil;self.queue=nil;
|
|
187
|
+
self.textureBytes=0; self.listener=nil;
|
|
188
|
+
}
|
|
189
|
+
- (void)advance:(double)now {
|
|
190
|
+
if(!self.playing || self.paused || self.disposed) return;
|
|
191
|
+
double t=MIN(1,MAX(0,(now-self.anchor)/self.remaining));
|
|
192
|
+
JoltImageParams p=self.params;
|
|
193
|
+
if(self.settling) p.displacement=self.settleStart*(float)(exp(-8*t)*(1-t));
|
|
194
|
+
else {self.progress=self.startProgress+(self.goal-self.startProgress)*t;p.progress=self.progress;}
|
|
195
|
+
self.params=p;
|
|
196
|
+
if(t>=1) {
|
|
197
|
+
self.playing=NO;self.surface.paused=YES;self.surface.enableSetNeedsDisplay=YES;
|
|
198
|
+
if(!self.settling && self.listener) self.listener(YES,self.progress);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
- (void)drawInMTKView:(MTKView *)view {
|
|
202
|
+
if(self.disposed) return;
|
|
203
|
+
double now=CACurrentMediaTime();
|
|
204
|
+
if(self.previousFrame>0 && self.playing) self.longestFrame=MAX(self.longestFrame,now-self.previousFrame);
|
|
205
|
+
self.previousFrame=now;self.frames++;
|
|
206
|
+
[self advance:now];
|
|
207
|
+
if(self.disposed) return;
|
|
208
|
+
MTLRenderPassDescriptor *pass=view.currentRenderPassDescriptor;
|
|
209
|
+
id<CAMetalDrawable> drawable=view.currentDrawable;
|
|
210
|
+
if(!pass || !drawable) return;
|
|
211
|
+
id<MTLCommandBuffer> command=[self.queue commandBuffer];
|
|
212
|
+
id<MTLRenderCommandEncoder> encoder=[command renderCommandEncoderWithDescriptor:pass];
|
|
213
|
+
[encoder setRenderPipelineState:self.pipeline];
|
|
214
|
+
JoltImageParams params=self.params;
|
|
215
|
+
[encoder setFragmentBytes:¶ms length:sizeof(params) atIndex:0];
|
|
216
|
+
[encoder setFragmentTexture:self.sourceTexture atIndex:0];
|
|
217
|
+
[encoder setFragmentTexture:self.destinationTexture atIndex:1];
|
|
218
|
+
[encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
|
219
|
+
[encoder endEncoding]; [command presentDrawable:drawable]; [command commit];
|
|
220
|
+
}
|
|
221
|
+
- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size {}
|
|
222
|
+
- (void)drag:(UIPanGestureRecognizer *)gesture {
|
|
223
|
+
if(self.disposed || self.paused) return;
|
|
224
|
+
CGFloat scale=self.image.window.screen.scale;
|
|
225
|
+
JoltImageParams p=self.params;
|
|
226
|
+
if(gesture.state==UIGestureRecognizerStateBegan) {
|
|
227
|
+
self.playing=NO;self.surface.paused=YES;self.surface.enableSetNeedsDisplay=YES;
|
|
228
|
+
CGPoint position=[gesture locationInView:self.image],translation=[gesture translationInView:self.image];
|
|
229
|
+
p.origin=(vector_float2){(float)((position.x-translation.x)*scale),(float)((position.y-translation.y)*scale)};
|
|
230
|
+
}
|
|
231
|
+
if(gesture.state==UIGestureRecognizerStateBegan || gesture.state==UIGestureRecognizerStateChanged) {
|
|
232
|
+
CGPoint delta=[gesture translationInView:self.image];
|
|
233
|
+
double length=hypot(delta.x,delta.y)*scale;
|
|
234
|
+
double factor=length==0?0:p.amplitude*tanh(length/MAX(p.amplitude,1))/length;
|
|
235
|
+
p.displacement=(vector_float2){(float)(delta.x*scale*factor),(float)(delta.y*scale*factor)};
|
|
236
|
+
self.params=p;[self.surface draw];
|
|
237
|
+
} else if(gesture.state==UIGestureRecognizerStateEnded || gesture.state==UIGestureRecognizerStateCancelled || gesture.state==UIGestureRecognizerStateFailed) {
|
|
238
|
+
self.settleStart=p.displacement;self.anchor=CACurrentMediaTime();self.remaining=MIN(.6,self.duration);
|
|
239
|
+
self.playing=YES;self.settling=YES;self.surface.enableSetNeedsDisplay=NO;self.surface.paused=NO;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
@end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
3
|
+
|
|
4
|
+
/** Native sampled playback for opt-in interactive views. No UIKit method replacement. */
|
|
5
|
+
@interface JoltInteractiveAnimation : NSObject
|
|
6
|
+
+ (instancetype)startView:(UIView *)view layers:(NSArray<CALayer *> *)layers keyPath:(NSString *)keyPath
|
|
7
|
+
values:(NSArray<NSNumber *> *)values times:(NSArray<NSNumber *> *)times
|
|
8
|
+
duration:(double)duration delay:(double)delay repeat:(double)repeat
|
|
9
|
+
reverse:(BOOL)reverse finalValue:(double)finalValue
|
|
10
|
+
startedAt:(double)startedAt completion:(void (^)(BOOL))completion;
|
|
11
|
+
+ (void)syncView:(UIView *)view excluding:(nullable NSString *)keyPath;
|
|
12
|
+
+ (nullable NSNumber *)valueForView:(UIView *)view keyPath:(NSString *)keyPath;
|
|
13
|
+
+ (NSUInteger)activeCount;
|
|
14
|
+
@property(nonatomic, readonly) double elapsed;
|
|
15
|
+
- (void)control:(double)elapsed paused:(BOOL)paused;
|
|
16
|
+
- (void)cancel;
|
|
17
|
+
@end
|
|
18
|
+
NS_ASSUME_NONNULL_END
|