Unlocking a phone with precision using RxJS-powered swipe gestures
How cool would it be to have a phone unlock pattern component? In this recipe, we’re going to build a component like that so that we can seamlessly react to every user touch swipe, orchestrate all user events, and unlock the phone once a correct combination of numbers is entered.
How to do it…
To create a phone unlock component, we’ll create UI controls representing number pads and identify key events to react to user actions. Once the user lifts their finger off the screen, we’ll compare the result with the correct pattern to unlock our phone.
Step 1 – Creating number pads
Our swipe-unlock.component.html
file must contain the following markup for the swipe area and all phone buttons:
<div #swipeArea class="swipe-area">
<button #one class="number">1</button>
<button #two class="number">2</button>
<button #three class="number">3</button>
<button #four class="number">4</button>
<button #five class="number">5</button>
<button #six class="number">6</button>
<button #seven class="number">7</button>
<button #eight class="number">8</button>
<button #nine class="number">9</button>
<button #zero class="number">0</button>
</div>
With a little bit of CSS magic, we can see the component in the UI:

Figure 2.1: Phone swipe component
Meanwhile, in our swipe-unlock.component.ts
file, we can reference various elements of the number pad’s UI so that we can manipulate any events that are performed on them:
@ViewChild('swipeArea')
swipeArea!: ElementRef;
@ViewChildren('one, two, three, four, five, six, seven,
eight, nine, zero')
numbers!: QueryList<ElementRef>;
Step 2 – Identifying user touch events
What we’re interested in are the events where a user touches the screen, moves (swipes), and lifts their finger off the screen. We can create those streams of events like so:
const touchStart$ = fromEvent<TouchEvent>(
this.swipeArea.nativeElement,
'touchstart'
);
const touchMove$ = fromEvent<TouchEvent>(
this.swipeArea.nativeElement,
'touchmove'
);
const touchEnd$ = fromEvent<TouchEvent>(
this.swipeArea.nativeElement,
'touchend'
);
From here, we can react to these events and figure out the coordinates of a touch event, check if it’s intersecting with the number pad area, and highlight it in the UI:
const swipe$ = touchStart$.pipe(
switchMap(() =>
touchMove$.pipe(
takeUntil(touchEnd$),
map((touchMove) => ({
x: touchMove.touches[0].clientX,
y: touchMove.touches[0].clientY,
}))
)
),
);
Now, when we subscribe to those swipe coordinates, we can perform the required actions in sequence, such as selecting the number pad and creating a dot trail:
swipe$.pipe(
tap((dot) => this.selectNumber(dot)),
mergeMap((dot) => this.createTrailDot(dot)),
).subscribe();
Step 3 – Marking selected number pads
After getting the coordinates from each swipe, we can easily check whether it’s intersecting the area surrounding the number pad:
private selectNumber(dot: PixelCoordinates): void {
this.numbersElement.forEach((number) => {
if (
dot.y > number.getBoundingClientRect().top &&
dot.y < number.getBoundingClientRect().bottom &&
dot.x > number.getBoundingClientRect().left &&
dot.x < number.getBoundingClientRect().right
) {
number.classList.add('selected');
this.patternAttempt.push(parseInt(
number.innerText)
);
}
});
}
By adding a selected
class to each intersecting element, we can visually represent the selected number pads:

Figure 2.2: Marking the selected number pads
Step 4 – Creating a trail
With the help of the mergeMap
operator, we can assemble all swipe events and their coordinates, create a dot in the DOM representing the trail of user action, and, after a certain delay, remove the trail from the DOM. Additionally, a nice performance consideration might be grouping many swipe events into one buffer. We can do this by using bufferCount
, an operator that helps us to ensure optimal memory usage and computational efficiency:
private createTrailDot(
dotCoordinates: PixelCoordinates
): Observable<string[]> {
const dot = document.createElement('div');
dot.classList.add('trail-dot');
dot.style.left = `${dotCoordinates.x}px`;
dot.style.top = `${dotCoordinates.y}px`;
this.swipeArea.nativeElement.appendChild(dot);
return of('').pipe(
delay(1000),
bufferCount(100, 50),
finalize(() => dot.remove())
);
}
Now, in our browser’s Dev Tools, we can inspect the creation of the trail by looking at the DOM:

Figure 2.3: Swipe trail
Step 5 – Checking the result
Finally, at the end of the stream in the showMessage
method, we must check whether the patternAttempt
array, which was filled with each selected number pad, matches our pattern for unlocking the phone, which is 1 2 5 8 7.
Pattern matching
Since this is pattern matching and not exact password matching, the phone can be unlocked by inputting those buttons in any order, so long as those numbers in the pattern are included.
See also
- The
fromEvent
function: https://rxjs.dev/api/index/function/fromEvent - The
switchMap
operator: https://rxjs.dev/api/operators/switchMap - The
takeUntil
operator: https://rxjs.dev/api/operators/takeUntil - The
finalize
operator: https://rxjs.dev/api/operators/finalize - The
mergeMap
operator: https://rxjs.dev/api/operators/mergeMap - The
bufferCount
operator: https://rxjs.dev/api/operators/bufferCount