自定义View系列-滑动选择分数或者刻度

效果图

效果图


写在最前面

  • 详细源码及demo地址:GradeLayout源码
  • 导进你的工程中直接使用:compile 'jack.view:gradelayout:1.0'
  • 上传到github中的已进行过拓展,可以动态更改一些属性,详见github的README.md

实现前的分析

我们可以把整个布局分为两部分,一部分是上面的分数显示,一部分是下面的滑块显示。对于分数的显示我选择使用一个水平布局的LinearLayout,使用addView添加TextView的方式来实现;对于滑块,可以使用Button或者ImageView来实现,那根黑色的线可以使用drawLine来实现。整体来说,没什么难度,唯一要注意的地方是滑动冲突的解决。


实现步骤

1、编写属性文件attrs.xml代码

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="GradeLayout">
        <!--刻度被选中时候的颜色-->
        <attr name="grade_color_chosen" format="color" />
        <!--刻度未被选中时候的颜色-->
        <attr name="grade_color_unchosen" format="color" />
        <!--刻度被选中后刻度的图标-->
        <attr name="grade_ico_chosen" format="reference" />
        <!--刻度未被选中时刻度的图标-->
        <attr name="grade_ico_unchosen" format="reference" />
        <!--刻度图标的宽高-->
        <attr name="grade_ico_size" format="dimension" />
        <!--刻度文字大小-->
        <attr name="grade_text_size" format="dimension" />
        <!--导航线未被选中部分的颜色-->
        <attr name="nav_line_unchosen_color" format="color" />
        <!--导航线被选中部分的颜色-->
        <attr name="nav_line_chosen_color" format="color" />
        <!--导航button的背景图片-->
        <attr name="nav_button_ico" format="reference" />
        <!--导航button的宽高-->
        <attr name="nav_button_size" format="dimension" />
        <!--刻度的数量-->
        <attr name="max_grade" format="integer" />
        <!--刻度图标和导航线之间的距离-->
        <attr name="gap" format="dimension" />
        <!--刻度图标和刻度文字之间的距离-->
        <attr name="grade_ico_padding" format="dimension" />
        <!--导航线被选中部分的宽度-->
        <attr name="nav_line_chosen_width" format="dimension" />
        <!--导航线未被选中部分的宽度-->
        <attr name="nav_line_unchosen_width" format="dimension" />
    </declare-styleable>
</resources>

2、自定义view类GradeLayout的编码实现

首先,我们需要在构造函数里对布局属性进行加载,当然我们也为一些属性设置了默认值,当用户没有指定属性的具体值得时候,就直接采用默认值。这部分代码如下:

private void loadAttrs(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.GradeLayout);
        mMaxGrade = ta.getInt(R.styleable.GradeLayout_max_grade, DEFAULT_MAX_GRADE);
        mGradeChosenColor =
                ta.getColor(R.styleable.GradeLayout_grade_color_chosen, DEFAULT_CHOSEN_COLOR);
        mGradeUnchosenColor =
                ta.getColor(R.styleable.GradeLayout_grade_color_unchosen, DEFAULT_UNCHOSEN_COLOR);
        mGradeChosenIcoId = ta.getResourceId(
                R.styleable.GradeLayout_grade_ico_chosen, R.drawable.redpoint_icon);
        mGradeUnchosenIcoId = ta.getResourceId(
                R.styleable.GradeLayout_grade_ico_unchosen, R.drawable.graypoint_icon);
        mGradeIcoSize = ta.getDimension(
                R.styleable.GradeLayout_grade_ico_size, dip2px(context, DEFAULT_GRADE_ICO_SIZE));
        mNavLineChosenColor = ta.getColor(
                R.styleable.GradeLayout_nav_line_chosen_color, DEFAULT_CHOSEN_COLOR);
        mNavLineUnchosenColor = ta.getColor(
                R.styleable.GradeLayout_nav_line_unchosen_color, DEFAULT_UNCHOSEN_COLOR);
        mNavButtonIcoId = ta.getResourceId(
                R.styleable.GradeLayout_nav_button_ico, R.drawable.nav_button_icon);
        mNavButtonSize = ta.getDimension(
                R.styleable.GradeLayout_nav_button_size, dip2px(context, DEFAULT_NAV_BUTTON_SIZE));
        mGap = ta.getDimension(R.styleable.GradeLayout_gap, dip2px(context, DEFAULT_GAP));
        mGradeIcoPadding = ta.getDimension(R.styleable.GradeLayout_grade_ico_padding,
                dip2px(context, DEFAULT_GRADE_ICO_PADDING));
        mNavLineChosenWidth = ta.getDimension(R.styleable.GradeLayout_nav_line_chosen_width,
                dip2px(context, DEFAULT_NAV_LINE_CHOSEN_WIDTH));
        mNavLineUnchosenWidth = ta.getDimension(R.styleable.GradeLayout_nav_line_unchosen_width,
                dip2px(context, DEFAULT_NAV_LINE_UNCHOSEN_WIDTH));
        mGradeTextSize = ta.getDimension(R.styleable.GradeLayout_grade_text_size,
                dip2px(context, DEFAULT_GRADE_TEXT_SIZE));
        ta.recycle();
    }

接下来,到了构造分数布局和滑块布局的时候了,实现代码如下:

        //构建分数布局
        LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        LinearLayout gradeLy = new LinearLayout(context);
        gradeLy.setLayoutParams(params);
        gradeLy.setOrientation(HORIZONTAL);

        try {
            for (int i = 1; i <= mMaxGrade; i++) {
                CharSequence str = mGradeTexts.get(i - 1);
                TextView tv = buildTextView(context, str);
                mTextViews.add(tv);
                gradeLy.addView(tv);
            }
        } catch (NullPointerException e) {
            Log.e(TAG, "Maybe mGradeTexts.size() != mMaxGrade", e);
        }
        addView(gradeLy);

        //构建滑块布局
        mPullButton = new ImageView(context);
        mPullButton.setBackgroundDrawable(
                context.getResources().getDrawable(mNavButtonIcoId));
        mPullButtonParams
                = new LayoutParams((int) mNavButtonSize, (int) mNavButtonSize);
        mPullButtonParams.topMargin = (int) 
        addView(mPullButton, mPullButtonParams);
private TextView buildTextView(Context context, CharSequence grade) {
        TextView textView = new TextView(context);
        textView.setText(grade);
        textView.setTextSize(px2dip(context, mGradeTextSize));
        textView.setGravity(Gravity.CENTER_HORIZONTAL);
        textView.setTextColor(mGradeUnchosenColor);
        textView.setCompoundDrawables(null, null, null, mGradeUnchosenIco);
        textView.setCompoundDrawablePadding((int) mGradeIcoPadding);
        LayoutParams params =
                new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT);
        params.weight = 1;
        textView.setLayoutParams(params);
        return textView;
    }

最后需要画出下面的那根线,我们叫它导航线。要画出那根线,就必须要进行一些简单的计算,要保证导航线要和上面的文字对齐。另外需要注意的是,Android中的ViewGroup是默认不调用onDraw()方法的,因此我们需要在构造函数中调用setWillNotDraw(false)方法迫使ViewGroup调用onDraw(),否则我们没有办法在onDraw()里面进行划线。下面是详细代码。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mCanvas = canvas;
        TextView first = mTextViews.get(0);
        TextView last = mTextViews.get(mMaxGrade - 1);
        int startX = first.getLeft() + (first.getRight() - first.getLeft()) / 2;
        int startY = (mPullButton.getBottom() - mPullButton.getTop()) / 2 + mPullButton.getTop();
        int grayEndX = last.getRight() - (last.getRight() - last.getLeft()) / 2;
        int redEndX = mPullButton.getRight() - (mPullButton.getRight() - mPullButton.getLeft()) / 2;
        //绘制滑动线段
        drawLine(startX, startY, grayEndX, startY, mUnchosenPaint);
        drawLine(startX, startY, redEndX, startY, mChosenPaint);
        //绘制线段两端的圆弧
        canvas.drawCircle(startX, startY, mNavLineChosenWidth / 2, mChosenPaint);
        canvas.drawCircle(grayEndX, startY, mNavLineUnchosenWidth / 2, mUnchosenPaint);

        //一开始只是在构造函数里使用addView将滑块加入了整体布局中,
        //这样的话,滑块和最左边的分数是对不齐的,因此当走完onLayout方法后,
        //选择在onDraw方法里对滑块的布局重新优化,达到与最左边分数对齐的效果。
        //当然,实现的时候不一定非要选择在onDraw方法里实现。
        if (isFirstDraw) {
            isFirstDraw = false;
            mExtraLeftMargin = startX - mPullButton.getRight() / 2;
            mPullButtonParams.leftMargin = mExtraLeftMargin;
            mPullButton.requestLayout();

            MAX_LEFT_MARGIN = last.getLeft() + mExtraLeftMargin;
        }
    }

布局差不多都画完了,接下来要对滑块设置滑动监听,来处理滑动事件以及滑动冲突。构造函数中为滑块设置滑动监听:

mPullButton.setOnTouchListener(this);

onTouch里处理滑动事件:

    private int mLastX = 0;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        requestDisallowInterceptTouchEvent(true);//处理滑动冲突,让父控件把滑动事件交给自己处理。
        int x = (int) event.getRawX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                mPullButtonParams.leftMargin += deltaX;
                if (mPullButtonParams.leftMargin < mExtraLeftMargin) {//左边界的限定
                    mPullButtonParams.leftMargin = mExtraLeftMargin;
                }
                if (mPullButtonParams.leftMargin > MAX_LEFT_MARGIN) {//右边界的限定
                    mPullButtonParams.leftMargin = MAX_LEFT_MARGIN;
                }
                mPullButton.requestLayout();
                mLastX = x;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                int nowMargin = mPullButtonParams.leftMargin;
                //根据mPullButtonParams的leftMargin来计算出当前分数的index。
                int index = (nowMargin * (mMaxGrade - 1) * 2 + MAX_LEFT_MARGIN)
                        / (2 * MAX_LEFT_MARGIN) + 1;
                updateUI(index - 1);//对UI进行更新
                notifyGradeHasChanged(mTextViews.get(index - 1).getText().toString());//内部实现是通过接口通知当前选择的分数。
                break;
            default:
                break;
        }
        return true;
    }

当分数选择完成后我们通过接口来通知调用者:

    public interface OnGradeUpdateListener {
        void onGradeUpdate(GradeLayout view, String grade);
    }

在Activity里实现此接口,并且设置监听即可。


Demo

详细的demo,已上传到了github中,demo文件夹下就是。点我直达

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值