/* * (c) 2020 by Panayotis Katsaloulis * * SPDX-License-Identifier: LGPL-3.0-only */ package crossmobile.ios.uikit; import crossmobile.ios.coregraphics.CGAffineTransform; import crossmobile.ios.coregraphics.CGRect; import crossmobile.ios.foundation.NSTimer; import crossmobile.ios.uikit.UIView.DelegateViews; import org.crossmobile.bind.graphics.GraphicsBridgeConstants; import org.crossmobile.bind.graphics.curve.InterpolationCurve; import org.crossmobile.bind.system.Ticker; import org.crossmobile.bind.system.TickerConsumer; import org.crossmobile.bridge.Native; import org.robovm.objc.block.VoidBlock1; import java.util.*; import static crossmobile.ios.coregraphics.GraphicsDrill.color; import static crossmobile.ios.coregraphics.GraphicsDrill.selfRotateScaleTranslate; import static crossmobile.ios.uikit.UIViewAnimationCurve.*; import static crossmobile.ios.uikit.UIViewAnimationTransition.*; class cmAnimation implements TickerConsumer { private static final boolean ignoreSmartTransformation = true; private final Set<AnimationTuple> tuples = Collections.synchronizedSet(new HashSet<>()); private VoidBlock1<Boolean> delegate; private double delay = 0; private double repeats = 0; private boolean ping_pong = false; private InterpolationCurve animationCurve = InterpolationCurve.Linear; private double duration = GraphicsBridgeConstants.DefaultAnimationDuration; private UIView parent; private Collection<DelegateViews> viewEnter; private Collection<DelegateViews> viewLeave; private Collection<UIView> viewFrames; // Might not needed -- might be supported by setFrame itself private AnimationTransition animationTransition = AnimationTransition.None; void setAlpha(UIView view, double to) { if (view.alpha != to) tuples.add(new AlphaTuple(view, to)); } void setBackground(UIView view, int to) { UIColor bgcolor = view.backgroundColor(); if (bgcolor != null && color(bgcolor.cgcolor) != to) tuples.add(new BackgroundTuple(view, to)); } void setFrame(UIView view, CGRect to) { // Set frame ONLY when setAnimationTransition is not set! if (parent == null) setFrameImpl(view, to); } // Recycle original frame private void setFrameImpl(UIView view, CGRect to) { if (to != null && !view.frame().equals(to)) { if (viewFrames == null) viewFrames = new LinkedHashSet<>(); viewFrames.add(view); tuples.add(new FrameTuple(view, to)); } } // Recycle original transformation void setTransformation(UIView view, CGAffineTransform to) { CGAffineTransform from = view.transform; if (from == null && (to == null || to.isIdentity())) return; if (from == null) from = CGAffineTransform.identity(); if (to == null) to = CGAffineTransform.identity(); if (from.equals(to)) return; double from_th1 = Math.atan(from.getC() / from.getD()); double from_th2 = Math.atan(-from.getB() / from.getA()); double to_th1 = Math.atan(to.getC() / to.getD()); double to_th2 = Math.atan(-to.getB() / to.getA()); if (ignoreSmartTransformation || Math.abs(from_th1 - from_th2) > 0.00001f || Math.abs(to_th1 - to_th2) > 0.00001f) tuples.add(new SimpleTransformationTuple(view, from.getA(), from.getB(), from.getC(), from.getD(), from.getTx(), from.getTy(), to.getA() - from.getA(), to.getB() - from.getB(), to.getC() - from.getC(), to.getD() - from.getD(), to.getTx() - from.getTx(), to.getTy() - from.getTy())); else { System.out.println("smart"); double fromSx = (from.getA() > 0 ? 1 : -1) * Math.sqrt(from.getA() * from.getA() + from.getC() * from.getC()); double fromSy = (from.getD() > 0 ? 1 : -1) * Math.sqrt(from.getB() * from.getB() + from.getD() * from.getD()); double toSx = (to.getA() > 0 ? 1 : -1) * Math.sqrt(to.getA() * to.getA() + to.getC() * to.getC()); double toSy = (to.getD() > 0 ? 1 : -1) * Math.sqrt(to.getB() * to.getB() + to.getD() * to.getD()); tuples.add(new SmartTransformationTuple(view, from_th1, fromSx, fromSy, from.getTx(), from.getTy(), to_th1 - from_th1, toSx - fromSx, toSy - fromSy, to.getTx() - from.getTx(), to.getTy() - from.getTy())); } } boolean viewToEnter(DelegateViews dv) { if (areParentsCompatible(dv)) { if (viewEnter == null) viewEnter = new HashSet<>(); viewEnter.add(dv); return true; } return false; } boolean viewToLeave(DelegateViews dv) { if (areParentsCompatible(dv)) { if (viewLeave == null) viewLeave = new HashSet<>(); viewLeave.add(dv); return true; } return false; } private boolean areParentsCompatible(DelegateViews dv) { UIView commonParent = dv.anyParent(); if (commonParent == null) return false; if (this.parent == null) { this.parent = commonParent; return true; } else return this.parent == commonParent; } void setDuration(double d) { this.duration = d; } void setDelay(double delay) { if (delay < 0) delay = 0; this.delay = delay; } void setDelegate(VoidBlock1<Boolean> delegate) { this.delegate = delegate; } void setCurve(int animationCurve) { switch (animationCurve) { case EaseIn: this.animationCurve = InterpolationCurve.EaseIn; break; case EaseOut: this.animationCurve = InterpolationCurve.EaseOut; break; case EaseInOut: this.animationCurve = InterpolationCurve.EaseInOut; break; case Linear: this.animationCurve = InterpolationCurve.Linear; break; default: } } public void setParent(UIView parent) { if (this.parent != null) throw new RuntimeException("Only one UIView allowed under setAnimationTransition"); this.parent = parent; if (this.parent == null) throw new NullPointerException("UIView under setAnimationTransition could not be null"); } public void setTransition(int animationTransition) { switch (animationTransition) { case FlipFromLeft: this.animationTransition = AnimationTransition.Left; break; case FlipFromRight: this.animationTransition = AnimationTransition.Right; break; case CurlUp: case FlipFromTop: this.animationTransition = AnimationTransition.Up; break; case CurlDown: case FlipFromBottom: this.animationTransition = AnimationTransition.Down; break; case CrossDissolve: case None: default: this.animationTransition = AnimationTransition.None; } } void setRepeats(double repeatCount) { if (repeatCount < 0) repeatCount = 0; this.repeats = repeatCount; } void setAutoReverse(boolean repeatAutoreverses) { this.ping_pong = repeatAutoreverses; } void commit() { if (delay > 0) NSTimer.scheduledTimerWithTimeInterval(delay, timer -> Ticker.add(cmAnimation.this, animationCurve, duration, repeats, ping_pong), null, false); else Ticker.add(this, animationCurve, duration, repeats, ping_pong); } @Override public void start() { // Add pending entering/leaving view animations if (parent != null) { if (viewEnter != null) for (DelegateViews dv : viewEnter) { dv.delegateBefore(); setFrameImpl(dv.view(), animationTransition.enterView(dv.view(), parent)); dv.doAdd(); } if (viewLeave != null) for (DelegateViews dv : viewLeave) { dv.delegateBefore(); setFrameImpl(dv.view(), animationTransition.exitView(dv.view(), parent)); } } } @Override public void apply(double progress) { try { for (AnimationTuple tuple : tuples) tuple.applyAt(progress); } catch (ConcurrentModificationException ex) { Native.system().error("Concurrent Animation modification", null); Native.graphics().refreshDisplay(); } } @Override public void end() { Native.system().postOnEventThread(() -> { if (parent != null) { if (viewLeave != null) for (DelegateViews dv : viewLeave) { dv.doRemove(); dv.delegateAfter(); } if (viewEnter != null) for (DelegateViews dv : viewEnter) dv.delegateAfter(); } if (viewFrames != null) for (UIView view : viewFrames) view.updateConstraints(); if (delegate != null) delegate.invoke(true); }); } private interface AnimationTransition { AnimationTransition None = new NoneTransition(); AnimationTransition Left = new FlipFromLeftTransition(); AnimationTransition Right = new FlipFromRightTransition(); AnimationTransition Up = new CurlUpTransition(); AnimationTransition Down = new CurlDownTransition(); CGRect enterView(UIView view, UIView parent); CGRect exitView(UIView view, UIView parent); } private abstract static class AnimationTuple { abstract void applyAt(double interpolatedTime); } private static class AlphaTuple extends AnimationTuple { private final UIView view; private final double from; private final double delta; private AlphaTuple(UIView view, double to) { this.view = view; this.from = view.alpha; this.delta = to - this.from; } @Override void applyAt(double interpolatedTime) { view.alpha = from + delta * interpolatedTime; } } private static class BackgroundTuple extends AnimationTuple { private final UIView view; private final int aF; private final int rF; private final int gF; private final int bF; private final int aD; private final int rD; private final int gD; private final int bD; private BackgroundTuple(UIView view, int to) { int from = color(view.backgroundColor().cgcolor); this.view = view; this.aF = (from >>> 24) & 0xFF; this.rF = (from >>> 16) & 0xFF; this.gF = (from >>> 8) & 0xFF; this.bF = from & 0xFF; this.aD = (to >>> 24) & 0xFF - this.aF; this.rD = (to >>> 16) & 0xFF - this.rF; this.gD = (to >>> 8) & 0xFF - this.gF; this.bD = to & 0xFF - this.bF; } @Override void applyAt(double interpolatedTime) { int aC = (int) (aF + aD * interpolatedTime); int rC = (int) (rF + rD * interpolatedTime); int gC = (int) (gF + gD * interpolatedTime); int bC = (int) (bF + bD * interpolatedTime); view.setBackgroundColorImpl(new UIColor((aC << 24) & (rC << 16) & (gC << 8) & bC)); } } private static class FrameTuple extends AnimationTuple { private final UIView view; private final double fromX; private final double fromY; private final double fromHeight; private final double fromWidth; private final double deltaX; private final double deltaY; private final double deltaHeight; private final double deltaWidth; private FrameTuple(UIView view, CGRect to) { this.view = view; fromX = view.getX(); fromY = view.getY(); fromWidth = view.getWidth(); fromHeight = view.getHeight(); deltaX = to.getOrigin().getX() - view.getX(); deltaY = to.getOrigin().getY() - view.getY(); deltaWidth = to.getSize().getWidth() - view.getWidth(); deltaHeight = to.getSize().getHeight() - view.getHeight(); } @Override void applyAt(double interpolatedTime) { view.setFrameImpl( fromX + deltaX * interpolatedTime, fromY + deltaY * interpolatedTime, fromWidth + deltaWidth * interpolatedTime, fromHeight + deltaHeight * interpolatedTime); view.setNeedsLayout(); } } private static abstract class TransformationTuple extends AnimationTuple { private final UIView view; private CGAffineTransform transfV; CGAffineTransform transf; TransformationTuple(UIView view) { this.view = view; transfV = CGAffineTransform.identity(); transf = CGAffineTransform.identity(); } @Override final void applyAt(double interpolatedTime) { CGAffineTransform swap = transfV; applyAt(transf, interpolatedTime); view.setTransformImpl(transfV = transf); transf = swap; } abstract void applyAt(CGAffineTransform transf, double interpolatedTime); } private static class SimpleTransformationTuple extends TransformationTuple { private final double fromA; private final double fromB; private final double fromC; private final double fromD; private final double fromTx; private final double fromTy; private final double deltaA; private final double deltaB; private final double deltaC; private final double deltaD; private final double deltaTx; private final double deltaTy; SimpleTransformationTuple(UIView view, double fromA, double fromB, double fromC, double fromD, double fromTx, double fromTy, double deltaA, double deltaB, double deltaC, double deltaD, double deltaTx, double deltaTy) { super(view); this.fromA = fromA; this.fromB = fromB; this.fromC = fromC; this.fromD = fromD; this.fromTx = fromTx; this.fromTy = fromTy; this.deltaA = deltaA; this.deltaB = deltaB; this.deltaC = deltaC; this.deltaD = deltaD; this.deltaTx = deltaTx; this.deltaTy = deltaTy; } @Override void applyAt(CGAffineTransform transf, double interpolatedTime) { transf.setA(fromA + deltaA * interpolatedTime); transf.setB(fromB + deltaB * interpolatedTime); transf.setC(fromC + deltaC * interpolatedTime); transf.setD(fromD + deltaD * interpolatedTime); transf.setTx(fromTx + deltaTx * interpolatedTime); transf.setTy(fromTy + deltaTy * interpolatedTime); } } private static class SmartTransformationTuple extends TransformationTuple { private final double fromTheta; private final double fromSx; private final double fromSy; private final double fromTx; private final double fromTy; private final double deltaTheta; private final double deltaSx; private final double deltaSy; private final double deltaTx; private final double deltaTy; SmartTransformationTuple(UIView view, double fromTheta, double fromSx, double fromSy, double fromTx, double fromTy, double deltaTheta, double deltaSx, double deltaSy, double deltaTx, double deltaTy) { super(view); this.fromTheta = fromTheta; this.fromSx = fromSx; this.fromSy = fromSy; this.fromTx = fromTx; this.fromTy = fromTy; this.deltaTheta = deltaTheta; this.deltaSx = deltaSx; this.deltaSy = deltaSy; this.deltaTx = deltaTx; this.deltaTy = deltaTy; } @Override void applyAt(CGAffineTransform transf, double interpolatedTime) { selfRotateScaleTranslate(transf, fromTheta + deltaTheta * interpolatedTime, fromSx + deltaSx * interpolatedTime, fromSy + deltaSy * interpolatedTime, fromTx + deltaTx * interpolatedTime, fromTy + deltaTy * interpolatedTime); } } private static class NoneTransition implements AnimationTransition { @Override public CGRect enterView(UIView view, UIView parent) { return null; } @Override public CGRect exitView(UIView view, UIView parent) { return null; } } private static class FlipFromLeftTransition implements AnimationTransition { @Override public CGRect enterView(UIView view, UIView parent) { CGRect last = view.frame(); view.setFrameImpl(view.getX() - parent.getWidth(), view.getY(), view.getWidth(), view.getHeight()); return last; } @Override public CGRect exitView(UIView view, UIView parent) { CGRect last = view.frame(); last.getOrigin().setX(last.getOrigin().getX() + parent.getWidth() / 2); return last; } } private static class FlipFromRightTransition implements AnimationTransition { @Override public CGRect enterView(UIView view, UIView parent) { CGRect last = view.frame(); view.setFrameImpl(view.getX() + parent.getWidth(), view.getY(), view.getWidth(), view.getHeight()); return last; } @Override public CGRect exitView(UIView view, UIView parent) { CGRect last = view.frame(); last.getOrigin().setX(last.getOrigin().getX() - (parent.getWidth() / 2)); return last; } } private static class CurlUpTransition implements AnimationTransition { @Override public CGRect enterView(UIView view, UIView parent) { CGRect last = view.frame(); view.setFrameImpl(view.getX() + parent.getWidth(), view.getY(), view.getWidth(), view.getHeight()); return last; } @Override public CGRect exitView(UIView view, UIView parent) { CGRect last = view.frame(); last.getOrigin().setX(last.getOrigin().getX() - parent.getWidth()); return last; } } private static class CurlDownTransition implements AnimationTransition { @Override public CGRect enterView(UIView view, UIView parent) { CGRect last = view.frame(); view.setFrameImpl(view.getX() - parent.getWidth(), view.getY(), view.getWidth(), view.getHeight()); return last; } @Override public CGRect exitView(UIView view, UIView parent) { CGRect lastpos = view.frame(); lastpos.getOrigin().setX(lastpos.getOrigin().getX() + parent.getWidth()); return lastpos; } } }