XMLでまるいdeterminateなProgressBarを作る

こんな感じのを作ります。
![ProgressBar.gif](https://qiita-image-store.s3.amazonaws.com/0/22848/1ba33b51-dc04-4678-cea0-f9154a2f8fc1.gif)


# DrawableのXML
`LayerDrawable`(layer-list)内に同一サイズのリング型`GradientDrawable`(shape)を
`@android:id/background`, `@android:id/secondaryProgress`, `@android:id/progress`として指定します。
`@android:id/background`, `@android:id/secondaryProgress`はなくてもかまいません。

ここでは、リング内側の円の半径を30dp、リングの厚さを2dpと指定しています。
いろいろな解像度の端末に合わせるならば、`dimens.xml`を使って指定するか、
`android:innerRadiusRatio`, `android:thicknessRatio`を指定した方がいいでしょう。

背景の`Drawable`は`ProgressBar`のプログレス値に関係なく表示するため、
`android:userLevel="false"`、その他は`android:userLevel="true"`にします。

```xml:drawable
<layer-list
  xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:id="@android:id/background">
    <shape
      android:shape="ring"
      android:innerRadius="30dp"
      android:thickness="2dp"
      android:useLevel="false">
      <solid
        android:color="#cccccc"/>
    </shape>
  </item>
  <item android:id="@android:id/secondaryProgress">
    <shape
      android:shape="ring"
      android:innerRadius="30dp"
      android:thickness="2dp"
      android:useLevel="true">
      <solid
        android:color="#80ff00"/>
    </shape>
  </item>
  <item android:id="@android:id/progress">
    <shape
      android:shape="ring"
      android:innerRadius="30dp"
      android:thickness="2dp"
      android:useLevel="true">
      <solid
        android:color="#0080ff"/>
    </shape>
  </item>
</layer-list>
```

# ProgressBarのXML
`ProgressBar`はスタイルで`minWidth`, `minHeight`, `maxWidth`, `maxHeight`が決まってしまうので、
下では`Drawable`のXMLを元に明示的に64dpと指定しています。

また、リング型`GradientDrawable`はレベル値に応じて右方向から時計回りに描画されるため、
`android:rotation="-90"`として、上方向から時計回りに描画されるように指定します。

```xml:layout
<ProgressBar
  android:id="@+id/progress"
  android:layout_width="64dp"
  android:layout_height="64dp"
  android:indeterminateOnly="false"
  android:max="100"
  android:rotation="-90"
  android:progressDrawable="@drawable/progress"/>
```

# おまけ：ProgressBar、GradientDrawableの実装
以上でまるいdeterminateな`ProgressBar`は表示できますが、少しAndroid(API level 23)のソースを見てみます。

`ProgressBar`では、プログレス値を[0, mMax]に補正後、
UIスレッドで`Drawable`のレベル値の範囲である[0, 10000]に再補正して`Drawable#setLevel`を呼び出しています。
`Drawable`が`LayerDrawable`の場合は`id`が`android.R.id.progress`の`setLevel`を呼び出しています。

```java:ProgressBar
@android.view.RemotableViewMethod
public synchronized void setProgress(int progress) {
  setProgress(progress, false);
}

@android.view.RemotableViewMethod
synchronized boolean setProgress(int progress, boolean fromUser) {
  if (mIndeterminate) {
    // Not applicable.
    return false;
  }

  progress = MathUtils.constrain(progress, 0, mMax);

  if (progress == mProgress) {
    // No change from current.
    return false;
  }

  mProgress = progress;
  refreshProgress(R.id.progress, mProgress, fromUser);
  return true;
}

private synchronized void refreshProgress(int id, int progress, boolean fromUser) {
  if (mUiThreadId == Thread.currentThread().getId()) {
    doRefreshProgress(id, progress, fromUser, true);
  } else {
    if (mRefreshProgressRunnable == null) {
      mRefreshProgressRunnable = new RefreshProgressRunnable();
    }

    final RefreshData rd = RefreshData.obtain(id, progress, fromUser);
    mRefreshData.add(rd);
    if (mAttached && !mRefreshIsPosted) {
      post(mRefreshProgressRunnable);
      mRefreshIsPosted = true;
    }
  }
}

private synchronized void doRefreshProgress(int id, int progress, boolean fromUser, boolean callBackToApp) {
  float scale = mMax > 0 ? (float) progress / (float) mMax : 0;
  final Drawable d = mCurrentDrawable;
  if (d != null) {
    Drawable progressDrawable = null;

    if (d instanceof LayerDrawable) {
      progressDrawable = ((LayerDrawable) d).findDrawableByLayerId(id);
      if (progressDrawable != null && canResolveLayoutDirection()) {
        progressDrawable.setLayoutDirection(getLayoutDirection());
      }
    }

    final int level = (int) (scale * MAX_LEVEL);  // MAX_LEVEL = 10000
    (progressDrawable != null ? progressDrawable : d).setLevel(level);
  } else {
    invalidate();
  }

  if (callBackToApp && id == R.id.progress) {
    onProgressRefresh(scale, fromUser, progress);
  }
}

```

`GradientDrawable`では、指定されたレベルから角度`360 * getLevel() / 10000.0f`を計算しています。
360度以外の場合は、`Path#arcTo`などで`Path`を作成し、
360度の場合は、`Path#addOval`で`Path`を作成して、描画しています。

`android:innerRadiusRatio`, `android:thicknessRatio`の指定はソースを見るとわかりやすいです。

```java:GradientDrawable
@Override
public void draw(Canvas canvas) {
  ...
  switch (st.mShape) {
    ...
    case RING:
      Path path = buildRing(st);
      canvas.drawPath(path, mFillPaint);
      if (haveStroke) {
        canvas.drawPath(path, mStrokePaint);
      }
      break;
  }
  ...
}

private Path buildRing(GradientState st) {
  if (mRingPath != null && (!st.mUseLevelForShape || !mPathIsDirty)) return mRingPath;
  mPathIsDirty = false;

  float sweep = st.mUseLevelForShape ? (360.0f * getLevel() / 10000.0f) : 360f;

  RectF bounds = new RectF(mRect);

  float x = bounds.width() / 2.0f;
  float y = bounds.height() / 2.0f;

  float thickness = st.mThickness != -1 ?
                st.mThickness : bounds.width() / st.mThicknessRatio;
  // inner radius
  float radius = st.mInnerRadius != -1 ?
                st.mInnerRadius : bounds.width() / st.mInnerRadiusRatio;

  RectF innerBounds = new RectF(bounds);
  innerBounds.inset(x - radius, y - radius);

  bounds = new RectF(innerBounds);
  bounds.inset(-thickness, -thickness);

  if (mRingPath == null) {
    mRingPath = new Path();
  } else {
    mRingPath.reset();
  }

  final Path ringPath = mRingPath;
  // arcTo treats the sweep angle mod 360, so check for that, since we
  // think 360 means draw the entire oval
  if (sweep < 360 && sweep > -360) {
    ringPath.setFillType(Path.FillType.EVEN_ODD);
    // inner top
    ringPath.moveTo(x + radius, y);
    // outer top
    ringPath.lineTo(x + radius + thickness, y);
    // outer arc
    ringPath.arcTo(bounds, 0.0f, sweep, false);
    // inner arc
    ringPath.arcTo(innerBounds, sweep, -sweep, false);
    ringPath.close();
  } else {
    // add the entire ovals
    ringPath.addOval(bounds, Path.Direction.CW);
    ringPath.addOval(innerBounds, Path.Direction.CCW);
  }

  return ringPath;
}
```