A simple library for implementing scratchable Views.
repositories {
mavenCentral()
}
dependencies {
compile('com.jackpocket:scratchoff:3.1.1')
}First, you need a parent ViewGroup that can vertically stack:
- a behind-View to be revealed
- a foreground-View to be scratched away
Here is an example using the ScratchableLinearLayout:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent" >
<RelativeLayout
android:id="@+id/scratch_view_behind"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#818B8D"
android:padding="25dip" >
<ImageView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:adjustViewBounds="true"
android:src="@drawable/some_drawable_to_be_revealed" />
</RelativeLayout>
<com.jackpocket.scratchoff.views.ScratchableLinearLayout
android:id="@+id/scratch_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#3C9ADF"
android:padding="25dip" >
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:gravity="center"
android:src="@drawable/some_top_drawable" />
</com.jackpocket.scratchoff.views.ScratchableLinearLayout>
</RelativeLayout>By default, the ScratchoffController will not adjust the width or height of the scratchable layout's LayoutParams. To enable this behavior, call setMatchLayoutWithBehindView(View) with the behind-View whose width/height should be matched with before attach().
If you choose not to utilize the available ScratchableLinearLayout or the ScratchableRelativeLayout, you will need to manually handle delegation to ScratchoffController.onDraw(Canvas).
The ScratchoffController will call the supplied ScratchoffController.ThresholdChangedListener methods when the scratched threshold has changed or has been reached, but a strong reference to it must be maintained.
public class MainActivity extends Activity implements ScratchoffController.ThresholdChangedListener {
@Override
public void onScratchPercentChanged(ScratchoffController controller, float percentCompleted) {
// This will be called on the main thread any time the scratch threshold has changed.
// The values will be between [0.0, 100.0]
}
@Override
public void onScratchThresholdReached(ScratchoffController controller) {
// This is called on the main thread the moment we know the scratched threshold has been reached.
// If the fade-on-clear animation is enabled, it will already have been started, but not completed.
}
}Find the ScratchoffController from the ScratchableLayout instances defined in the layout resource.
// Find the ScratchoffController in your Activity layout
ScratchoffController.findByViewId(activity, R.id.scratch_view)
// Find the ScratchoffController in some View's layout
ScratchoffController.findByViewId(view, R.id.scratch_view)
// Find the ScratchoffController manually
((ScratchableLayout) activity.findViewById(R.id.scratch_view))
.getScratchoffController()In order to receive threshold change and completion events, a ScratchoffController.ThresholdChangedListener must be set on the ScratchoffController instance.
ScratchoffController.findByViewId(activity, R.id.scratch_view)
.setThresholdChangedListener(listener)
...Note: The ScratchoffController.ThresholdChangedListener instance is weakly-held
Certain properties, like thresholds, scratch sizes, or clearing animation behavior, can be overridden before attaching the ScratchoffController instance.
ScratchoffController.findByViewId(activity, R.id.scratch_view)
...
.setThresholdPercent(0.40f)
.setTouchRadiusDip(context, 30)
.setClearAnimationEnabled(true)
.setClearOnThresholdReached(true)
...To start the processors and allow scratching, call attach() on the ScratchoffController instance.
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.
Ensure that onDestroy() is called from the correct lifecycle method so that resources can be properly recycled.
@Override
public void onDestroy(){
controller.onDestroy();
super.onDestroy();
}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.
public void onScratchThresholdReached(ScratchoffController controller) {
// 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);
// Reset after a delay, as the clearing animation may still be running at this point
new Handler(Looper.getMainLooper())
.postDelayed(() -> controller.attach(), 2000);
}You can add an OnTouchListener to the ScratchoffController to observe MotionEvents as they come in, regardless of enabled state. When adding these observers, make sure to remove them in the appropriate lifecycle methods.
@Override
public void onPause(){
...
controller.removeTouchObservers()
super.onPause()
}
@Override
public void onResume(){
super.onResume()
controller.addTouchObserver((view, event) -> {
// Do something on a particular MotionEvent?
});
...
}Note 1: the return values of onTouch will be ignored, as the ScratchoffController must maintain control of the touch event collection.
Note 2: all touch observers will automatically be removed when calling ScratchoffController.onDestroy()
By default, the ScratchoffController will use a Bitmap of the same size as the actual scratchable layout. To reduce the memory impact, you can set a ScratchoffThresholdProcessor.Quality value other than HIGH. A MEDIUM Quality would attempt a 50% reduction in size, while LOW will attempt to go as low as 1 / min(touchRadius, width, height). The scalar is then applied to both the x and y coordinates of the touch events used to calculate the threshold.
ScratchoffController.findByViewId(activity, R.id.scratch_view)
...
.setThresholdAccuracyQuality(ScratchoffThresholdProcessor.Quality.LOW)The ScratchoffController can be configured to calculate the thresholds of specific rectangular regions by supplying it with a ScratchoffThresholdProcessor.TargetRegionsProvider. Only regions returned by the call to createScratchableRegions would be used for evaluating which areas of a scratchable layout are used when determining the total scratched percentage.
ScratchoffController.findByViewId(activity, R.id.scratch_view)
...
.setThresholdTargetRegionsProvider((source) -> {
ArrayList<Rect> regions = new ArrayList<>();
regions.add(Rect(0, 0, source.getWidth(), source.getHeight()))
return regions;
})The size of the Bitmap used by the ScratchoffThresholdProcessor is determined by the ScratchoffThresholdProcessor.Quality and the runtime conditions of the scratchable layout. If the quality is not set to ScratchoffThresholdProcessor.Quality#HIGH, the Bitmap will likely be much smaller than the size on screen.
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
Follow the upgrade guide.
As of version 2.0.0, scratchoff will be hosted on MavenCentral. Versions 1.x and below will remain on JCenter.
