Twitterなどのように文字数に制限があるアプリケーションでは入力した文字数が制限におさまっているかどうかを視覚的にユーザが理解できるように円形のプログレスバーが設定されている場合があります。プラグインやライブラリもありますが自分の力で円のプログレスバーを作ってみましょう。こんな方法があるんだと新しい発見もあるかもしれません。

本文書で説明する円のプレズレスバー仕組みを理解することができればアニメーションを加えた円のプログレスバーも作成することができます。前半のCSSの話が中心で後半はvue.jsを利用していますが、vue.jsは利用する必要はありません。

今回目指した円のプログレスバーはTwitterで使われており、赤丸の部分の円の縁が入力した文字数によって変わります。

Twitterでの円のプログレスバー
Twitterでの円のプログレスバー

[commnt]Twitterではプログレスバーの実装をSVGを利用しています。この方法についても最後に説明しています。[/comment]

直線のプログレスバー

仕組みを理解してしまえば円のプログレスバーの作成は難しくはないのですがまずはプログレスバーがどのようなものか確認するために直線のプログレスバーを作成してみましょう。

文字列を入力するtextareaを作成してv-modeでモデルバインディングを行います。divタグにv-bindでstyle属性を設定し、computedプロパティを使って入力した文字列が文字制限のどれくらいの割合なのかをborderを利用したバーで表示します。

本文書では、文字数の制限はtwitterを意識して144文字としています。制限については何文字でも構いません。

デフォルトでデータプロパティのmessageにHelloを入れているのでHelloの文字数5/文字制限144文字の割合が赤いバーとして表示されます。ブラウザの横幅が100%です。

プログレスバーデフォルト
プログレスバーデフォルト

コードは下記の通りです。


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Circle Progress Bar</title>
  </head>
  <style>
  </style>
  <body>
    <div id="app">
      <textarea v-model="message"></textarea>
      <div :style="styles"></div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
    <script>
     new Vue({
        el: "#app",
        data() {
          return {
            message:"Hello"
          }
        },
        computed:{
          styles(){
            let width = this.message.length/144*100
            return {
              "border": "5px solid red",
              "width": width + '%'
            }
          },
        }
      })
    </script>
  </body>
</html>

文字を入力していくとプログレスバーが横に伸びていくことがわかります。文字数を減らすとプログレスバーは縮みます。144文字入れ終わるとブラウザの横幅いっぱいに広がります。

文字を入力するとプログレスバーが伸びる
文字を入力するとプログレスバーが伸びる

直接のプログレスバーは簡単に実現できることがわかりますが、いざ円のプログレスバーを作成してくださいと言われたら即作成できるでしょうか??私はできませんでした。下記で説明を行いますがこの仕組みを見つけた人はすごいなと正直思います。

円のプログレスバーの仕組みを理解

どのように円のプログレスバーを作成するのか図を使いながら仕組みを説明していきます。

今回は円の直径サイズを200pxとして進めていきます。

(1)最初に縦と横のサイズが200pxの領域を作成します。この領域を基準にしてこの中に円プログレスバーを作成するために必要な要素を入れていきます。複数の領域をこの基準領域の中に入れていくことになるのでpositionをrelativeとしていきます。これから追加する他の要素はすべてpositionをabsoluteとして設定していきます。

(2)基準領域の右側(right:0)から縦200pxと横100pxの2つ目の領域を作成します。わかりやすいように今は背景色をつけていますが最終的には背景色は付けません。

(3)3つ目の領域は2つ目の領域の内側に作成を行い、赤の領域の左端から100%の位置から縦200pxと横100pxの領域を作成します。わかりやすいように色をつけていますが、2つ目と3つ目の2つの領域の重複はありません。

プログレスバーの作成の流れ1
プログレスバーの作成の流れ1

<div style="position:relative;width:200px;height:200px;">
  <div style="position:absolute;width:100px;height:200px;right:0;background-color: red;">
    <div style="position:absolute;width:100px;height:200px;right:100%;background-color: blue;">
      <div style="position:absolute;width: 200px;height: 200px;border-radius: 50%;left:0;background-color: green"></div>
    </div>
  </div>
</div>

(4)直径200pxの円を3つ目の領域で追加したdivの内部に追加します。

(5)赤の領域にoverflow:hiddenを設定すると赤以外の青の領域は見えなくなります。この時に青の領域にもoverflow:hiddenを設定します。

(6)青と赤の背景色をなくします。背景色をなくすととoverflow:hiddenの効果により画面からは何も見えなくなります。しかし、実際にはこの下に緑色の円が存在します。

プログレスバーの作成の流れ2
プログレスバーの作成の流れ2

<div style="position:relative;width:200px;height:200px;">
  <div style="position:absolute;width:100px;height:200px;right:0;overflow: hidden;">
    <div style="position:absolute;width:100px;height:200px;right:100%;overflow:hidden;">
      <div style="position:absolute;width: 200px;height: 200px;border-radius: 50%;left:0;background-color: green"></div>
    </div>
  </div>
</div>

(7)先ほどまで青の背景色を持っていた領域を円の中心を軸に回転させます。これまでoverflow:hiddenで隠れていた円の一部が背景色をつけていた赤と青の領域が回転の結果重なることで重なり領域がoverflow:hiddenの領域ではなくなるので表示されます。

(8)さらに回転を進めると表示される領域が増えます。

(9)180度回転させると半円分が表示されます。

プログレスバーの作成の流れ3
プログレスバーの作成の流れ3

領域の回転にはcssのtransformとtransform-originを利用します。transform-originでは回転の軸となる場所を指定します。ここではtransform-origin: 100% 50%を設定するとちょうど軸は円の中心になります。transformでは回転のrotate(30deg)を設定し、30度回転することを指定しています。


<div style="position:relative;width:200px;height:200px;">
  <div style="position:absolute;width:100px;height:200px;right:0;overflow: hidden;">
    <div style="position:absolute;width:100px;height:200px;right:100%;overflow:hidden;transform-origin: 100% 50%;transform:rotate(30deg);">
      <div style="position:absolute;width: 200px;height: 200px;border-radius: 50%;left:0;background-color: green"></div>
    </div>
  </div>
</div>

実際にブラウザで確認すると下記のような表示となります。180度の半円までは期待した通りの動作になります。

ブラウザ上での表示
ブラウザ上での表示

180度以上回転させると背景色が赤と青だった領域の重なる部分が現象して下記のように丈夫から順番に円の領域がへ行っていきます。下記は210度回転させた場合の結果です。

210度回転するとうまく行かない
210度回転するとうまく行かない

180度までの半円までしかこれまでの設定ではうまく動作しないことがわかります。しかし半円までは期待した動作になることがわかったので、下記のようにもう一つの領域を使うことで実現します。右川の180度分の領域がここまでに説明してきた箇所で、左側の180度分の領域が追加した領域です。追加した領域の動作方法は先ほどの右側での設定とほぼ同じです。


<div style="position:relative;width:200px;height:200px;">
  <!-- 右側の180度分の領域 -->
  <div style="position:absolute;width:100px;height:200px;right:0;overflow: hidden;">
    <div style="position:absolute;width:100px;height:200px;right:100%;overflow:hidden;transform-origin: 100% 50%;transform:rotate(180deg);">
      <div style="position:absolute;width: 200px;height: 200px;border-radius: 50%;left:0;background-color: green"></div>
    </div>
  </div>
  <!-- 左側の180度分の領域 -->
  <div style="position:absolute;width:100px;height:200px;left:0;overflow: hidden;">
    <div style="position:absolute;width:100px;height:200px;left:100%;overflow: hidden;transform-origin: 0 50%;transform:rotate(30deg);">
      <div style="position:absolute;width: 200px;height: 200px;border-radius: 50%;right:0;background-color: green">
      </div>
    </div>
  </div>
</div>

下記のような210度の円を実現したい場合は2つの領域を足すことで実現しています。

2つの領域を利用して210度実現
2つの領域を利用して210度実現

右側の領域で180までの角度に対しての処理を行い、180度を超えた領域に関しては追加した領域で対応します。その結果2つの領域を足すことで360度に対応する円を作成することができます。

プログレスバーの作成の流れ
プログレスバーの作成の流れ

ここまでの仕組みが理解できたら後はvue.jsを利用して文字数によって2つの領域の角度を調整することができれば円のプログレスバーは完成します。

styleへの追加が多くなってしまいコードが読みにくくなったので共通のstyleについてはsquare, circleクラスを追加してコードを見やすくしました。


<style>
  .square{
    position:absolute;
    width:100px;
    height:200px;
    overflow:hidden;
  }
  .circle{
    position:absolute;
    width: 200px;
    height: 200px;
    border-radius: 50%;
    background-color: green;
  }
</style>
// 略
<div style="position:relative;width:200px;height:200px;">
  <!-- 右側の180度分の領域 -->
  <div class="square" style="right:0">
    <div class="square" style="right:100%;transform-origin: 100% 50%;transform:rotate(180deg);">
      <div class="circle" style="left:0;"></div>
    </div>
  </div>
  <!-- 左側の180度分の領域 -->
  <div class="square" style="left:0;">
    <div class="square" style="left:100%;transform-origin: 0 50%;transform:rotate(30deg);">
      <div class="circle" style="right:0;">
      </div>
    </div>
  </div>
</div>

vue.jsによるプログレスバーの制御

文字数による角度の計算

文字数の数によって角度を計算する必要があります。144文字の制限を想定しているので、144文字を入力するとプログレスバーは100%になります。computedプロパティを追加して、入力した文字数と角度の計算式を追加します。textLenghtはlengthメソッドで入力した文字数の数を出しています。

currentAngleではtextLengthの文字数を使って144の文字数のどれくらいの割合なのかを計算しています。


computed:{
  textLength(){
    return this.message.length;
  },
  currentAngle(){
    console.log(Math.floor(360*this.textLength/144));
    return Math.floor(360*this.textLength/144);
  },          
}

画面上で文字数と角度がわかるように表示させます。


<h2>円のプログレスバー</h2>
<p>文字数:{{ textLength }}</p>
<p>角度:{{ currentAngle }}</p>

下記ではtextareに36文字入力して角度が90度になることを表しています。36文字で90, 72文字で180, 144文字で360となることを確認します。正常に表示されれば次の処理に進んでください。

文字数と角度を計算
文字数と角度を計算

半円への角度の適用

文字数から角度の出す計算式がわかったので、最後に作成した半円への角度の反映方法を確認していきます。

右側の半円は0文字から72文字、つまり0度から180までを表すために利用します。そのため下記のように180までは文字数から計算される角度を適用し、72文字を超えると180度以上の数字にならないようにMath.minを利用します。


rightAngle(){
  let angle = Math.min(this.currentAngle, 180);
  return {
    "transform": "rotate(" + angle + "deg)",
  }
}
Math.minではカッコに入れた数字の中で一番小さな数字を戻す関数です。Math.maxはその逆でカッコに入った数字の中から一番大きな数字を戻します。

次は左側の半円への反映です。文字数が72、角度が180より小さい場合は何も変化がありません。文字数72, 角度が180を超えると適用されるようにMath.maxとMath.minを利用して設定を行います。


leftAngle(){
  let angle = Math.min(Math.max(this.currentAngle-180, 0),180);
  return {
    "transform": "rotate(" + angle + "deg)",
  }
},

追加したrightAngleとleftAngleのcomputedプロパティは下記のように各半円のタグに追加します。


<div style="position:relative;width:200px;height:200px;">
  <!-- 右側の180度分の領域 -->
  <div class="square" style="right:0">
    <div class="square" style="right:100%;transform-origin: 100% 50%;" :style="rightAngle">
      <div class="circle" style="left:0;"></div>
    </div>
  </div>
  <!-- 左側の180度分の領域 -->
  <div class="square" style="left:0;">
    <div class="square" style="left:100%;transform-origin: 0% 50%;" :style="leftAngle">
      <div class="circle" style="right:0;">
      </div>
    </div>
  </div>
</div>

108文字を入力すると角度は270度となり下記のように表示されます。文字数と連動する円のプログラスバーを作成することができました。文字を入力と削除と同時にリアルタイムで円の領域が変動します。

文字数と連動するプログレスバー
文字数と連動するプログレスバー

外側の円のボーダーのみ表示

ここまでの設定では円の内側まですべて円の背景色が設定されてしましたが、中央の背景色をなくし、外側のボーター部分のみ太さを持つように設定を行います。

cssクラスの.circleを下記のように更新します。ボーダーの色を変更したり、太さの変更も可能です。box-siziingも設定が必須です。


.circle{
  position:absolute;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  /*background-color: green;*/
  border:10px solid green;
  box-sizing: border-box;
}

textareaに文字を入力すると下記のように中央部分が中抜きになった円が表示されます。

円の中央部分の背景色はなし
円の中央部分の背景色はなし

円の中心に文字を表示

円の中心に文字数が表示されるように設定を行います。また文字数が144文字をオーバーしたら、オーバーした文字数を表示するように設定を行います。

新たに基準となるdivの中に下記のdiv要素を追加します。円の中央に文字を表示させるためにline-heightを200pxに設定し、text-alignで中央に表示するように設定を行っています。line-heightは円の大きさによって変更する必要があります。


<div style="position:absolute;width:200px;line-height: 200px;text-align: center;font-size:20px;font-weight: bold;">
  <span v-if="tweetLength <= 144"> {{ tweetLength}}</span>
  <span v-else> {{ 144 - tweetLength}}</span>
</div>

設定を反映させると円の中央に文字が表示されます。

円の中央に文字が表示
円の中央に文字が表示

文字が144を超えると中の数字は超えた数字が表示されます。

超えた文字数がマイナスで表示
超えた文字数がマイナスで表示

文字数に応じた円のプログレスバーの完成です。

このプログレスバーを基本としてアニメーションを追加することが可能ですが、その方法については別の機会の行わせてください。急ぎの方は@reffect2020のtwitterアカウントまで連絡してください。

Twitterでの実現方法

TwitterではSVGを利用しています。stroke-dashoffsetの値を文字数と連動して変更することでプログレスバーを実現することができます。


<div style="transform:rotate(-90deg)">
    <svg height="100%" viewBox="0 0 20 20" width="100%" style="overflow: visible;">
    <circle cx="50%" cy="50%" fill="none" stroke-width="2" r="9" stroke="#E6ECF0" />
    <circle
        cx="50%"
        cy="50%"
        fill="none"
        stroke-width="2"
        r="9"
        stroke="#1DA1F2"
        stroke-linecap="round"
        style="stroke-dashoffset: 19.5901; stroke-dasharray: 56.5487;"
    />
    </svg>
</div>
TwitterでのSVGを利用したプログレスバー
TwitterでのSVGを利用したプログレスバー