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"
にします。
<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"
として、上方向から時計回りに描画されるように指定します。
<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
を呼び出しています。
@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
の指定はソースを見るとわかりやすいです。
@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;
}