Android自定义View - 百分比扇形图
前两天舍友在网上看到了一个扇形图的自定义View,看到后自己也想借此机会试一下自定义View中Canvas的使用,便写了个简单的扇形图,效果图如下,具体细节还有些不是很完善:
GIF动图(以实际运行效果为准):
具体的实现方式就是canvas的绘制了,主要参考了 Carson_Ho 的系列:https://www.jianshu.com/p/762b490403c3 ,讲解非常全面。
大致思路:
-
自定义
View
,重写两个参数的构造函数public SectorProcessView(Context context, AttributeSet attrs)
,并在其中中初始化画笔Paint
等数据。例如:public SectorProcessView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setTextAlign(Paint.Align.CENTER); mPaint.setTextSize(mTextSize); }
-
在自定义
View
的onDraw(Canvas)
中编写具体的逻辑。例如绘制一个扇形drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
,参数的具体含义也不难理解,详细的可以参考上面提到的链接或者官方文档 https://developer.android.google.cn/reference/android/graphics/Canvas。关于这个方法有几点需要注意:
onDraw(Canvas)
方法会在创建该View
时调用,并且是在测量之后调用的,因此可以在该方法中获取到当前View
的宽和高。onDraw(Canvas)
方法会在invalidate()
方法调用后执行,因此可以通过调用invalidate()
方法实现View
的重绘,进而实现刷新或动画的效果。canvas
绘制时,绘制出来的对象按照次序叠放(即图层),因此无须使用saveLayer()
方法。
例:
protected void onDraw(Canvas canvas) { if (mWidth == 0) { mWidth = getWidth(); mHeight = getHeight(); } mPaint.setColor(mSectorColor); canvas.drawArc(0, 0, 100, 100, 0, mCurrentRate * 360, true, mPaint); }
-
在子线程中执行操作,需要更新时通过
post(Runnable action)
方法在主线程中更新数据并执行invalidate()
方法实现刷新。由于线程之间共享数据可能引发问题,因此最好在action
中而不是子线程中更新数据。
例:public void startAnimate() { new Thread(new Runnable() { @Override public void run() { int tick = 1; while (tick <= 1000) { post(new Runnable() { @Override public void run() { // update data invalidate(); } }); tick++; try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); }
-
然后就是具体细节的定义了,例如文字颜色,前景色背景色啊什么的。引用的时候可以直接在布局文件中通过包名+自定义View名使用,很是方便。
大致就是这些了,下面是代码,如果有问题,欢迎评论区讨论 ?。
以下是自定义的扇形图View的代码:
public class SectorProcessView extends View {
/**
* 默认动画持续时间为 1s
*/
public static final int DEFAULT_ANIMATION_DURATION = 1000;
/**
* 宽高
*/
private int mWidth;
private int mHeight;
/**
* 画笔
*/
private Paint mPaint;
/**
* 是否显示背景圆以及文字
*/
private boolean mShowBGOval;
private boolean mShowText;
/**
* 扇形矩形与背景色矩形
*/
private RectF mSectorRect;
private RectF mBGRect;
/**
* 扇形颜色,背景圆形颜色,文字颜色
* 默认:#77007FFF #007FFF #000000
*/
private int mSectorColor;
private int mBGColor;
private int mTextColor;
/**
* 文字大小,默认为 40
*/
private float mTextSize;
/**
* 绘制动画时当前比例变化的监听器
*/
private OnRateChangeListener mOnRateChangeListener;
/**
* 当前比例,绘制动画时使用
*/
private float mCurrentRate;
/**
* 是否正在绘制动画
*/
private boolean mIsAnimating;
public SectorProcessView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mShowBGOval = true;
mShowText = true;
mSectorRect = new RectF(0, 0, 0, 0);
mBGRect = new RectF(0, 0, 0, 0);
mSectorColor = Color.parseColor("#77007FFF");
mBGColor = Color.parseColor("#007FFF");
mTextColor = Color.BLACK;
mTextSize = 40;
mWidth = mHeight = 0;
mCurrentRate = (float) (2 / 3.0);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(mTextSize);
}
@Override
protected void onDraw(Canvas canvas) {
if (mWidth == 0) {
mWidth = getWidth();
mHeight = getHeight();
}
if (mShowBGOval) {
mBGRect.left = mWidth * (1 - mCurrentRate) / 2;
mBGRect.right = mWidth * (1 + mCurrentRate) / 2;
mBGRect.top = mHeight * (1 - mCurrentRate) / 2;
mBGRect.bottom = mHeight * (1 + mCurrentRate) / 2;
mPaint.setColor(mBGColor);
canvas.drawOval(mBGRect, mPaint);
}
if (mSectorRect.bottom == 0) {
mSectorRect.right = mWidth;
mSectorRect.bottom = mHeight;
}
mPaint.setColor(mSectorColor);
canvas.drawArc(mSectorRect, 0, mCurrentRate * 360, true, mPaint);
if (mShowText) {
mPaint.setColor(mTextColor);
canvas.drawText(String.format(Locale.CHINA, "%.0f%%", mCurrentRate * 100),
mWidth / 2, mHeight / 2 + mTextSize / 3, mPaint);
}
if (mOnRateChangeListener != null)
mOnRateChangeListener.onRateChange(this, mCurrentRate);
}
/**
* 开始绘制动画
*
* @param fromRate 起始比例,0~1之间
* @param toRate 结束比例,0~1之间
* @param animateDuration 时长
*/
public void startAnimate(final float fromRate, final float toRate, final int animateDuration) {
if (mIsAnimating)
return;
if (fromRate > 1 || toRate > 1)
return;
mIsAnimating = true;
mCurrentRate = fromRate;
new Thread(new Runnable() {
int tick = 1;
@Override
public void run() {
while (tick <= animateDuration) {
post(new Runnable() {
@Override
public void run() {
mCurrentRate += (toRate - fromRate) / animateDuration;
invalidate();
}
});
tick++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mIsAnimating = false;
}
}).start();
}
public void drawRate(float rate) {
if (mIsAnimating)
return;
mCurrentRate = rate;
invalidate();
}
public void setBGOvalColor(int bgColor) {
this.mBGColor = bgColor;
invalidate();
}
public void setSectorColor(int sectorColor) {
this.mSectorColor = sectorColor;
invalidate();
}
public void setTextColor(int textColor) {
this.mTextColor = textColor;
invalidate();
}
public void setShowBGOval(boolean showBGOval) {
this.mShowBGOval = showBGOval;
}
public void setShowText(boolean showText) {
this.mShowText = showText;
}
public void setOnRateChangeListener(OnRateChangeListener onRateChangeListener) {
this.mOnRateChangeListener = onRateChangeListener;
}
public void setTextSize(float textSize) {
this.mTextSize = textSize;
mPaint.setTextSize(mTextSize);
invalidate();
}
/**
* 绘制动画时比例变化监听器
*/
public interface OnRateChangeListener {
void onRateChange(View view, float rate);
}
}
下面是Demo的代码,主要是 seekBar
与自定义View的联动以及一个自制的简易颜色选择器,需要的话可以参考:
SectorUsageActivity
public class SectorUsageActivity extends AppCompatActivity {
private SectorProcessView spvView;
private SeekBar seekBar;
private int mTextColor;
private int mSectorColor;
private int mBGOvalColor;
private int mDuration;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sector_usage);
mTextColor = Color.BLACK;
mSectorColor = getResources().getColor(R.color.colorAzureHalfTran);
mBGOvalColor = getResources().getColor(R.color.colorAzure);
mDuration = DEFAULT_ANIMATION_DURATION;
//联动
seekBar = findViewById(R.id.sk_bar_sector_usage);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser)
spvView.drawRate((float) (progress / 100.0));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
//联动
spvView = findViewById(R.id.spv_sector_usage);
spvView.setOnRateChangeListener(new SectorProcessView.OnRateChangeListener() {
@Override
public void onRateChange(View view, float rate) {
seekBar.setProgress((int) (rate * 100));
}
});
//启动按钮
findViewById(R.id.btn_sector_usage_to_draw).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
spvView.startAnimate(0, (float) Math.random(), mDuration);
}
});
//文字颜色
final View pickTextColor = findViewById(R.id.view_sector_usage_pick_text_color);
pickTextColor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startColorPicker(pickTextColor, 0);
}
});
//扇形颜色
final View pickSectorColor = findViewById(R.id.view_sector_usage_pick_sector_color);
pickSectorColor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startColorPicker(pickSectorColor, 1);
}
});
//背景圆颜色
final View pickOvalColor = findViewById(R.id.view_sector_usage_pick_oval_color);
pickOvalColor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startColorPicker(pickOvalColor, 2);
}
});
//文字大小
SeekBar seekBarTextSize = findViewById(R.id.sk_bar_sector_text_size);
seekBarTextSize.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser)
spvView.setTextSize(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
//设置文字可见性
CheckBox checkBoxShowText = findViewById(R.id.checkbox_sector_usage_show_text);
checkBoxShowText.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
spvView.setShowText(isChecked);
}
});
//设置背景可见性
CheckBox checkBoxShowOval = findViewById(R.id.checkbox_sector_usage_show_oval);
checkBoxShowOval.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
spvView.setShowBGOval(isChecked);
}
});
//动画时长
SeekBar seekBarAnimateDura = findViewById(R.id.sk_bar_sector_duration);
seekBarAnimateDura.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser)
mDuration = progress * 10;
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
/**
* 自制颜色选择器
*
* @param showColorView 设置颜色的view
* @param whichToSet 指定设置的是什么颜色
* 0 文字
* 1 扇形
* 2 背景圆
*/
private void startColorPicker(final View showColorView, final int whichToSet) {
LinearLayout linearLayout = new LinearLayout(this);
View view = LayoutInflater.from(this).inflate(R.layout.ll_color_picker_dialog_view, linearLayout);
final SeekBar red = view.findViewById(R.id.sk_bar_dialog_view_red);
final SeekBar green = view.findViewById(R.id.sk_bar_dialog_view_green);
final SeekBar blue = view.findViewById(R.id.sk_bar_dialog_view_blue);
final SeekBar alpha = view.findViewById(R.id.sk_bar_dialog_view_alpha);
final View bgView = view.findViewById(R.id.view_dialog_view_bg);
switch (whichToSet) {
case 0:
alpha.setProgress((mTextColor >> 24) & 0xFF);
red.setProgress((mTextColor >> 16) & 0xFF);
green.setProgress((mTextColor >> 8) & 0xFF);
blue.setProgress(mTextColor & 0xFF);
bgView.setBackgroundColor(mTextColor);
break;
case 1:
alpha.setProgress((mSectorColor >> 24) & 0xFF);
red.setProgress((mSectorColor >> 16) & 0xFF);
green.setProgress((mSectorColor >> 8) & 0xFF);
blue.setProgress(mSectorColor & 0xFF);
bgView.setBackgroundColor(mSectorColor);
break;
case 2:
alpha.setProgress((mBGOvalColor >> 24) & 0xFF);
red.setProgress((mBGOvalColor >> 16) & 0xFF);
green.setProgress((mBGOvalColor >> 8) & 0xFF);
blue.setProgress(mBGOvalColor & 0xFF);
bgView.setBackgroundColor(mBGOvalColor);
break;
}
SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser)
bgView.setBackgroundColor(Color.argb(alpha.getProgress(), red.getProgress(), green.getProgress(), blue.getProgress()));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
};
red.setOnSeekBarChangeListener(seekBarChangeListener);
green.setOnSeekBarChangeListener(seekBarChangeListener);
blue.setOnSeekBarChangeListener(seekBarChangeListener);
alpha.setOnSeekBarChangeListener(seekBarChangeListener);
new AlertDialog.Builder(this)
.setView(linearLayout)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int c = Color.argb(alpha.getProgress(), red.getProgress(), green.getProgress(), blue.getProgress());
showColorView.setBackgroundColor(c);
switch (whichToSet) {
case 0:
mTextColor = c;
spvView.setTextColor(c);
break;
case 1:
mSectorColor = c;
spvView.setSectorColor(c);
break;
case 2:
mBGOvalColor = c;
spvView.setBGOvalColor(c);
break;
}
}
}).show();
}
布局文件 activity_sector_usage
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".activities.SectorUsageActivity">
<com.example.ericjeffrey.helloworld.views.SectorProcessView
android:id="@+id/spv_sector_usage"
android:layout_width="150dp"
android:layout_height="150dp" />
<Button
android:id="@+id/btn_sector_usage_to_draw"
android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="click to animate"
tools:ignore="HardcodedText" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Progress: "
android:textColor="@android:color/black"
tools:ignore="HardcodedText" />
<SeekBar
android:id="@+id/sk_bar_sector_usage"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:text="TextColor: "
tools:ignore="HardcodedText" />
<View
android:id="@+id/view_sector_usage_pick_text_color"
android:background="@android:color/black"
android:layout_width="40dp"
android:layout_height="match_parent"/>
<TextView
android:layout_marginStart="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:text="SectorColor: "
tools:ignore="HardcodedText" />
<View
android:id="@+id/view_sector_usage_pick_sector_color"
android:background="@color/colorAzureHalfTran"
android:layout_width="40dp"
android:layout_height="match_parent"/>
<TextView
android:layout_marginStart="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:text="OvalColor: "
tools:ignore="HardcodedText" />
<View
android:id="@+id/view_sector_usage_pick_oval_color"
android:background="@color/colorAzure"
android:layout_width="40dp"
android:layout_height="match_parent"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textAllCaps="false"
android:text="TextSize: "
tools:ignore="HardcodedText" />
<SeekBar
android:id="@+id/sk_bar_sector_text_size"
android:max="100"
android:progress="40"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center_vertical">
<CheckBox
android:id="@+id/checkbox_sector_usage_show_text"
android:text="show text"
android:checked="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="HardcodedText" />
<CheckBox
android:id="@+id/checkbox_sector_usage_show_oval"
android:text="show oval BG"
android:checked="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textAllCaps="false"
android:text="Duration: "
tools:ignore="HardcodedText" />
<SeekBar
android:id="@+id/sk_bar_sector_duration"
android:max="100"
android:progress="1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
颜色选择对话框布局文件: ll_color_picker_dialog_view
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="10dp">
<View
android:id="@+id/view_dialog_view_bg"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#007FFF"
android:elevation="5dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Alpha: "
android:textColor="#0000FF"
tools:ignore="HardcodedText" />
<SeekBar
android:id="@+id/sk_bar_dialog_view_alpha"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="255"
android:progress="255" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Red: "
android:textColor="#FF0000"
tools:ignore="HardcodedText" />
<SeekBar
android:id="@+id/sk_bar_dialog_view_red"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="255"
android:progress="0" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Green: "
android:textColor="#00FF00"
tools:ignore="HardcodedText" />
<SeekBar
android:id="@+id/sk_bar_dialog_view_green"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="255"
android:progress="127" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Blue: "
android:textColor="#0000FF"
tools:ignore="HardcodedText" />
<SeekBar
android:id="@+id/sk_bar_dialog_view_blue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="255"
android:progress="255" />
</LinearLayout>
</LinearLayout>