9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

BlazorAdvent Calendar 2023

Day 13

"ゲームなどでよく見る「長押しのボタン」のUIをWEBで表現してみた" を Blazor で実装してみた

Last updated at Posted at 2023-12-13

プロローグ

先日、Qiita で以下の記事を見ました。

自分は、CSS の background-imageconic-gradient という指定を使うと、このように円グラフ状の Progress 表示が実現できるとは知らず、それまで SVG を使った実装しか使ったことがなく、とても勉強になりました。

さて自分は、何か Web アプリケーションを作るとなると、いちばん好きなフレームワークは、C# で実装できる Blazor なので、この「長押しボタン」を Blazor のコンポーネントとして実装してみました。

Blazor プロジェクトを新規作成し、メーター部分の HTML & CSS を記述

まずは新規に Blazor WebAssembly プロジェクトを作成します (もちろん、Blazor Server や、Interactive Mode に None 以外を指定した Blazor Web App でも構いません)。

プロジェクトを作成したら、LongPressButton.razor ファイルと、LongPressButton.razor.css ファイルを新規作成します。

まずは LongPressButton.razor ファイルに、"ゲームなどでよく見る「長押しのボタン」のUIをWEBで表現してみた" の記事からのコピペで、以下のように HTML をマークアップします。これが "長押しボタン" の表示要素となります。なお、元記事では id 属性で各 <div> 要素を識別していましたが、今回は class 属性で識別するようにしました。

LongPressButton.razor
<div class="outer_circle">
    <div class="inner_circle"></div>
</div>

続けて、同じくコピペで、LongPressButton.razor.css に CSS を記述します。なお、こちらも id セレクタを class セレクタに変更しました。

LongPressButton.razor.css
.outer_circle {
    position: relative;
    margin-right: auto;
    margin-left: auto;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background-image: conic-gradient(#d5525f 0% 0%, #d9d9d9 0% 100%);
}

.inner_circle {
    text-align: center;
    background-color: #676767;
    width: 80px;
    height: 80px;
    border-radius: 50%;
    position: relative;
    top: 10px;
    left: 10px;
}

これで、"長押しボタン" を実装したコンポーネント、LongPressButton の最低限の見た目が実装できましたので、App.razor を以下のように書き換えて実行してみます。

App.razor
<LongPressButton />

すると無事、最初の表示が再現されました。

image.png

ロジック部分の実装

コンポーネントで公開するパラメーターの用意

続けて LongPressButton.razor 内に @code コードブロックを設け、C# でロジック部分を実装します。

まず、長押しの秒数をコンポーネント外から指定できるよう、および、その秒数長押しし続けたら発生させるイベントコールバックを、この LongPressButton コンポーネントのパラメーターとして用意します。長押しの秒数は、とくに指定がない場合は 3 秒を既定値としました。

LongPressButton.razor
...
@code
{
    [Parameter]
    public int Second { get; set; } = 3;

    [Parameter]
    public EventCallback OnLongPress { get; set; }
}

タイマーおよびタイマーイベントの発生回数を数えるカウンター変数を用意

続けて、長押しが開始されて以降の経過時間、というかタイマーイベントの発生回数を数えるカウンター変数と、定期的にイベントを発生させるためのタイマーを、フィールド変数に用意します。Blazor なので、JavaScript にある setIntervalsetTimeout といった JavaScript API ではなく、.NET の System.Timers.Timer クラスを使います。タイマーのインターバルは 10 ミリ秒とします。

LongPressButton.razor
...
@code
{
    ...
    private int _count = 0;

    private readonly System.Timers.Timer _timer = new(interval: 10);
}

タイマーイベント発生時にカウンターを加算する処理を実装

タイマーが用意できたので、タイマーのインターバルごとに発生する Elapsed イベント用のハンドラメソッドを実装して、そこでカウンター変数をインクリメントします。また、コンポーネント初期化のタイミングで呼び出される OnInitialized ライフサイクルメソッドで、タイマーの Elapsed イベントにこのイベントハンドラメソッドを購読追加します。

なお、タイマーの Elapsed イベント発生では、これは EventCallback<T> ではないことから、暗黙の StateHasChanged() は発生しません。すなわち、Elapsed イベントハンドラ内で何か状態変更 (ここではカウンター変数のインクリメント) が発生しても、表示に反映されません。なので、Elapsed イベントハンドラ内で明示的に StateHasChanged() メソッドを呼び出して、Blazor に再描画を指示するようにします。

LongPressButton.razor
...
@code
{
    ...
    override protected void OnInitialized()
    {
        _timer.Elapsed += Timer_Elapsed;
    }

    private async void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
    {
        _count++;
        StateHasChanged();
    }
}

さてこのままでは、この LongPressButton コンポーネントが破棄されるときに、タイマーが起動しっぱなしになるなどの恐れがあります。そこで、LongPressButton コンポーネントで IDisposable インターフェースを実装し、タイマーの破棄処理を実装します。念のため、Elapsed イベントの購読解除も行なっておきましょう。

LongPressButton.razor
@implements IDisposable
...
@code
{
    ...
    public void Dispose()
    {
        _timer.Dispose();
        _timer.Elapsed -= Timer_Elapsed;
    }
}

ボタンが押されたとき/離されたときの処理

ボタンが押されたとき/離されたときの処理も実装していきます。それぞれ Start および End というメソッドで実装します。それぞれ、タイマーの開始/終了と、カウンター変数のリセットを実装します。また、これらメソッドを後ほど、表示要素の ponterdown/up イベントハンドラとして登録していきます。

LongPressButton.razor
...
@code {
    ...
    private void Start()
    {
        _timer.Start();
    }

    private void End()
    {
        _timer.Stop();
        _count = 0;
    }
    ...
}

それでは、上記で実装した Start および End メソッドを、表示要素の pointerdown/up イベントハンドラとして登録します。これらメソッドは C# 側のメソッドなので、@ + イベント名の属性で、HTML 上でマークアップします。

LongPressButton.razor
...
<div class="outer_circle"
     @onpointerdown="Start"
     @onpointerup="End">
     ...
</div>
...

長押しの経過時間を表示に反映

引き続きは、長押しの時間経過に伴って、CSS のグラデーション定義によって、進捗状況を表示していきますが、そのために、まずは CSS に仕込みをします。進捗状況のパーセント値を、CSS カスタムプロパティ (俗に言う CSS 変数) で指定するように LongPressButton.razor.css 内の定義を書き換えます。参照する CSS カスタムプロパティ名は、--long-press-progress としてみました。

LongPressButton.razor.css
.outer_circle {
    ...
    background-image: conic-gradient(#d5525f 0% var(--long-press-progress, 0%), #d9d9d9 0% 100%);
}
...

LongPressButton.razor に戻り、タイマーイベント発生のたびにインクリメントするカウンター変数 (_counter) と、長押しの秒数パラメーター (Second) を使って、進捗状況のパーセンテージを計算し、それをメーター部分の要素のスタイル指定で CSS カスタムプロパティ --long-press-progress に設定するようにします。

LongPressButton.razor
...
<div class="outer_circle"
     ...
     style="@($"--long-press-progress: {_count/this.Second}%")">
     ...
</div>
...

これで、ボタンを押す/離すで、押している時間に従って、進捗状況がされるところまでできました。

長押し秒数が経過したらイベント発火

もう完成間近です。タイマーイベントのハンドラにて、Second パラメーターに指定された秒数が経過したらタイマーを止めて OnLongPress イベントコールバックを呼び出す処理を追加します。

LongPressButton.razor
...
@code {
    ...
    private async void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
    {
        _count++;
        StateHasChanged();

        if (_count / 100 == this.Second)
        {
            _timer.Stop();
            await OnLongPress.InvokeAsync();

            _count = 0;
            StateHasChanged();
        }
    }
    ...
}

App.razor で動作確認

これで基本形は完成です。App.razor 側で、この LongRessButton コンポーネントの OnLongPress イベントコールバックをハンドルして alert メッセージを表示するように実装し、動作を確認してみます。

App.razor
@inject IJSRuntime JSRuntime

<div>
    <LongPressButton OnLongPress="OnLongPress">
    </LongPressButton>
</div>

@code
{
    private async Task OnLongPress()
    {
        await this.JSRuntime.InvokeVoidAsync("alert", $"ボタンを長押ししました!");
    }
}

無事動作しました!

movie-000.gif

仕上げ

最後に、元記事の「4. バグについて」の記述に倣って、「長押し中に右クリック」を阻止したり、「長押し中にマウスのポインタが外れる」といったケースで長押しを中断するよう、イベントハンドリングを追加します。oncontextmenu イベントの動作阻止だけは Blazor 側では対処でいないため (※ Blazor Server での動作を想定すると、DOM イベントの処理はどうしても非同期処理とならざるを得ず、非同期処理では、イベントの阻止は間に合わないため)、インラインで return false; するという JavaScript 実装になっている点はご注意ください。

LongPressButton.razor
...
<div class="outer_circle"
     ...
     @onpointerout="End"
     @onpointercancel="End"
     @onpointerleave="End"
     oncontextmenu="return false;"
     ...>
    ...
</div>
...

おわりに

以上で元記事にて紹介されていた「長押しのボタン」を Blazor 上で Razor コンポーネントとして実装することができました。

元記事で紹介されている GitHub リポジトリでは、さらに、色やサイズもカスタマイズできるように実装が進んでいます。今回本記事で紹介した Blazor 版も、色やサイズを指定できるパラメーターを増設してさらに再利用しやすくできると思います。

さらには、この LongPressButton コンポーネントを独立した NuGet パッケージに切り出して、パッケージ参照するだけで他の様々な Blazor プロジェクトから再利用できるようにもできますね。コンポーネントを NuGet パッケージにする方法については、2019 年の Blazor Advent Calendar に投稿した下記の記事を参照ください。

Happy Coding! :)

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?