第一次写博客,看得不爽,请不要打死我
今天打开了Same,然后发现了投票频道这货,然后发现投票功能长得还行,于是就觉得来自定义来实现
下面的投票功能,same效果图如下:
实现方法有两种:
- 通过组合系统控件来实现,即通过多层嵌套来实现
- 通过自定义View来实现
方法的选择:
首先我们先来考察一下两种方法的优劣:
- 从实现难度上来说,组合的方法会相对比较简单,只要通过控件的组合便可以实现,只是比较麻烦,自定义View则相对比较复杂
- 从执行效率上来说,组合方法实现的法,在
layout
,measure
阶段会大量耗时,而自定义View则会相当快捷,只需要进行简单的绘制,在测量上并不需要耗费太大时间 - 从扩展来说,自定义View更加方便的进行修改和扩展
因此,综合考虑,我选择了自定义View的方法实现投票功能
实现
首先,我们要对投票功能进行分析,通过简单的瞄一眼,我们可以知道该View是有投票前和投票后两个状态
- 投票前,是只显示投票选项,投票选项在每一个选项中,居中显示,每个选项以1px的线进行分割
- 投票后,底部显示进度,以百分比进行显示,然后中间显示选项内容,右边显示进度的百分比,也就是投票权重,选中项的颜色和非选中项的颜色不同
分析完毕,现在开始实现了。。(为了简单,本例子中的属性赋值基本进行硬编码,大家可以可以通过在attr.xml
进行配置,从而实现在xml进行赋值的功能)
我们首先实现来看下定义的属性和measure方法
public class VoteView extends View {
//用于存储投票选项文字
private String[] items;
//用于存储投票选项对应的人数
private int[] nums;
//背景之类的画笔
private Paint mPaint;
//比例条颜色以及投票视图文字颜色
private int percentColor = Color.parseColor("#00bbff");
//分割线的颜色以及触摸反馈颜色
private int lineColor = Color.parseColor("#f6f6f6");
//view背景色
private int bgColor = Color.parseColor("#ffffff");
//投票项字体颜色
private int chooseTextColor=Color.parseColor("#555555");
//非投票项字体颜色
private int textColor=Color.parseColor("#777777");
//当前答案数量
private int size = 0;
//view的宽度,高度,每一个投票项的高度,每一项高度的一半
private int viewHeight, viewWidth, itemHeight, halfItemHeight;
//是否显示投票结果页面
private boolean isShowVoteResult = true;
private TextPaint mTextPaint;
//当前触摸的位置,用于触摸反馈
private float touchY;
//手指离开时的位置,用于判断点击的位置
private int clickPosition = -1;
//用于处理动画
private int currentOffset = 100;
//当前选择的投票项
private int votePosition=-1;
//用于实现动画
private ValueAnimator valueAnimator;
//右边比例数字最大宽度的一半
private int percentTextHalfWidth;
//用于存储位置对应的色值
private SparseArray mSparseArray=new SparseArray();
public VoteView(Context context) {
super(context);
init();
}
public VoteView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public VoteView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(1);
mTextPaint = new TextPaint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
//指定文字的大小为13.3sp
mTextPaint.setTextSize(getResources().getDimensionPixelSize(R.dimen.text_size_13_3));
//getResources().getDimension(R.dimen.padding_12)是为了和View右边的padding
percentTextHalfWidth= (int) (mTextPaint.measureText("100%") / 2+getResources().getDimension(R.dimen.padding_12));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (items != null) {
size = items.length;
}
//根据选项设置View的高度
setMeasuredDimension(this.getMeasuredWidth(), getResources().getDimensionPixelOffset(R.dimen.height_50) * size);
viewWidth = getMeasuredWidth();
viewHeight = getMeasuredHeight();
if (size > 0) {
itemHeight = (int) (viewHeight * 1.0f / size);
halfItemHeight = (int) (viewHeight * 1.0f / size / 2);
}
}
上面列出了用到的属性,大家目测一看就明白,看不明白你打我。。可以有两个地方需要注意一下,一个就是在onmeasure
里面,我是根据选项的个数来设置View的高度,所以在xml里面设置了高度的话并没有什么卵用,这里每一项的高度我是写死了50dp,halfItemHeight
主要是为了draw的时候减少计算的次数,提高效率。还有一个地方是percentTextHalfWidth
计算的是投票后进度百分比的最大宽度,meausreText()
方法可以计算出指定文字在当前画笔字体大小下的绘制宽度。
下面我们可以在onDraw()
来绘制未投票的状态了(这里为了方便代码的展示,就把两种状态都写在onDraw()
方法里面,正确的做法是把两种状态的绘制抽取出来):
protected void onDraw(Canvas canvas) {
mPaint.setAlpha(255);
mPaint.setColor(lineColor);
mTextPaint.setColor(percentColor);
for (int i = 0; i < size; i++) {
canvas.drawText(items[i], viewWidth / 2, halfItemHeight * (2 * i + 1) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
if (i != 0) {
canvas.drawLine(0, itemHeight * i, getMeasuredWidth(), itemHeight * i, mPaint);
}
}
}
这就很简单了,通过for循环drawLine()
方法绘制三条直线,然后drawText()
绘制选项对应的文本,mTextPaint.descent() + mTextPaint.ascent()) / 2
计算的是绘制文字的偏差,用于纠正文字无法在每一项item中居中的问题
效果图如下:
是不是很so easy,下面我们就来完成投票结果的页面了,代码如下:
@Override
protected void onDraw(Canvas canvas) {
//画背景色,可以不需要设置,这里是显示我的颜色是黑色的,就是为了好看一点而已
canvas.drawColor(bgColor);
//显示投票结果
if (isShowVoteResult) {
mPaint.setColor(percentColor);
for (int i = 0; i < size; i++) {
//设置字体的颜色,判断是否是选中的位置
if (i ==votePosition){
mPaint.setAlpha(255);
mTextPaint.setColor(chooseTextColor);
}else{
mTextPaint.setColor(textColor);
mPaint.setAlpha(mSparseArray.get(i) == null ? 255 : (Integer) mSparseArray.get(i));
}
//只有当比例大于0的时候,才进行绘制进度条
if(nums[i]>0){
canvas.drawRect(0, itemHeight * i, viewWidth *nums[i] / 100, itemHeight * (i + 1), mPaint);
}
canvas.drawText(items[i], viewWidth / 2, halfItemHeight * (2 * i + 1) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
canvas.drawText(nums[i] + "%", viewWidth -percentTextHalfWidth , halfItemHeight * (2 * i + 1) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
}
} else {
mPaint.setAlpha(255);
mPaint.setColor(lineColor);
mTextPaint.setColor(percentColor);
for (int i = 0; i < size; i++) {
canvas.drawText(items[i], viewWidth / 2, halfItemHeight * (2 * i + 1) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
if (i != 0) {
canvas.drawLine(0, itemHeight * i, getMeasuredWidth(), itemHeight * i, mPaint);
}
}
}
}
看到来代码好像有点多,但是只要我们一分析就发现,原来这么简单!!!但是只要我们一分析就发现,原来这么简单!!!但是只要我们一分析就发现,原来这么简单!!!重要事情说三遍。同样是通过for循环进行绘制,我们首先根据是否当前项是选中项,给画笔设置对应的颜色,mSparseArray
里面存储的是对应值的透明度,我这里的做法,是根据投票人数的多小,进度条的颜色透明度也相应的降低,大家可以随便设置颜色,可以固定的4种颜色,并不影响。然后就通过drawRect()
绘制进度条,drawText()
绘制选项文字和进度值,这里也就不解释了,有什么不太明白的可以提出来。。。
效果图如下
那我们是怎么让用户进行设置?
我们是通过暴露public void setVoteInfo(String[] items, int[] nums, boolean isShowVoteResult,int votePosition,boolean isAnimation,int themeColor)
方法来进行设置
/**
* 设置投票信息
* @param items 文字
* @param nums 选项对应的人数
* @param isShowVoteResult 是否显示投票结果视图
* @param votePosition 当前选中项
* @param isAnimation 当显示投票结果时,是否需要动画
* @param themeColor 进度条,投票选项字体颜色
*/
public void setVoteInfo(String[] items, int[] nums, boolean isShowVoteResult,int votePosition,boolean isAnimation,int themeColor) {
this.percentColor=themeColor;
//文字和分数是否数量对等
if (items.length == nums.length) {
this.mSparseArray.clear();
this.items = items;
this.nums=nums;
this.size=items.length;
this.isShowVoteResult = isShowVoteResult;
this.votePosition=votePosition;
//设置防止不能正确显示进度条,保证达到最大值
currentOffset = 100;
//reset点击项
clickPosition=-1;
if (isShowVoteResult) {
int max = 0;
//求出总值
int all = 0;
for (int i = 0; i < nums.length; i++) {
all += nums[i];
}
//要有人投票才计算对应百分比的颜色并设置最大值,否则最大值为0
if (all != 0) {
//求出百分比
for (int i = 0; i < nums.length; i++) {
nums[i] = nums[i] * 100 / all;
}
max = setPositionColor();
}
requestLayout();
}else {
requestLayout();
}
} else {
requestLayout();
}
}
}
/**
* 设置对应位置的颜色
* @return nums最大值
*/
private int setPositionColor(){
mSparseArray.clear();
if (nums==null||nums.length<1)
return 0;
int[] temps=Arrays.copyOf(nums,nums.length);
int[] checkPostionArr=Arrays.copyOf(nums,nums.length);
//简单的冒泡排序
quick_sort(temps);
//是否已经遍历了选中项
boolean isChooseMe=false;
for(int j=0;j<temps.length;j++){
for (int i=0;i<checkPostionArr.length;i++){
if (temps[j]==checkPostionArr[i]){
if (i==votePosition&&isChooseMe==false){
isChooseMe=true;
}else{
if (isChooseMe)
mSparseArray.put(i,155-(j-1)*50);
else
mSparseArray.put(i,155-j*50);
}
checkPostionArr[i]=-1;
break;
}
}
}
if (temps!=null&&temps.length>0)
return temps[0];
else
return 0;
}
这个也很简单,没什么好讲的,大家一看就知道,只是对传入的item
和nums
的长度进行判断是否一样,然后根据传前来的人数求出每个选项的百分比。setPositionColor()
方法是为了计算出mSparseArray
中存储的透明度,上面有讲,不需要的话,可以设置设置4种颜色或不设置,只用一种颜色就可以,觉得怎样好看就怎样弄。设置完成后调用requestLayout()
重新布局,计算View所需要的高度
然后界面都弄好,我们需要提供一个接口,让调用VoteView的地方,知道他选择了哪个答案,以及在投票结果页面,知道点击了哪个选项
public interface OnItemClickListener {
public void onClick(VoteView view, boolean isShowReult, int clickPosition);
}
然后在init()
方法里面设置监听回调
private void init() {
...省略
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//回调监听
if (mOnItemClickListener != null) {
mOnItemClickListener.onClick(VoteView.this, isShowVoteResult, (int) (touchY / itemHeight));
}
}
});
}
那我们怎么知道用户点击了哪一项?很简单,我们只需要重写onTouch
方法,知道用户手指离开的那一瞬间知道用户点击的Y值,然后根据每一投票项的高度就可以计算出用户点击了哪一项。clickPosition是为了产生触摸反馈,当用户没有投票时,手指按上去会变色
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取手指的坐标,触摸反馈
touchY = event.getY();
if (!isShowVoteResult) {
if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
clickPosition = -1;
} else {
clickPosition = (int) (touchY / itemHeight);
}
invalidate();
}
return super.onTouchEvent(event);
}
这样我们的效果就和Same的差不多了,就还剩下动画了。实现如下:
public void setVoteInfo(String[] items, int[] nums, boolean isShowVoteResult,int votePosition,boolean isAnimation,int themeColor) {
...省略
if (items.length == nums.length) {
...省略
if (isShowVoteResult) {
...省略
if (isAnimation) {
if (valueAnimator == null) {
//动画
valueAnimator = ValueAnimator.ofInt(0, 0).setDuration(200);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentOffset = (int) animation.getAnimatedValue();
invalidate();
}
});
}
valueAnimator.setIntValues(0, max);
valueAnimator.start();
requestLayout();
}else {
stopAnimation();
requestLayout();
}
} else {
stopAnimation();
requestLayout();
}
}
}
动画我通过ValueAnimator
属性动画来实现,根据选项的最大权重,从0开始增长。比如,我们现在三个选项的值权重分别是10,20,70,那我们的动画就从0-70开始变化,当动画的值小于当前权重的,就显示动画的值,当动画的值大于权重的时候就显示权重的值,比如现在动画进行到8,那三个进度条的占比分别是8,8,8。如果现在动画进行到25,那三个进度条的占比分别是10,20,25。文字也是同理,这时我们只需要在onDraw
的时候加上判断就可以
之前
protected void onDraw(Canvas canvas) {
//只有当比例大于0的时候,才进行绘制进度条
if(nums[i]>0){
canvas.drawRect(0, itemHeight * i, viewWidth *nums[i] / 100, itemHeight * (i + 1), mPaint);
}
canvas.drawText(items[i], viewWidth / 2, halfItemHeight * (2 * i + 1) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
canvas.drawText(nums[i]+ "%", viewWidth -percentTextHalfWidth , halfItemHeight * (2 * i + 1) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
}
现在
protected void onDraw(Canvas canvas) {
//只有当比例大于0的时候,才进行绘制进度条
if(nums[i]>0){
canvas.drawRect(0, itemHeight * i, viewWidth * (nums[i] > currentOffset ? currentOffset : nums[i]) / 100, itemHeight * (i + 1), mPaint);
}
canvas.drawText(items[i], viewWidth / 2, halfItemHeight * (2 * i + 1) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
canvas.drawText((nums[i] > currentOffset ? currentOffset : nums[i]) + "%", viewWidth -percentTextHalfWidth , halfItemHeight * (2 * i + 1) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
}
到这里,我们的投票自定义View就完成了,效果图如下(真机非常流畅,因为上传图片大小有限制,所以减少了帧数):