diff --git a/readme.md b/readme.md index 4058e2d..d0687fe 100644 --- a/readme.md +++ b/readme.md @@ -22,7 +22,7 @@ dependencies { ### Layout Setup -First, you need a parent `ViewGroup` that can vertically stack: +First, you need a parent `ViewGroup` that can vertically stack: 1. a behind-View to be revealed 2. a foreground-View to be scratched away @@ -94,7 +94,7 @@ public class MainActivity extends Activity implements ScratchoffController.Thres } ``` -### Finding the `ScratchoffController` +### Finding the `ScratchoffController` Find the `ScratchoffController` from the `ScratchableLayout` instances defined in the layout resource. @@ -108,7 +108,7 @@ ScratchoffController.findByViewId(view, R.id.scratch_view) // Find the ScratchoffController manually ((ScratchableLayout) activity.findViewById(R.id.scratch_view)) .getScratchoffController() -``` +``` ### Listening for threshold change / completion events @@ -138,7 +138,7 @@ ScratchoffController.findByViewId(activity, R.id.scratch_view) ### Attaching the `ScratchoffController` -To start the processors and allow scratching, call `attach()` on the `ScratchoffController` instance. +To start the processors and allow scratching, call `attach()` on the `ScratchoffController` instance. ```java ScratchoffController.findByViewId(activity, R.id.scratch_view) @@ -146,7 +146,7 @@ ScratchoffController.findByViewId(activity, R.id.scratch_view) .attach(); ``` -If the `ScratchableLayout` View has been restored, the dimensions match the persisted values, and state-restoration is enabled on the `ScratchoffController` instance, then attaching will attempt to restore the scratched path history from the cached state. If the restored state's threshold has already been reached, the content will be automatically cleared, regardless of desired clear animation behavior. +If the `ScratchableLayout` View has been restored, the dimensions match the persisted values, and state-restoration is enabled on the `ScratchoffController` instance, then attaching will attempt to restore the scratched path history from the cached state. If the restored state's threshold has already been reached, the content will be automatically cleared, regardless of desired clear animation behavior. ### Lifecycle @@ -163,13 +163,13 @@ public void onDestroy(){ ### Re-using the `ScratchoffController` -The `ScratchoffController` can be reset with the same call that started it: `ScratchController.attach()`. +The `ScratchoffController` can be reset with the same call that started it: `ScratchController.attach()`. However, **the background color of your scratchable layout must be manually set back to something opaque before calling it**, as the `ScratchableLayoutDrawer` will set the background to transparent when scratching is enabled. ```java public void onScratchThresholdReached(ScratchoffController controller) { - // Make sure to set the background of the foreground-View. + // Make sure to set the background of the foreground-View. // Don't worry, it's hidden if it cleared or still clearing. findViewById(R.id.scratch_view) .setBackgroundColor(0xFF3C9ADF); @@ -237,9 +237,10 @@ The size of the Bitmap used by the `ScratchoffThresholdProcessor` is determined It is recommended that you calculate the positions of the desired regions by their relative positioning from the edges of the original Bitmap. e.g. left = 0.25 * bitmap.width -## Upgrading from Version 1.x to Version 2.0.0 +## Migration Guides -Follow the [upgrade guide](https://github.com/jackpocket/android-scratchoff/raw/main/upgrade_1.x-2.0.md). +* [1.x-2.x](upgrade_1.x-2.0.md) +* [3.x-4.x](upgrade_3.x-4.0.md) ### Moved to MavenCentral diff --git a/scratchoff-sample/src/main/java/com/jackpocket/scratchoff/test/MainActivity.kt b/scratchoff-sample/src/main/java/com/jackpocket/scratchoff/test/MainActivity.kt index 87a2fd3..16ed420 100644 --- a/scratchoff-sample/src/main/java/com/jackpocket/scratchoff/test/MainActivity.kt +++ b/scratchoff-sample/src/main/java/com/jackpocket/scratchoff/test/MainActivity.kt @@ -34,9 +34,6 @@ class MainActivity: AppCompatActivity(), .setClearAnimationEnabled(true) .setClearAnimationDuration(1, TimeUnit.SECONDS) .setClearAnimationInterpolator(LinearInterpolator()) - .setUsePreDrawOverGlobalLayoutEnabled(true) - // .setAttemptLastDitchPostForLayoutResolutionFailure(true) - .setKeepListeningForDrawUntilValidSizeDiscovered(true) // .setTouchRadiusPx(25) // .setThresholdAccuracyQuality(Quality.LOW) // .setThresholdTargetRegionsProvider({ diff --git a/scratchoff/src/main/java/com/jackpocket/scratchoff/ScratchableLayoutDrawer.java b/scratchoff/src/main/java/com/jackpocket/scratchoff/ScratchableLayoutDrawer.java index d5a0747..a870a0f 100644 --- a/scratchoff/src/main/java/com/jackpocket/scratchoff/ScratchableLayoutDrawer.java +++ b/scratchoff/src/main/java/com/jackpocket/scratchoff/ScratchableLayoutDrawer.java @@ -7,8 +7,6 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.os.Build; -import android.os.Handler; -import android.os.Looper; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -62,13 +60,8 @@ public interface Delegate { private Long activeClearTag = 0L; - private boolean usePreDrawOverGlobalLayoutEnabled = false; - private boolean attemptLastDitchPostForLayoutResolutionFailure = false; - private boolean keepListeningForDrawUntilValidSizeDiscovered = false; - private WeakReference initializeLayoutTarget = new WeakReference<>(null); private ViewTreeObserver.OnGlobalLayoutListener initializationGlobalLayoutListener; - private ViewTreeObserver.OnPreDrawListener initializationPreDrawListener; public ScratchableLayoutDrawer(Delegate delegate) { this.delegate = new WeakReference<>(delegate); @@ -305,16 +298,6 @@ private void removeInitializationViewTreeObservers() { this.initializationGlobalLayoutListener = null; } - - ViewTreeObserver.OnPreDrawListener preDrawListener = this.initializationPreDrawListener; - - if (preDrawListener != null) { - layoutTarget - .getViewTreeObserver() - .removeOnPreDrawListener(preDrawListener); - - this.initializationPreDrawListener = null; - } } public void clear(boolean animationEnabled) { @@ -398,58 +381,27 @@ protected void hideAndMarkScratchableSurfaceViewCleared() { private void deferRunnableUntilViewIsLaidOut(final View view, final Runnable runnable) { this.initializeLayoutTarget = new WeakReference<>(view); - if (usePreDrawOverGlobalLayoutEnabled) { - deferRunnableWithPreDrawListener(view, runnable); - } - else { - deferRunnableWithGlobalLayoutListener(view, runnable); - } + deferRunnableWithGlobalLayoutListener(view, runnable); view.requestLayout(); } - private void deferRunnableWithPreDrawListener(final View view, final Runnable runnable) { - ViewTreeObserver.OnPreDrawListener preDrawListenerForInit = new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - boolean sizeValid = isViewSizeValidForInitialization(view); - - if (keepListeningForDrawUntilValidSizeDiscovered && !sizeValid) { - return true; - } - - ScratchableLayoutDrawer.this.initializationPreDrawListener = null; - - triggerOrPostRunnableOnLaidOut(runnable, sizeValid); - - view - .getViewTreeObserver() - .removeOnPreDrawListener(this); - - return true; - } - }; - - view - .getViewTreeObserver() - .addOnPreDrawListener(preDrawListenerForInit); - - this.initializationPreDrawListener = preDrawListenerForInit; - } - private void deferRunnableWithGlobalLayoutListener(final View view, final Runnable runnable) { ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { boolean sizeValid = isViewSizeValidForInitialization(view); - if (keepListeningForDrawUntilValidSizeDiscovered && !sizeValid) { + if (!sizeValid) { return; } ScratchableLayoutDrawer.this.initializationGlobalLayoutListener = null; - triggerOrPostRunnableOnLaidOut(runnable, sizeValid); + if (runnable != null) { + runnable.run(); + } + removeGlobalLayoutListener(view, this); } }; @@ -461,31 +413,6 @@ public void onGlobalLayout() { this.initializationGlobalLayoutListener = globalLayoutListener; } - /** - * This function will be removed in 4.x, given {@link attemptLastDitchPostForLayoutResolutionFailure} - * did more harm than good, and replaced with directly calling ``run`` on the ``Runnable``. - */ - protected void triggerOrPostRunnableOnLaidOut( - Runnable runnable, - boolean sizeValidForInitialization - ) { - - if (runnable == null) { - return; - } - - if (attemptLastDitchPostForLayoutResolutionFailure) { - if (!sizeValidForInitialization) { - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(runnable); - - return; - } - } - - runnable.run(); - } - private boolean isViewSizeValidForInitialization(View view) { return 0 < view.getWidth() && 0 < view.getHeight(); } @@ -516,30 +443,4 @@ public ScratchableLayoutDrawer setClearAnimationInterpolator(Interpolator clearA return this; } - - @SuppressWarnings("WeakerAccess") - public ScratchableLayoutDrawer setUsePreDrawOverGlobalLayoutEnabled(boolean usePreDrawOverGlobalLayoutEnabled) { - this.usePreDrawOverGlobalLayoutEnabled = usePreDrawOverGlobalLayoutEnabled; - - return this; - } - - @SuppressWarnings("WeakerAccess") - public ScratchableLayoutDrawer setAttemptLastDitchPostForLayoutResolutionFailure( - boolean attemptLastDitchPostForLayoutResolutionFailure - ) { - - this.attemptLastDitchPostForLayoutResolutionFailure = attemptLastDitchPostForLayoutResolutionFailure; - - return this; - } - - public ScratchableLayoutDrawer setKeepListeningForDrawUntilValidSizeDiscovered( - boolean keepListeningForDrawUntilValidSizeDiscovered - ) { - - this.keepListeningForDrawUntilValidSizeDiscovered = keepListeningForDrawUntilValidSizeDiscovered; - - return this; - } } diff --git a/scratchoff/src/main/java/com/jackpocket/scratchoff/ScratchoffController.java b/scratchoff/src/main/java/com/jackpocket/scratchoff/ScratchoffController.java index 0ec39cc..975195a 100644 --- a/scratchoff/src/main/java/com/jackpocket/scratchoff/ScratchoffController.java +++ b/scratchoff/src/main/java/com/jackpocket/scratchoff/ScratchoffController.java @@ -79,10 +79,6 @@ public interface ThresholdChangedListener { private final LinkedBlockingQueue history = new LinkedBlockingQueue(); private boolean stateRestorationEnabled; - private boolean usePreDrawOverGlobalLayoutEnabled = false; - private boolean attemptLastDitchPostForLayoutResolutionFailure = false; - private boolean keepListeningForDrawUntilValidSizeDiscovered = false; - /** * Create a new {@link ScratchoffController} instance targeting a scratchable layout. */ @@ -156,10 +152,7 @@ public ScratchoffController attach() { protected ScratchableLayoutDrawer createLayoutDrawer() { return new ScratchableLayoutDrawer(this) .setClearAnimationDurationMs(clearAnimationDurationMs) - .setClearAnimationInterpolator(clearAnimationInterpolator) - .setUsePreDrawOverGlobalLayoutEnabled(usePreDrawOverGlobalLayoutEnabled) - .setAttemptLastDitchPostForLayoutResolutionFailure(attemptLastDitchPostForLayoutResolutionFailure) - .setKeepListeningForDrawUntilValidSizeDiscovered(keepListeningForDrawUntilValidSizeDiscovered); + .setClearAnimationInterpolator(clearAnimationInterpolator); } protected ScratchoffThresholdProcessor createThresholdProcessor() { @@ -487,56 +480,6 @@ public ScratchoffController setStateRestorationEnabled(boolean stateRestorationE return this; } - /** - * Set whether or not to use the new {@link android.view.ViewTreeObserver.OnPreDrawListener} - * code paths to determine the layout sizing, instead of the original - * {@link android.view.ViewTreeObserver.OnGlobalLayoutListener} implementation. - * This is in attempt to fix #19 caused by the width or height of the View being - * zero when attempting to create the scratchable {@link Bitmap} instances. - * The default for this value is false for the original (crashing) behavior. - */ - public ScratchoffController setUsePreDrawOverGlobalLayoutEnabled(boolean usePreDrawOverGlobalLayoutEnabled) { - this.usePreDrawOverGlobalLayoutEnabled = usePreDrawOverGlobalLayoutEnabled; - - return this; - } - - /** - * Set whether or not to attempt one final last-ditch {@link android.os.Handler#post} on - * the main Thread when determining the layout sizing of our {@link #layoutDrawer} if - * our {@link android.view.ViewTreeObserver} attempt ran while the {@link #scratchableLayout}'s - * width or height is still zero. - * This is in attempt to fix #19 caused by the width or height of the View being - * zero when attempting to create the scratchable {@link Bitmap} instances. - * The default for this value is false for the original (crashing) behavior. - */ - public ScratchoffController setAttemptLastDitchPostForLayoutResolutionFailure( - boolean attemptLastDitchPostForLayoutResolutionFailure - ) { - - this.attemptLastDitchPostForLayoutResolutionFailure = attemptLastDitchPostForLayoutResolutionFailure; - - return this; - } - - /** - * Set whether or not to continue listening for {@link android.view.ViewTreeObserver.OnGlobalLayoutListener} - * or {@link android.view.ViewTreeObserver.OnPreDrawListener} events when the callbacks are - * triggered with a size that is invalid for initialization. Setting this to true will override the - * behavior for {@link setAttemptLastDitchPostForLayoutResolutionFailure}. - * This is in attempt to fix #19 caused by the width or height of the View being - * zero when attempting to create the scratchable {@link Bitmap} instances. - * The default for this value is false for the original (crashing) behavior. - */ - public ScratchoffController setKeepListeningForDrawUntilValidSizeDiscovered( - boolean keepListeningForDrawUntilValidSizeDiscovered - ) { - - this.keepListeningForDrawUntilValidSizeDiscovered = keepListeningForDrawUntilValidSizeDiscovered; - - return this; - } - public View getViewBehind() { return behindView.get(); } diff --git a/scratchoff/src/test/java/com/jackpocket/scratchoff/ScratchableLayoutDrawerTests.kt b/scratchoff/src/test/java/com/jackpocket/scratchoff/ScratchableLayoutDrawerTests.kt index 257d7a2..795f5ed 100644 --- a/scratchoff/src/test/java/com/jackpocket/scratchoff/ScratchableLayoutDrawerTests.kt +++ b/scratchoff/src/test/java/com/jackpocket/scratchoff/ScratchableLayoutDrawerTests.kt @@ -17,13 +17,7 @@ import com.jackpocket.scratchoff.views.ScratchableLinearLayout import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.times -import org.mockito.kotlin.verify import org.robolectric.annotation.GraphicsMode -import org.robolectric.annotation.LooperMode -import org.robolectric.shadows.ShadowLooper @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @@ -34,29 +28,18 @@ class ScratchableLayoutDrawerTests { @Test fun testSetsUpAndDrawsCorrectlyThenStopsDrawingAfterDestroy() { testSetsUpAndDrawsCorrectlyThenStopsDrawingAfterDestroy( - usePreDrawListener = false, keepListeningUntilLaidOut = false ) } @Test - fun testSetsUpAndDrawsCorrectlyThenStopsDrawingAfterDestroyPreDraw() { + fun testSetsUpAndDrawsCorrectlyThenStopsDrawingAfterDestroyAndKeepListening() { testSetsUpAndDrawsCorrectlyThenStopsDrawingAfterDestroy( - usePreDrawListener = true, - keepListeningUntilLaidOut = false - ) - } - - @Test - fun testSetsUpAndDrawsCorrectlyThenStopsDrawingAfterDestroyPreDrawAndKeepListening() { - testSetsUpAndDrawsCorrectlyThenStopsDrawingAfterDestroy( - usePreDrawListener = true, keepListeningUntilLaidOut = true ) } private fun testSetsUpAndDrawsCorrectlyThenStopsDrawingAfterDestroy( - usePreDrawListener: Boolean, keepListeningUntilLaidOut: Boolean ) { @@ -68,6 +51,8 @@ class ScratchableLayoutDrawerTests { view.setBackgroundColor(Color.WHITE) if (keepListeningUntilLaidOut) { + // Layout with an invalid size so we force the drawer + // to continue listening view.layout(0, 0, 0, 10) } else { @@ -81,23 +66,16 @@ class ScratchableLayoutDrawerTests { .apply({ this.color = Color.BLACK }) } } - drawer.setUsePreDrawOverGlobalLayoutEnabled(usePreDrawListener) - drawer.setKeepListeningForDrawUntilValidSizeDiscovered(keepListeningUntilLaidOut) drawer.attach(1, view, null) - if (usePreDrawListener) { - view.viewTreeObserver.dispatchOnPreDraw() - } - else { - view.viewTreeObserver.dispatchOnGlobalLayout() - } + view.viewTreeObserver.dispatchOnGlobalLayout() // We can remove this to confirm the tests fail as the // view will never have laid out with a valid size if (keepListeningUntilLaidOut) { view.layout(0, 0, 10, 10) - view.viewTreeObserver.dispatchOnPreDraw() + view.viewTreeObserver.dispatchOnGlobalLayout() } drawer.addScratchPathPoints( @@ -125,22 +103,6 @@ class ScratchableLayoutDrawerTests { @Test fun testRemovesGlobalLayoutInitListenerOnDestroy() { - testRemovesInitListenerOnDestroy( - usePreDrawListener = false - ) - } - - @Test - fun testRemovesPreDrawInitListenerOnDestroy() { - testRemovesInitListenerOnDestroy( - usePreDrawListener = true - ) - } - - private fun testRemovesInitListenerOnDestroy( - usePreDrawListener: Boolean - ) { - val result = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) val resultCanvas = Canvas(result) val fullSizeRegion = ThresholdCalculator.createFullSizeThresholdRegion(result) @@ -156,16 +118,10 @@ class ScratchableLayoutDrawerTests { .apply({ this.color = Color.BLACK }) } } - drawer.setUsePreDrawOverGlobalLayoutEnabled(usePreDrawListener) drawer.attach(1, view, null) drawer.destroy() - if (usePreDrawListener) { - view.viewTreeObserver.dispatchOnPreDraw() - } - else { - view.viewTreeObserver.dispatchOnGlobalLayout() - } + view.viewTreeObserver.dispatchOnGlobalLayout() drawer.addScratchPathPoints( listOf( @@ -259,54 +215,4 @@ class ScratchableLayoutDrawerTests { assertEquals(10, scratchView.layoutParams.width) assertEquals(20, scratchView.layoutParams.height) } - - @Test - fun testTriggerOrPostRunnableOnLaidOutTriggersImmediatelyWhenAttemptPostDisabledAndWidthHeightZero() { - val delegate = mock<() -> Unit>() - - val view = View(context) - view.layout(0, 0, 0, 0) - - val drawer = ScratchableLayoutDrawer(null) - drawer.triggerOrPostRunnableOnLaidOut(delegate, false) - - verify(delegate, times(1)) - .invoke() - } - - @Test - fun testTriggerOrPostRunnableOnLaidOutTriggersImmediatelyWhenAttemptPostEnabledAndWidthHeightNotZero() { - val delegate = mock<() -> Unit>() - - val view = View(context) - view.layout(0, 0, 1, 1) - - val drawer = ScratchableLayoutDrawer(null) - drawer.setAttemptLastDitchPostForLayoutResolutionFailure(true) - drawer.triggerOrPostRunnableOnLaidOut(delegate, true) - - verify(delegate, times(1)) - .invoke() - } - - @Test - @LooperMode(LooperMode.Mode.PAUSED) - fun testTriggerOrPostRunnableOnLaidOutTriggersDelayedWhenAttemptPostEnabledAndWidthHeightZero() { - val delegate = mock<() -> Unit>() - - val view = View(context) - view.layout(0, 0, 0, 0) - - val drawer = ScratchableLayoutDrawer(null) - drawer.setAttemptLastDitchPostForLayoutResolutionFailure(true) - drawer.triggerOrPostRunnableOnLaidOut(delegate, false) - - verify(delegate, never()) - .invoke() - - ShadowLooper.runUiThreadTasks() - - verify(delegate, times(1)) - .invoke() - } } \ No newline at end of file diff --git a/scratchoff/src/test/java/com/jackpocket/scratchoff/ScratchoffControllerTests.kt b/scratchoff/src/test/java/com/jackpocket/scratchoff/ScratchoffControllerTests.kt index f1d8861..d16eb2c 100644 --- a/scratchoff/src/test/java/com/jackpocket/scratchoff/ScratchoffControllerTests.kt +++ b/scratchoff/src/test/java/com/jackpocket/scratchoff/ScratchoffControllerTests.kt @@ -428,17 +428,6 @@ class ScratchoffControllerTests { controller.setTouchRadiusPx(0) } - fun testCreateLayoutDrawerWithFix19ChangesEnabled() { - // This is purely to ensure the new ScratchoffController code paths run under our - // test environment even though they will be removed in a future release. - // We're not going to actively expose these temporary variables, so there will - // be no actual assertions made in this test, which will ultimately be removed. - val controller = ScratchoffController(mockScratchableLayout) - controller.setUsePreDrawOverGlobalLayoutEnabled(true) - controller.setAttemptLastDitchPostForLayoutResolutionFailure(true) - controller.createLayoutDrawer() - } - private class LoggingThresholdChangedListener: ScratchoffController.ThresholdChangedListener { var threshold: Float = 0f diff --git a/upgrade_3.x-4.0.md b/upgrade_3.x-4.0.md new file mode 100644 index 0000000..631af3c --- /dev/null +++ b/upgrade_3.x-4.0.md @@ -0,0 +1,9 @@ +# Upgrading from version 3.x to version 4.0.0 + +Version 3.x | Version 4.0.0 +--- | --- +`ScratchoffController.setUsePreDrawOverGlobalLayoutEnabled(boolean)` | None +`ScratchoffController.setAttemptLastDitchPostForLayoutResolutionFailure(boolean)` | None +`ScratchoffController.setKeepListeningForDrawUntilValidSizeDiscovered(boolean)` | None + +With 4.x, it's also important that `ScratchoffController.onDestroy` is properly called to ensure the strongly-referenced `OnGlobalLayoutListener` is removed from the `ViewTreeObserver`. \ No newline at end of file