Pattern Lock View | Jetpack Compose Canvas | Android Studio | Kotlin

In android, pattern view is our first Security warrior as we all use this to protect our mobile from unauthorized access. You can find many 3rd party library which can provide you the pattern view lock functionality to impliment in you android app but we will build our custom pattern view using Jetpack Compose.

To create a basic 3 by 3 pattern view we need 9 circles. These boxes will be arranged in grid of 3 column by 3 rows. So to show the items in the grid we need to create a data class to hold the information about each item. This class will contain 3 variables as follows:

  1. offSet : Offset – it will contain the location information of the item
  2. color : Color – the color of the each circle
  3. strokeWidth : Float – the stroke of the circle
data class CellModel(
    val offset: Offset,
    val color: Color = Color.Blue.copy(0.5f),
    val strokeWidth: Float = 5f
)

Now create some variables to hold information about our patten while re-composition.

  1. selectedCellList – this list will contain all the cells selected by the user
  2. selectedCellIndex – it will hold info about the index of current selected cell
  3. selectedCellsIndexList – this list will contain index info of the selected cells
  4. selectedCellsCenterList – this list will hold the center offset of each selected cells.
val rowCount = 3
val columnCount = 3

val selectedCellsList = remember { mutableStateListOf<CellModel>() }

var selectedCellIndex by remember { mutableIntStateOf(0) }

val selectedCellsIndexList = remember {
    mutableStateListOf<Int?>()
}

val selectedCellCenterList = remember { mutableStateListOf(Offset.Zero) }

var text by remember {
    mutableStateOf("Draw Your Pattern")
}

The logic is like this, when user touch the screen then we save the information of that offset in our cell data and keep add the cells info till the user keep moving the fingers. As we decided to show 9 circles as 3 row in 3 column format and each circle will have a certain radius. So the idea is that if the user passes his fingers through any of the circle area then that will be count as selected cell else not. we will be keep doing so thill he lifts his finger up.

We will use the Canvas composable to show our pattern. To detect the dragging we will use PointerInteropFilter modifier to catch the dragging motions. It can handle 3 type of event as follows:

  1. ActionDown – when user first time touch the screen
  2. ActionMove – when user starts dragging the fingers
  3. AcitonUp – when user lifts up his fingers

When user touch down then create the cell data and insert into the selectedCellList list and clear the selectedCellCenterList. Then while dragging keep adding the cell info to the selectedCellList list.

Canvas(
  modifier = Modifier
      .fillMaxWidth(
      .height(400.dp)
    .background(Color.White)
    .pointerInteropFilter {
      when (it.action) {
        MotionEvent.ACTION_DOWN -> {
            val cell = CellModel(
                offset = Offset(it.x, it.y)
            )
            //add new cell to list
            selectedCellsList.add(cell)
            //clear previous cells centers
            selectedCellCenterList.clear()
        }

        MotionEvent.ACTION_MOVE -> {
            text = "Release Finger When Done"
            val cell = CellModel(
                offset = Offset(it.x, it.y)
            )
            selectedCellsList.add(cell)
        }

        MotionEvent.ACTION_UP -> {
            text = "Selected Index : ${selectedCellsIndexList.toList()}"
        }
    }
    true
}
)

Now come inside the canvas and draw our circles using drawCircle function. To draw circle we need to find the size of each circle so lets define some variables and draw the circle.

val width = size.width
val height = size.height

val boxSizeInX = width / 3
val boxCenterInX = boxSizeInX / 2
val boxSizeInY = height / 3
val boxCenterInY = boxSizeInY / 2

val circleRadius = width / 8

//to draw all dots
for (row in 0..<rowCount) {
    for (column in 0..<columnCount) {
        drawCircle(
            color = Color.Black,
            radius = 25f,
            center = Offset((boxCenterInX + boxSizeInX * column), (boxCenterInY + boxSizeInY * row))
        )
    }
}

Now we will loop through our selected cell list and get index and offset of each cell. Inside this loop we will do following calculation :

  1. find Row and Column Index- with help of offset we can find the index of each selected cell
  2. find cellCenter – we need to find the center offset of each selected cell
  3. calculate distance from center of circle to the touch point- we need to find the distance to that we can compare it with the circle radius as if this distance is more than than radius means user has dragged outside the circle so no need include this circle
  4. Finally draw the circle around all the selected cells and a line joining each circle.
selectedCellsList.forEachIndexed { index, offset ->
    //first find the index of each cell we have selected
    //index will vary b/w 0,1,2
    val columnIndex = (offset.offset.x / width * columnCount).toInt()
    val rowIndex = (offset.offset.y / height * rowCount).toInt()

    val pathOffset = Offset(offset.offset.x, offset.offset.y)
    val currentCellCenter =
        Offset((boxCenterInX + boxSizeInX * columnIndex), (boxCenterInY + boxSizeInY * rowIndex))

    //use pythagoras  theorem ->  r*r = X*X + Y*Y
    val distanceFromCenter = sqrt(
        (pathOffset.x - currentCellCenter.x) * (pathOffset.x - currentCellCenter.x) +
                ((pathOffset.y - currentCellCenter.y) * (pathOffset.y - currentCellCenter.y))
    )

    //we will only include a cell if we move fingers only through a specific radius around the cell
    if (distanceFromCenter < circleRadius) {
        selectedCellIndex = columnIndex + 1 + rowIndex * columnCount

        if (!selectedCellsIndexList.contains(selectedCellIndex) && selectedCellIndex > 0) {
            selectedCellsIndexList.add(selectedCellIndex)
        }

        if (!selectedCellCenterList.contains(currentCellCenter)) {
            selectedCellCenterList.add(currentCellCenter)
        }

        drawCircle(
            offset.color,
            center = currentCellCenter,
            radius = 50f,
            style = Stroke(offset.strokeWidth)
        )

    }
}

Now we also want to show a line joining each selected circle. So for this we will create a path of all the selected cells center offset and draw a path adjoining all the points.

//to draw green line behind
if (selectedCellCenterList.size > 1) {
    //First create path for line
    val path = Path().apply {
        //start from first circle and draw line till the end one
        selectedCellCenterList.forEachIndexed { index, offset ->
            if (index == 0) {
                moveTo(offset.x, offset.y)
            } else {
                lineTo(
                    (offset.x),
                    (offset.y)
                )
            }
        }
    }

    //finally draw the path
    drawPath(path, Color.Blue.copy(0.5f), style = Stroke(15f))
}

GitHub gist for this video code : here

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top