Scratch Card View like google pay in Android with Example
Scratch Card View is among the most popular UI components found in Android applications. This kind of UI component is typically used in payment applications like Google pay, among others payment applications. If you’re an Android developer, you will be amazed at how we can implement this kind of UI component within our Android application. In this post, we’ll review the way to implement Scratch Card View in Android. To implement Scratch Card View we’ll use the library available on GitHub. With this library, we’ll make a basic Scratch Card View and we will show it
Scratch Card View like Google pay Example
So let’s start to create a new project and build and wait for successfully syn and the follow below steps. for making scratch card views like google play. When the user scratches it and wins the point and price.
Step 1: Open android studio and create a new project.
create a new project and sync it. and add the below dependency.
dependencies { implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'com.google.android.material:material:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' }
Step 2: Create a res file in value folder XML file attrs Add below code.
create a res file attrs in the value folder and code below code.
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="ScratchView"> <attr name="overlay_image" format="reference" /> <attr name="tile_mode" format="string" /> <attr name="overlay_width" format="dimension" /> <attr name="overlay_height" format="dimension" /> </declare-styleable> </resources>
Step 3: – Create a New Java class with the name ScratchView and extends View
create a new java class in this class I make a scratch view using with canva and paint to drow and errase the view flow. used below code for making scratch view.
package com.codeplayon.scratchview; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import androidx.core.content.ContextCompat; import com.codeplayon.scratchview.utils.BitmapUtils; public class ScratchView extends View { public static final float STROKE_WIDTH = 12f; private static final float TOUCH_TOLERANCE = 4; private final Context mContext; Bitmap scratchBitmap; private AttributeSet attrs; private int styleAttr; private View view; private float mX, mY; private Bitmap mScratchBitmap; private Canvas mCanvas; private Path mErasePath; private Path mTouchPath; private Paint mBitmapPaint; private Paint mErasePaint; private Paint mGradientBgPaint; private BitmapDrawable mDrawable; private IRevealListener mRevealListener; private float mRevealPercent; private int mThreadCount = 0; public ScratchView(Context context) { super(context); this.mContext = context; init(); } public ScratchView(Context context, AttributeSet attrs) { super(context, attrs); this.mContext = context; this.attrs = attrs; init(); } public ScratchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; this.attrs = attrs; this.styleAttr = defStyleAttr; init(); } private void init() { mTouchPath = new Path(); mErasePaint = new Paint(); mErasePaint.setAntiAlias(true); mErasePaint.setDither(true); mErasePaint.setColor(0xFFFF0000); mErasePaint.setStyle(Paint.Style.STROKE); mErasePaint.setStrokeJoin(Paint.Join.BEVEL); mErasePaint.setStrokeCap(Paint.Cap.ROUND); mErasePaint.setXfermode(new PorterDuffXfermode( PorterDuff.Mode.CLEAR)); setStrokeWidth(6); mGradientBgPaint = new Paint(); mErasePath = new Path(); mBitmapPaint = new Paint(Paint.DITHER_FLAG); TypedArray arr = mContext.obtainStyledAttributes(attrs, R.styleable.ScratchView, styleAttr, 0); int overlayImage = arr.getResourceId(R.styleable.ScratchView_overlay_image, R.drawable.ic_scratch_pattern); float overlayWidth = arr.getDimension(R.styleable.ScratchView_overlay_width, 1000); float overlayHeight = arr.getDimension(R.styleable.ScratchView_overlay_height, 1000); String tileMode = arr.getString(R.styleable.ScratchView_tile_mode); if (tileMode == null) { tileMode = "CLAMP"; } scratchBitmap = BitmapFactory.decodeResource(getResources(), overlayImage); if (scratchBitmap == null) { scratchBitmap = drawableToBitmap(ContextCompat.getDrawable(getContext(), overlayImage)); } scratchBitmap = Bitmap.createScaledBitmap(scratchBitmap, (int) overlayWidth, (int) overlayHeight, false); mDrawable = new BitmapDrawable(getResources(), scratchBitmap); switch (tileMode) { case "REPEAT": mDrawable.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); break; case "MIRROR": mDrawable.setTileModeXY(Shader.TileMode.MIRROR, Shader.TileMode.MIRROR); break; default: mDrawable.setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); } } /** * Set the strokes width based on the parameter multiplier. * * @param multiplier can be 1,2,3 and so on to set the stroke width of the paint. */ public void setStrokeWidth(int multiplier) { mErasePaint.setStrokeWidth(multiplier * STROKE_WIDTH); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mScratchBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mScratchBitmap); Rect rect = new Rect(0, 0, getWidth(), getHeight()); mDrawable.setBounds(rect); int startGradientColor = ContextCompat.getColor(getContext(), R.color.scratch_start_gradient); int endGradientColor = ContextCompat.getColor(getContext(), R.color.scratch_end_gradient); mGradientBgPaint.setShader(new LinearGradient(0, 0, 0, getHeight(), startGradientColor, endGradientColor, Shader.TileMode.MIRROR)); mCanvas.drawRect(rect, mGradientBgPaint); mDrawable.draw(mCanvas); // Toast.makeText(mContext, String.valueOf(getWidth()), Toast.LENGTH_LONG).show(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawBitmap(mScratchBitmap, 0, 0, mBitmapPaint); canvas.drawPath(mErasePath, mErasePaint); } private void touch_start(float x, float y) { mErasePath.reset(); mErasePath.moveTo(x, y); mX = x; mY = y; } /** * clears the scratch area to reveal the hidden image. */ public void clear() { int[] bounds = getViewBounds(); int left = bounds[0]; int top = bounds[1]; int right = bounds[2]; int bottom = bounds[3]; int width = right - left; int height = bottom - top; int centerX = left + width / 2; int centerY = top + height / 2; left = centerX - width / 2; top = centerY - height / 2; right = left + width; bottom = top + height; Paint paint = new Paint(); paint.setXfermode(new PorterDuffXfermode( PorterDuff.Mode.CLEAR)); mCanvas.drawRect(left, top, right, bottom, paint); checkRevealed(); invalidate(); } private void touch_move(float x, float y) { float dx = Math.abs(x - mX); float dy = Math.abs(y - mY); if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { mErasePath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); mX = x; mY = y; drawPath(); } mTouchPath.reset(); mTouchPath.addCircle(mX, mY, 30, Path.Direction.CW); } private void drawPath() { mErasePath.lineTo(mX, mY); // commit the path to our offscreen mCanvas.drawPath(mErasePath, mErasePaint); // kill this so we don't double draw mTouchPath.reset(); mErasePath.reset(); mErasePath.moveTo(mX, mY); checkRevealed(); } public void reveal() { clear(); } public void mask() { clear(); mRevealPercent = 0; mCanvas.drawBitmap(scratchBitmap, 0, 0, mBitmapPaint); invalidate(); } private void touch_up() { drawPath(); } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: touch_start(x, y); invalidate(); break; case MotionEvent.ACTION_MOVE: touch_move(x, y); invalidate(); break; case MotionEvent.ACTION_UP: touch_up(); invalidate(); break; default: break; } return true; } public int getColor() { return mErasePaint.getColor(); } public Paint getErasePaint() { return mErasePaint; } public void setEraserMode() { getErasePaint().setXfermode(new PorterDuffXfermode( PorterDuff.Mode.CLEAR)); } public void setRevealListener(IRevealListener listener) { this.mRevealListener = listener; } public boolean isRevealed() { return mRevealPercent >= 0.33; } private void checkRevealed() { if (!isRevealed() && mRevealListener != null) { int[] bounds = getViewBounds(); int left = bounds[0]; int top = bounds[1]; int width = bounds[2] - left; int height = bounds[3] - top; // Do not create multiple calls to compare. if (mThreadCount > 1) { Log.d("Captcha", "Count greater than 1"); return; } mThreadCount++; new AsyncTask<Integer, Void, Float>() { @Override protected Float doInBackground(Integer... params) { try { int left = params[0]; int top = params[1]; int width = params[2]; int height = params[3]; Bitmap croppedBitmap = Bitmap.createBitmap(mScratchBitmap, left, top, width, height); return BitmapUtils.getTransparentPixelPercent(croppedBitmap); } finally { mThreadCount--; } } public void onPostExecute(Float percentRevealed) { // check if not revealed before. if (!isRevealed()) { float oldValue = mRevealPercent; mRevealPercent = percentRevealed; if (oldValue != percentRevealed) { mRevealListener.onRevealPercentChangedListener(ScratchView.this, percentRevealed); } // if now revealed. if (isRevealed()) { mRevealListener.onRevealed(ScratchView.this); } } } }.execute(left, top, width, height); } } public int[] getViewBounds() { int left = 0; int top = 0; int width = getWidth(); int height = getHeight(); return new int[]{left, top, left + width, top + height}; } private Bitmap drawableToBitmap(Drawable drawable) { Bitmap bitmap; if (drawable instanceof BitmapDrawable) { BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; if (bitmapDrawable.getBitmap() != null) { return bitmapDrawable.getBitmap(); } } if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); } else { bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); } Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } public interface IRevealListener { void onRevealed(ScratchView scratchView); void onRevealPercentChangedListener(ScratchView scratchView, float percent); } }
Step 4 :- Create New Java class with name BitmapUtils
create a new java class with the name bitmap utils for captchuring the empty view percentage % .
package com.codeplayon.scratchview.utils; import android.graphics.Bitmap; import java.nio.ByteBuffer; public class BitmapUtils { /** * Compares two bitmaps and gives the percentage of similarity * * @param bitmap1 input bitmap 1 * @param bitmap2 input bitmap 2 * @return a value between 0.0 to 1.0 . Note the method will return 0.0 if either of bitmaps are null nor of same size. */ public static float compareEquivalance(Bitmap bitmap1, Bitmap bitmap2) { if (bitmap1 == null || bitmap2 == null || bitmap1.getWidth() != bitmap2.getWidth() || bitmap1.getHeight() != bitmap2.getHeight()) { return 0f; } ByteBuffer buffer1 = ByteBuffer.allocate(bitmap1.getHeight() * bitmap1.getRowBytes()); bitmap1.copyPixelsToBuffer(buffer1); ByteBuffer buffer2 = ByteBuffer.allocate(bitmap2.getHeight() * bitmap2.getRowBytes()); bitmap2.copyPixelsToBuffer(buffer2); byte[] array1 = buffer1.array(); byte[] array2 = buffer2.array(); int len = array1.length; // array1 and array2 will be of some length. int count = 0; for (int i = 0; i < len; i++) { if (array1[i] == array2[i]) { count++; } } return ((float) (count)) / len; } /** * Finds the percentage of pixels that do are empty. * * @param bitmap input bitmap * @return a value between 0.0 to 1.0 . Note the method will return 0.0 if either of bitmaps are null nor of same size. */ public static float getTransparentPixelPercent(Bitmap bitmap) { if (bitmap == null) { return 0f; } ByteBuffer buffer = ByteBuffer.allocate(bitmap.getHeight() * bitmap.getRowBytes()); bitmap.copyPixelsToBuffer(buffer); byte[] array = buffer.array(); int len = array.length; int count = 0; for (byte b : array) { if (b == 0) { count++; } } return ((float) (count)) / len; } }
Step 5: Main Activity UI XML code.
In your main activity you can create a UI for drow and showing functly for scratch view.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.cardview.widget.CardView android:layout_width="210dp" android:layout_height="210dp" android:background="@color/white" app:cardCornerRadius="10dp" app:cardElevation="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" android:contentDescription="@string/app_name" android:gravity="center" android:orientation="vertical" tools:ignore="UseCompoundDrawables"> <ImageView android:layout_width="100dp" android:layout_height="100dp" android:contentDescription="@string/app_name" android:src="@drawable/trophy" /> <TextView android:id="@+id/tvMessage" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:gravity="center" android:text="You've won\n $ 200" android:textColor="@color/black" android:textSize="20sp" android:textStyle="bold" /> </LinearLayout> <com.codeplayon.scratchview.ScratchView android:id="@+id/scratchView" android:layout_width="match_parent" android:layout_height="match_parent" app:overlay_height="210dp" app:overlay_image="@drawable/scratch_top" app:overlay_width="210dp" /> </androidx.cardview.widget.CardView> <Button android:id="@+id/mask" android:layout_width="200dp" android:layout_height="50dp" android:gravity="center" android:text="Re Try" android:textColor="@color/white" tools:ignore="MissingConstraints" android:layout_marginTop="350dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
Step 6: Main Activity Java Code.
Implimant all the funcslty in your main activity. show you scratch view and when user easrs the 70% of scratch card show the reward message in toast.
package com.codeplayon.scratchview; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.Button; import android.widget.Toast; public class MainActivity extends AppCompatActivity { ScratchView scratchView; Button mask; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mask = findViewById(R.id.mask); scratchView = findViewById(R.id.scratchView); scratchView.setRevealListener(new ScratchView.IRevealListener() { @Override public void onRevealed(ScratchView scratchView) { Toast.makeText(getApplicationContext(), "Reveled", Toast.LENGTH_LONG).show(); } @Override public void onRevealPercentChangedListener(ScratchView scratchView, float percent) { if (percent >= 0.7) { Log.d("Reveal Percentage", "onRevealPercentChangedListener: " + percent); } } }); mask.setOnClickListener(v -> scratchView.mask()); } }
Now its complete you can run your App and see result. also you can add image as your reqarment in this code drawable image is not add you can make it in your end.
Read More:-
- Codeplayon Jetpack Compose Tutorial
- Codeplayon Android Tutorial
- Codeplayon Flutter Tutorial
- Codeplayon on Github