プロローグ
先日、Qiita で以下の記事を見ました。
自分は、CSS の background-image
で conic-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 属性で識別するようにしました。
<div class="outer_circle">
<div class="inner_circle"></div>
</div>
続けて、同じくコピペで、LongPressButton.razor.css
に CSS を記述します。なお、こちらも id セレクタを class セレクタに変更しました。
.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
を以下のように書き換えて実行してみます。
<LongPressButton />
すると無事、最初の表示が再現されました。
ロジック部分の実装
コンポーネントで公開するパラメーターの用意
続けて LongPressButton.razor
内に @code
コードブロックを設け、C# でロジック部分を実装します。
まず、長押しの秒数をコンポーネント外から指定できるよう、および、その秒数長押しし続けたら発生させるイベントコールバックを、この LongPressButton
コンポーネントのパラメーターとして用意します。長押しの秒数は、とくに指定がない場合は 3 秒を既定値としました。
...
@code
{
[Parameter]
public int Second { get; set; } = 3;
[Parameter]
public EventCallback OnLongPress { get; set; }
}
タイマーおよびタイマーイベントの発生回数を数えるカウンター変数を用意
続けて、長押しが開始されて以降の経過時間、というかタイマーイベントの発生回数を数えるカウンター変数と、定期的にイベントを発生させるためのタイマーを、フィールド変数に用意します。Blazor なので、JavaScript にある setInterval
や setTimeout
といった JavaScript API ではなく、.NET の System.Timers.Timer
クラスを使います。タイマーのインターバルは 10 ミリ秒とします。
...
@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 に再描画を指示するようにします。
...
@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
イベントの購読解除も行なっておきましょう。
@implements IDisposable
...
@code
{
...
public void Dispose()
{
_timer.Dispose();
_timer.Elapsed -= Timer_Elapsed;
}
}
ボタンが押されたとき/離されたときの処理
ボタンが押されたとき/離されたときの処理も実装していきます。それぞれ Start
および End
というメソッドで実装します。それぞれ、タイマーの開始/終了と、カウンター変数のリセットを実装します。また、これらメソッドを後ほど、表示要素の ponterdown/up イベントハンドラとして登録していきます。
...
@code {
...
private void Start()
{
_timer.Start();
}
private void End()
{
_timer.Stop();
_count = 0;
}
...
}
それでは、上記で実装した Start
および End
メソッドを、表示要素の pointerdown/up イベントハンドラとして登録します。これらメソッドは C# 側のメソッドなので、@
+ イベント名の属性で、HTML 上でマークアップします。
...
<div class="outer_circle"
@onpointerdown="Start"
@onpointerup="End">
...
</div>
...
長押しの経過時間を表示に反映
引き続きは、長押しの時間経過に伴って、CSS のグラデーション定義によって、進捗状況を表示していきますが、そのために、まずは CSS に仕込みをします。進捗状況のパーセント値を、CSS カスタムプロパティ (俗に言う CSS 変数) で指定するように LongPressButton.razor.css
内の定義を書き換えます。参照する CSS カスタムプロパティ名は、--long-press-progress
としてみました。
.outer_circle {
...
background-image: conic-gradient(#d5525f 0% var(--long-press-progress, 0%), #d9d9d9 0% 100%);
}
...
LongPressButton.razor
に戻り、タイマーイベント発生のたびにインクリメントするカウンター変数 (_counter
) と、長押しの秒数パラメーター (Second
) を使って、進捗状況のパーセンテージを計算し、それをメーター部分の要素のスタイル指定で CSS カスタムプロパティ --long-press-progress
に設定するようにします。
...
<div class="outer_circle"
...
style="@($"--long-press-progress: {_count/this.Second}%")">
...
</div>
...
これで、ボタンを押す/離すで、押している時間に従って、進捗状況がされるところまでできました。
長押し秒数が経過したらイベント発火
もう完成間近です。タイマーイベントのハンドラにて、Second
パラメーターに指定された秒数が経過したらタイマーを止めて OnLongPress
イベントコールバックを呼び出す処理を追加します。
...
@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 メッセージを表示するように実装し、動作を確認してみます。
@inject IJSRuntime JSRuntime
<div>
<LongPressButton OnLongPress="OnLongPress">
</LongPressButton>
</div>
@code
{
private async Task OnLongPress()
{
await this.JSRuntime.InvokeVoidAsync("alert", $"ボタンを長押ししました!");
}
}
無事動作しました!
仕上げ
最後に、元記事の「4. バグについて」の記述に倣って、「長押し中に右クリック」を阻止したり、「長押し中にマウスのポインタが外れる」といったケースで長押しを中断するよう、イベントハンドリングを追加します。oncontextmenu イベントの動作阻止だけは Blazor 側では対処でいないため (※ Blazor Server での動作を想定すると、DOM イベントの処理はどうしても非同期処理とならざるを得ず、非同期処理では、イベントの阻止は間に合わないため)、インラインで return false;
するという JavaScript 実装になっている点はご注意ください。
...
<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! :)