From fc7ee363678442ec6ee19d02b406dc110881bed7 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 15:42:47 +0200 Subject: [PATCH 01/30] Implementierung von Easing-Funktionen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Implementierungen wurden von https://easings.net übernommen. --- src/schule/ngb/zm/anim/Easing.java | 252 +++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 src/schule/ngb/zm/anim/Easing.java diff --git a/src/schule/ngb/zm/anim/Easing.java b/src/schule/ngb/zm/anim/Easing.java new file mode 100644 index 0000000..8dea06b --- /dev/null +++ b/src/schule/ngb/zm/anim/Easing.java @@ -0,0 +1,252 @@ +package schule.ngb.zm.anim; + +import java.util.function.DoubleUnaryOperator; + +/** + * @see Cheat Sheet für Easing-Funktionen + */ +public class Easing { + + public static final DoubleUnaryOperator DEFAULT_EASING = Easing::cubicInOut; + + public static final DoubleUnaryOperator thereAndBack() { + return Easing::thereAndBack; + } + + public static final DoubleUnaryOperator thereAndBack( final DoubleUnaryOperator baseEasing ) { + return ( t ) -> thereAndBack(t, baseEasing); + } + + public static final double thereAndBack( double t ) { + return thereAndBack(t, DEFAULT_EASING); + } + + public static final double thereAndBack( double t, DoubleUnaryOperator baseEasing ) { + if( t < 0.5 ) { + return baseEasing.applyAsDouble(2 * t); + } else { + return baseEasing.applyAsDouble(2 - 2 * t); + } + } + + public static final DoubleUnaryOperator halfAndHalf( final DoubleUnaryOperator firstEasing, final DoubleUnaryOperator secondEasing ) { + return ( t ) -> halfAndHalf(t, firstEasing, secondEasing); + } + + public static final DoubleUnaryOperator halfAndHalf( final DoubleUnaryOperator firstEasing, final DoubleUnaryOperator secondEasing, final double split ) { + return ( t ) -> halfAndHalf(t, firstEasing, secondEasing, split); + } + + public static final double halfAndHalf( double t, DoubleUnaryOperator firstEasing, DoubleUnaryOperator secondEasing ) { + return halfAndHalf(t, firstEasing, secondEasing, 0.5); + } + + public static final double halfAndHalf( double t, DoubleUnaryOperator firstEasing, DoubleUnaryOperator secondEasing, double split ) { + if( t < split ) { + return firstEasing.applyAsDouble(2 * t); + } else { + return secondEasing.applyAsDouble(1 - 2 * t); + } + } + + + public static final DoubleUnaryOperator linear() { + return Easing::linear; + } + + public static final double linear( double t ) { + return t; + } + + public static final DoubleUnaryOperator quadIn() { + return Easing::quadIn; + } + + public static final double quadIn( double t ) { + return t * t; + } + + public static final DoubleUnaryOperator quadOut() { + return Easing::quadOut; + } + + public static final double quadOut( double t ) { + return 1 - (1 - t) * (1 - t); + } + + public static final DoubleUnaryOperator quadInOut() { + return Easing::quadInOut; + } + + public static final double quadInOut( double t ) { + return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; + } + + public static final DoubleUnaryOperator cubicIn() { + return Easing::cubicIn; + } + + public static final double cubicIn( double t ) { + return t * t * t; + } + + public static final DoubleUnaryOperator cubicOut() { + return Easing::cubicOut; + } + + public static final double cubicOut( double t ) { + return 1 - Math.pow(1 - t, 3); + } + + public static final DoubleUnaryOperator cubicInOut() { + return Easing::cubicInOut; + } + + public static final double cubicInOut( double t ) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; + } + + public static final DoubleUnaryOperator sineIn() { + return Easing::sineIn; + } + + public static final double sineIn( double t ) { + return 1 - Math.cos((t * Math.PI) / 2); + } + + public static final DoubleUnaryOperator sineOut() { + return Easing::sineOut; + } + + public static final double sineOut( double t ) { + return Math.sin((t * Math.PI) / 2); + } + + public static final DoubleUnaryOperator sineInOut() { + return Easing::sineInOut; + } + + public static final double sineInOut( double t ) { + return -(Math.cos(Math.PI * t) - 1) / 2; + } + + public static final DoubleUnaryOperator elasticIn() { + return Easing::elasticIn; + } + + public static final double elasticIn( double t ) { + double c4 = (2 * Math.PI) / 3; + + return t == 0 + ? 0 + : t == 1 + ? 1 + : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4); + } + + public static final DoubleUnaryOperator elasticOut() { + return Easing::elasticOut; + } + + public static final double elasticOut( double t ) { + double c4 = (2 * Math.PI) / 3; + + return t == 0 + ? 0 + : t == 1 + ? 1 + : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1; + } + + public static final DoubleUnaryOperator elasticInOut() { + return Easing::elasticInOut; + } + + public static final double elasticInOut( double t ) { + double c5 = (2 * Math.PI) / 4.5; + + return t == 0 + ? 0 + : t == 1 + ? 1 + : t < 0.5 + ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1; + } + + public static final DoubleUnaryOperator bounceIn() { + return Easing::bounceIn; + } + + public static final double bounceIn( double t ) { + return 1 - bounceOut(1 - t); + } + + public static final DoubleUnaryOperator bounceOut() { + return Easing::bounceOut; + } + + public static final double bounceOut( double t ) { + double n1 = 7.5625; + double d1 = 2.75; + + if (t < 1.0 / d1) { + return n1 * t * t; + } else if (t < 2.0 / d1) { + return n1 * (t -= 1.5 / d1) * t + 0.75; + } else if (t < 2.5 / d1) { + return n1 * (t -= 2.25 / d1) * t + 0.9375; + } else { + return n1 * (t -= 2.625 / d1) * t + 0.984375; + } + } + + public static final DoubleUnaryOperator bounceInOut() { + return Easing::bounceInOut; + } + + public static final double bounceInOut( double t ) { + return t < 0.5 + ? (1 - bounceOut(1 - 2 * t)) / 2 + : (1 + bounceOut(2 * t - 1)) / 2; + } + + public static final DoubleUnaryOperator backIn() { + return Easing::backIn; + } + + public static final double backIn( double t ) { + double c1 = 1.70158; + double c3 = c1 + 1; + + return c3 * t * t * t - c1 * t * t; + } + + public static final DoubleUnaryOperator backOut() { + return Easing::backOut; + } + + public static final double backOut( double t ) { + double c1 = 1.70158; + double c3 = c1 + 1; + + return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); + } + + public static final DoubleUnaryOperator backInOut() { + return Easing::backInOut; + } + + public static final double backInOut( double t ) { + double c1 = 1.70158; + double c2 = c1 * 1.525; + + return t < 0.5 + ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 + : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2; + } + + private Easing() { + } + +} From 2f59d29d088ec5cc0b7e4e109097087ad9b95772 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 15:44:36 +0200 Subject: [PATCH 02/30] Animationen als eigener Prozess Die Animations API verwendet Funktionale Aspekte der Java 8 API und erlaubt die Animation beliebiger Objekte, aber ist vor allem auf die `shape.*` Klassen ausgelegt. --- src/schule/ngb/zm/anim/Animations.java | 183 +++++++++++++++++++++++++ src/schule/ngb/zm/anim/Animator.java | 11 ++ 2 files changed, 194 insertions(+) create mode 100644 src/schule/ngb/zm/anim/Animations.java create mode 100644 src/schule/ngb/zm/anim/Animator.java diff --git a/src/schule/ngb/zm/anim/Animations.java b/src/schule/ngb/zm/anim/Animations.java new file mode 100644 index 0000000..c4e7b96 --- /dev/null +++ b/src/schule/ngb/zm/anim/Animations.java @@ -0,0 +1,183 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.Color; +import schule.ngb.zm.Constants; +import schule.ngb.zm.Vector; +import schule.ngb.zm.tasks.TaskRunner; +import schule.ngb.zm.util.Log; +import schule.ngb.zm.util.Validator; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.Future; +import java.util.function.*; + +public class Animations { + + public static final Future animateProperty( String propName, T target, double to, int runtime, DoubleUnaryOperator easing ) { + double from; + try { + from = callGetter(target, propName, double.class); + } catch( InvocationTargetException | NoSuchMethodException | + IllegalAccessException ex ) { + throw new RuntimeException("Can't access property getter for animation.", ex); + } + + Method propSetter; + try { + propSetter = findSetter(target, propName, double.class); + } catch( NoSuchMethodException ex ) { + throw new RuntimeException("Can't find property setter for animation.", ex); + } + + return animateProperty(target, from, to, runtime, easing, ( d ) -> { + try { + propSetter.invoke(target, d); + } catch( IllegalAccessException | InvocationTargetException e ) { + throw new RuntimeException("Can't access property setter for animation.", e); + } + }); + } + + public static final Future animateProperty( String propName, T target, Color to, int runtime, DoubleUnaryOperator easing ) { + Color from; + try { + from = callGetter(target, propName, Color.class); + } catch( InvocationTargetException | NoSuchMethodException | + IllegalAccessException ex ) { + throw new RuntimeException("Can't access property getter for animation.", ex); + } + + Method propSetter; + try { + propSetter = findSetter(target, propName, Color.class); + } catch( NoSuchMethodException ex ) { + throw new RuntimeException("Can't find property setter for animation.", ex); + } + + return animateProperty(target, from, to, runtime, easing, ( d ) -> { + try { + propSetter.invoke(target, d); + } catch( IllegalAccessException | InvocationTargetException e ) { + throw new RuntimeException("Can't access property setter for animation.", e); + } + }); + } + + public static final Future animateProperty( String propName, T target, Vector to, int runtime, DoubleUnaryOperator easing ) { + Vector from; + try { + from = callGetter(target, propName, Vector.class); + } catch( InvocationTargetException | NoSuchMethodException | + IllegalAccessException ex ) { + throw new RuntimeException("Can't access property getter for animation.", ex); + } + + Method propSetter; + try { + propSetter = findSetter(target, propName, Vector.class); + } catch( NoSuchMethodException ex ) { + throw new RuntimeException("Can't find property setter for animation.", ex); + } + + return animateProperty(target, from, to, runtime, easing, ( d ) -> { + try { + propSetter.invoke(target, d); + } catch( IllegalAccessException | InvocationTargetException e ) { + throw new RuntimeException("Can't access property setter for animation.", e); + } + }); + } + + private static final R callGetter( T target, String propName, Class propType ) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + String getterName = makeMethodName("get", propName); + Method getter = target.getClass().getMethod(getterName); + if( getter != null && getter.getReturnType().equals(propType) ) { + return (R) getter.invoke(target); + } else { + throw new NoSuchMethodException(String.format("No getter for property <%s> found.", propName)); + } + } + + private static final Method findSetter( T target, String propName, Class propType ) throws NoSuchMethodException { + String setterName = makeMethodName("set", propName); + Method setter = target.getClass().getMethod(setterName, propType); + if( setter != null && setter.getReturnType().equals(void.class) && setter.getParameterCount() == 1 ) { + return setter; + } else { + throw new NoSuchMethodException(String.format("No setter for property <%s> found.", propName)); + } + } + + private static final String makeMethodName( String prefix, String propName ) { + String firstChar = propName.substring(0, 1).toUpperCase(); + String tail = ""; + if( propName.length() > 1 ) { + tail = propName.substring(1); + } + return prefix + firstChar + tail; + } + + public static final Future animateProperty( T target, final double from, final double to, int runtime, DoubleUnaryOperator easing, DoubleConsumer propSetter ) { + Validator.requireNotNull(target); + Validator.requireNotNull(propSetter); + return animate(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e))); + } + + public static final Future animateProperty( T target, final Color from, final Color to, int runtime, DoubleUnaryOperator easing, Consumer propSetter ) { + return animate(target, runtime, easing, ( e ) -> propSetter.accept(Color.interpolate(from, to, e))); + } + + + public static final Future animateProperty( T target, final Vector from, final Vector to, int runtime, DoubleUnaryOperator easing, Consumer propSetter ) { + return animate(target, runtime, easing, ( e ) -> propSetter.accept(Vector.interpolate(from, to, e))); + } + + public static final Future animateProperty( T target, R from, R to, int runtime, DoubleUnaryOperator easing, DoubleFunction interpolator, Consumer propSetter ) { + return animate(target, runtime, easing, interpolator, ( t, r ) -> propSetter.accept(r)); + } + + + public static final Future animate( T target, int runtime, DoubleUnaryOperator easing, DoubleFunction interpolator, BiConsumer applicator ) { + return animate(target, runtime, easing, ( e ) -> applicator.accept(target, interpolator.apply(e))); + } + + public static final Future animate( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) { + final long starttime = System.currentTimeMillis(); + return TaskRunner.run(() -> { + double t = 0.0; + do { + // One animation step for t in [0,1] + stepper.accept(easing.applyAsDouble(t)); + try { + Thread.sleep(1000 / Constants.framesPerSecond); + } catch( InterruptedException ex ) { + } + t = (double) (System.currentTimeMillis() - starttime) / (double) runtime; + } while( t < 1.0 ); + stepper.accept(easing.applyAsDouble(1.0)); + }, target); + } + + public static final Future animate( T target, int runtime, Animator animator ) { + return animate( + target, runtime, + animator::easing, + animator::interpolator, + animator::applicator + ); + } + + public static Future animate( Animation animation ) { + animation.start(); + return null; + } + + public static Future animate( Animation animation, DoubleUnaryOperator easing ) { + animation.start(easing); + return null; + } + + public static final Log LOG = Log.getLogger(Animations.class); + +} diff --git a/src/schule/ngb/zm/anim/Animator.java b/src/schule/ngb/zm/anim/Animator.java new file mode 100644 index 0000000..951279d --- /dev/null +++ b/src/schule/ngb/zm/anim/Animator.java @@ -0,0 +1,11 @@ +package schule.ngb.zm.anim; + +public interface Animator { + + double easing(double t); + + R interpolator(double e); + + void applicator(T target, R value); + +} From 303b667cbffe1cc9d29babc1ee78f491b6c989a4 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 15:45:25 +0200 Subject: [PATCH 03/30] =?UTF-8?q?Tests=20f=C3=BCr=20Animationen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schule/ngb/zm/anim/AnimationsTest.java | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 test/src/schule/ngb/zm/anim/AnimationsTest.java diff --git a/test/src/schule/ngb/zm/anim/AnimationsTest.java b/test/src/schule/ngb/zm/anim/AnimationsTest.java new file mode 100644 index 0000000..7aa9eb3 --- /dev/null +++ b/test/src/schule/ngb/zm/anim/AnimationsTest.java @@ -0,0 +1,238 @@ +package schule.ngb.zm.anim; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import schule.ngb.zm.Color; +import schule.ngb.zm.Constants; +import schule.ngb.zm.Zeichenmaschine; +import schule.ngb.zm.shapes.*; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.DoubleUnaryOperator; + +import static org.junit.jupiter.api.Assertions.*; + +class AnimationsTest { + + private static Zeichenmaschine zm; + + private static ShapesLayer shapes; + + @BeforeAll + static void beforeAll() { + zm = new Zeichenmaschine(400, 400, "zm-test: Animations", false); + shapes = zm.getShapesLayer(); + assertNotNull(shapes); + } + + @AfterAll + static void afterAll() { + zm.exit(); + } + + @BeforeEach + void setUp() { + shapes.removeAll(); + } + + @Test + void animateMove() { + Shape s = new Circle(0, 0, 10); + shapes.add(s); + + _animateMove(s, 2500, Easing.DEFAULT_EASING); + assertEquals(zm.getWidth(), s.getX(), 0.0001); + assertEquals(zm.getHeight(), s.getY(), 0.0001); + + _animateMove(s, 2500, Easing.thereAndBack(Easing.linear())); + assertEquals(0.0, s.getX(), 0.0001); + assertEquals(0.0, s.getY(), 0.0001); + + _animateMove(s, 4000, Easing::bounceInOut); + assertEquals(zm.getWidth(), s.getX(), 0.0001); + assertEquals(zm.getHeight(), s.getY(), 0.0001); + } + + private void _animateMove( Shape s, int runtime, DoubleUnaryOperator easing ) { + s.moveTo(0, 0); + Future future = Animations.animate( + s, runtime, + easing, + ( e ) -> Constants.interpolate(0, 400, e), + ( t, p ) -> { + t.moveTo(p, p); + } + ); + assertNotNull(future); + try { + assertEquals(s, future.get()); + } catch( Exception e ) { + fail(e); + } + } + + @Test + void animateCircle() { + Shape s = new Circle(0, 0, 10); + shapes.add(s); + + _animateCircle(s, 5000, Easing.linear()); + } + + private void _animateCircle( Shape s, final int runtime, final DoubleUnaryOperator easing ) { + final int midX = (int) (zm.getWidth() * .5); + final int midY = (int) (zm.getHeight() * .5); + final int radius = (int) (zm.getWidth() * .25); + + Animator ani = new Animator() { + @Override + public double easing( double t ) { + return easing.applyAsDouble(t); + } + + @Override + public Double interpolator( double e ) { + return Constants.interpolate(0, 360, e); + } + + @Override + public void applicator( Shape s, Double angle ) { + double rad = Math.toRadians(angle); + s.moveTo(midX + radius * Math.cos(rad), midY + radius * Math.sin(rad)); + } + }; + + Future future = Animations.animate(s, runtime, ani); + assertNotNull(future); + try { + assertEquals(s, future.get()); + } catch( Exception e ) { + fail(e); + } + } + + @Test + void animateRotate() { + Shape s = new Rectangle(0, 0, 129, 80); + s.setAnchor(Constants.CENTER); + shapes.add(s); + + _animateRotate(s, 3000, Easing::cubicIn); + assertEquals(zm.getWidth() * 0.5, s.getX(), 0.0001); + assertEquals(zm.getHeight() * 0.5, s.getY(), 0.0001); + assertEquals(0.0, s.getRotation(), 0.0001); + + _animateRotate(s, 500, Easing::elasticInOut); + assertEquals(zm.getWidth() * 0.5, s.getX(), 0.0001); + assertEquals(zm.getHeight() * 0.5, s.getY(), 0.0001); + assertEquals(0.0, s.getRotation(), 0.0001); + + _animateRotate(s, 1000, Easing::bounceOut); + assertEquals(zm.getWidth() * 0.5, s.getX(), 0.0001); + assertEquals(zm.getHeight() * 0.5, s.getY(), 0.0001); + assertEquals(0.0, s.getRotation(), 0.0001); + + _animateRotate(s, 6000, Easing::backInOut); + assertEquals(zm.getWidth() * 0.5, s.getX(), 0.0001); + assertEquals(zm.getHeight() * 0.5, s.getY(), 0.0001); + assertEquals(0.0, s.getRotation(), 0.0001); + } + + private void _animateRotate( Shape s, int runtime, DoubleUnaryOperator easing ) { + s.moveTo(zm.getWidth() * .5, zm.getHeight() * .5); + s.rotateTo(0); + Future future = Animations.animate( + s, runtime, + easing, + ( e ) -> s.rotateTo(Constants.interpolate(0, 720, e)) + ); + assertNotNull(future); + try { + assertEquals(s, future.get()); + } catch( Exception e ) { + fail(e); + } + } + + @Test + void animateColor() { + Shape s = new Ellipse(0, 0, 129, 80); + s.setAnchor(Constants.CENTER); + shapes.add(s); + + _animateColor(s, Color.RED, 1000, Easing.DEFAULT_EASING); + assertEquals(Color.RED, s.getFillColor()); + _animateColor(s, Color.BLUE, 1500, Easing::backInOut); + assertEquals(Color.BLUE, s.getFillColor()); + _animateColor(s, Color.GREEN, 2000, Easing::bounceOut); + assertEquals(Color.GREEN, s.getFillColor()); + _animateColor(s, Color.YELLOW, 300, Easing::thereAndBack); + assertEquals(Color.GREEN, s.getFillColor()); + } + + private void _animateColor( Shape s, Color to, int runtime, DoubleUnaryOperator easing ) { + s.moveTo(zm.getWidth() * .5, zm.getHeight() * .5); + final Color from = s.getFillColor(); + Future future = Animations.animate( + s, runtime, + easing, + ( e ) -> Color.interpolate(from, to, e), + ( t, c ) -> t.setFillColor(c) + ); + assertNotNull(future); + try { + assertEquals(s, future.get()); + } catch( Exception e ) { + fail(e); + } + } + + + @Test + void animatePropertyColor() { + Shape s = new Ellipse(0, 0, 129, 80); + s.setAnchor(Constants.CENTER); + shapes.add(s); + + _animatePropertyColor(s, Color.RED, 1000, Easing.DEFAULT_EASING); + assertEquals(Color.RED, s.getFillColor()); + _animatePropertyColor(s, Color.BLUE, 1500, Easing::backInOut); + assertEquals(Color.BLUE, s.getFillColor()); + _animatePropertyColor(s, Color.GREEN, 2000, Easing::bounceOut); + assertEquals(Color.GREEN, s.getFillColor()); + _animatePropertyColor(s, Color.YELLOW, 300, Easing::thereAndBack); + assertEquals(Color.GREEN, s.getFillColor()); + } + + private void _animatePropertyColor( Shape s, Color to, int runtime, DoubleUnaryOperator easing ) { + s.moveTo(zm.getWidth() * .5, zm.getHeight() * .5); + final Color from = s.getFillColor(); + Future future = Animations.animateProperty( + s, from, to, runtime, easing, s::setFillColor + ); + assertNotNull(future); + try { + assertEquals(s, future.get()); + } catch( Exception e ) { + fail(e); + } + } + + @Test + void animatePropertyReflect() { + Shape s = new Ellipse(0, 200, 129, 80); + shapes.add(s); + + try { + Animations.animateProperty("x", s, 400, 1000, Easing.DEFAULT_EASING); + Animations.animateProperty("strokeColor", s, Color.RED, 1000, Easing.DEFAULT_EASING).get(); + } catch( InterruptedException | ExecutionException e ) { + fail(e); + } + } + +} From e4818d4f3e2f95aa1b14bf9379efde2eaa8f9185 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 21:45:08 +0200 Subject: [PATCH 04/30] =?UTF-8?q?Abstraktion=20f=C3=BCr=20Listener=20API?= =?UTF-8?q?=20erstellt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EventGenerator soll als einheitlicher Unterbau zur Umstzung von Listener Patterns dienen. Damit sollen Listener wie AudioListener oder AnimationListener umgesetzt werden. --- src/schule/ngb/zm/events/EventGenerator.java | 50 ++++++++++++++ src/schule/ngb/zm/events/Listener.java | 7 ++ .../ngb/zm/events/EventGeneratorTest.java | 65 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 src/schule/ngb/zm/events/EventGenerator.java create mode 100644 src/schule/ngb/zm/events/Listener.java create mode 100644 test/src/schule/ngb/zm/events/EventGeneratorTest.java diff --git a/src/schule/ngb/zm/events/EventGenerator.java b/src/schule/ngb/zm/events/EventGenerator.java new file mode 100644 index 0000000..f8834a0 --- /dev/null +++ b/src/schule/ngb/zm/events/EventGenerator.java @@ -0,0 +1,50 @@ +package schule.ngb.zm.events; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.BiConsumer; + +public class EventGenerator> { + + private CopyOnWriteArraySet listeners; + + private ConcurrentMap> eventRegistry; + + public EventGenerator() { + listeners = new CopyOnWriteArraySet<>(); + eventRegistry = new ConcurrentHashMap<>(); + } + + public void registerEventType( String eventKey, BiConsumer dispatcher ) { + if( !eventRegistered(eventKey) ) { + eventRegistry.put(eventKey, dispatcher); + } + } + + public void addListener( L listener ) { + listeners.add(listener); + } + + public void removeListener( L listener ) { + listeners.remove(listener); + } + + public boolean hasListeners() { + return !listeners.isEmpty(); + } + + public boolean eventRegistered( String eventKey ) { + return eventRegistry.containsKey(eventKey); + } + + public void dispatchEvent( String eventKey, final E event ) { + if( eventRegistered(eventKey) ) { + final BiConsumer dispatcher = eventRegistry.get(eventKey); + listeners.stream().forEach(( listener ) -> { + dispatcher.accept(event, listener); + }); + } + } + +} diff --git a/src/schule/ngb/zm/events/Listener.java b/src/schule/ngb/zm/events/Listener.java new file mode 100644 index 0000000..54c6025 --- /dev/null +++ b/src/schule/ngb/zm/events/Listener.java @@ -0,0 +1,7 @@ +package schule.ngb.zm.events; + +public interface Listener { + + + +} diff --git a/test/src/schule/ngb/zm/events/EventGeneratorTest.java b/test/src/schule/ngb/zm/events/EventGeneratorTest.java new file mode 100644 index 0000000..f10979b --- /dev/null +++ b/test/src/schule/ngb/zm/events/EventGeneratorTest.java @@ -0,0 +1,65 @@ +package schule.ngb.zm.events; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EventGeneratorTest { + + class TestEvent { + + private String data; + + private String type; + + public TestEvent( String data, boolean isStart ) { + this.data = data; + this.type = isStart ? "start" : "stop"; + } + + public String getData() { + return data; + } + + public String getType() { + return type; + } + + } + + interface TestListener extends Listener { + + void startEvent( TestEvent t ); + + void stopEvent( TestEvent t ); + + } + + @Test + void eventRegistry() { + EventGenerator gen = new EventGenerator<>(); + + gen.registerEventType("start", ( event, listener ) -> listener.startEvent(event)); + gen.registerEventType("stop", ( event, listener ) -> listener.stopEvent(event)); + + gen.addListener(new TestListener() { + @Override + public void startEvent( TestEvent t ) { + assertEquals("start", t.getType()); + assertTrue(t.getData().startsWith("Start Event")); + } + + @Override + public void stopEvent( TestEvent t ) { + assertEquals("stop", t.getType()); + assertTrue(t.getData().startsWith("Stop Event")); + } + }); + + gen.dispatchEvent("start", new TestEvent("Start Event 1", true)); + gen.dispatchEvent("stop", new TestEvent("Stop Event 1", false)); + gen.dispatchEvent("stop", new TestEvent("Stop Event 2", false)); + gen.dispatchEvent("start", new TestEvent("Start Event 2", true)); + } + +} From d48b167fb389730c2cd34313426c0a90008871ec Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Fri, 8 Jul 2022 07:31:37 +0200 Subject: [PATCH 05/30] Renamed generator to dispatcher --- ...entGenerator.java => EventDispatcher.java} | 14 +++++++++--- src/schule/ngb/zm/media/Audio.java | 2 ++ src/schule/ngb/zm/media/AudioListener.java | 11 ++++++++++ src/schule/ngb/zm/media/Mixer.java | 11 ++++++++++ src/schule/ngb/zm/media/Music.java | 22 +++++++++++++++++++ src/schule/ngb/zm/media/Sound.java | 4 ++++ ...atorTest.java => EventDispatcherTest.java} | 4 ++-- 7 files changed, 63 insertions(+), 5 deletions(-) rename src/schule/ngb/zm/events/{EventGenerator.java => EventDispatcher.java} (78%) create mode 100644 src/schule/ngb/zm/media/AudioListener.java rename test/src/schule/ngb/zm/events/{EventGeneratorTest.java => EventDispatcherTest.java} (93%) diff --git a/src/schule/ngb/zm/events/EventGenerator.java b/src/schule/ngb/zm/events/EventDispatcher.java similarity index 78% rename from src/schule/ngb/zm/events/EventGenerator.java rename to src/schule/ngb/zm/events/EventDispatcher.java index f8834a0..61722c5 100644 --- a/src/schule/ngb/zm/events/EventGenerator.java +++ b/src/schule/ngb/zm/events/EventDispatcher.java @@ -1,22 +1,27 @@ package schule.ngb.zm.events; +import schule.ngb.zm.util.Validator; + import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.BiConsumer; -public class EventGenerator> { +public class EventDispatcher> { private CopyOnWriteArraySet listeners; private ConcurrentMap> eventRegistry; - public EventGenerator() { + public EventDispatcher() { listeners = new CopyOnWriteArraySet<>(); eventRegistry = new ConcurrentHashMap<>(); } public void registerEventType( String eventKey, BiConsumer dispatcher ) { + Validator.requireNotNull(eventKey); + Validator.requireNotNull(dispatcher); + if( !eventRegistered(eventKey) ) { eventRegistry.put(eventKey, dispatcher); } @@ -39,9 +44,12 @@ public class EventGenerator> { } public void dispatchEvent( String eventKey, final E event ) { + Validator.requireNotNull(eventKey); + Validator.requireNotNull(event); + if( eventRegistered(eventKey) ) { final BiConsumer dispatcher = eventRegistry.get(eventKey); - listeners.stream().forEach(( listener ) -> { + listeners.forEach(( listener ) -> { dispatcher.accept(event, listener); }); } diff --git a/src/schule/ngb/zm/media/Audio.java b/src/schule/ngb/zm/media/Audio.java index e2f5137..0292df6 100644 --- a/src/schule/ngb/zm/media/Audio.java +++ b/src/schule/ngb/zm/media/Audio.java @@ -5,6 +5,8 @@ package schule.ngb.zm.media; */ public interface Audio { + String getSource(); + /** * Prüft, ob das Medium gerade abgespielt wird. * diff --git a/src/schule/ngb/zm/media/AudioListener.java b/src/schule/ngb/zm/media/AudioListener.java new file mode 100644 index 0000000..1600e07 --- /dev/null +++ b/src/schule/ngb/zm/media/AudioListener.java @@ -0,0 +1,11 @@ +package schule.ngb.zm.media; + +import schule.ngb.zm.events.Listener; + +public interface AudioListener extends Listener