LoginSignup
17
20

More than 5 years have passed since last update.

XMLでまるいdeterminateなProgressBarを作る

Last updated at Posted at 2016-01-04

こんな感じのを作ります。
ProgressBar.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を指定した方がいいでしょう。

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

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"として、上方向から時計回りに描画されるように指定します。

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を呼び出しています。
DrawableLayerDrawableの場合はidandroid.R.id.progresssetLevelを呼び出しています。

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#addOvalPathを作成して、描画しています。

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

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;
}
17
20
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
17
20