自定义View-Same投票功能

第一次写博客,看得不爽,请不要打死我

今天打开了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;
    }

这个也很简单,没什么好讲的,大家一看就知道,只是对传入的itemnums 的长度进行判断是否一样,然后根据传前来的人数求出每个选项的百分比。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就完成了,效果图如下(真机非常流畅,因为上传图片大小有限制,所以减少了帧数):
效果图

最后,感谢能看到这里的朋友
第一次写博客,有不足的欢迎指出
源码戳这里
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值