自定义view实现一个游标效果

本文介绍了一种自定义Android View实现的时间游标效果,支持手势滑动、颜色渐变及滑动冲突处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近公司的项目中需要一个游标效果控件,可以滑动游标选择相应的时间域。设计效果图如下:

 

于是乎,决定自己动手实现一下这个效果,也算是复习自定义view相关知识。

 

首先,自定义属性:attr.xml

    <declare-styleable name="CursorView">
        <attr name="current_bg_color" format="color|reference"></attr>
        <attr name="current_text_color" format="color|reference"></attr>
        <attr name="incurrent_text_color" format="color|reference"></attr>
        <attr name="textsize" format="dimension"></attr>
        <attr name="stroke_width" format="dimension"></attr>
        <attr name="stroke_color" format="color|reference"></attr>
    </declare-styleable>

自定义View类如下:

 

package com.baicells.omcserver.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;

import com.baicells.omcserver.R;

import java.util.ArrayList;

public class CursorView extends View {
    private int width;
    private int height;
    private int radius;
    private int strokeWidth;
    private int curTextColor;
    private int inCurTextColor;
    private ArrayList<String> cursorText;
    private Paint borderPaint;
    private Paint innerBgPaint;
    private Paint textPaint;
    private int current;
    private float offsetX;
    private RectF currentRectF;
    private boolean isInCurrent;


    public CursorView(Context context) {
        super(context);
    }

    public CursorView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        borderPaint = new Paint();
        borderPaint.setAntiAlias(true);
        borderPaint.setStyle(Paint.Style.FILL.STROKE);

        innerBgPaint = new Paint();
        innerBgPaint.setAntiAlias(true);
        innerBgPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CursorView);
        int indexCount = typedArray.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
            int index = typedArray.getIndex(i);
            switch (index) {
                case R.styleable.CursorView_current_bg_color:
                    innerBgPaint.setColor(typedArray.getColor(index, Color.GREEN));
                    break;
                case R.styleable.CursorView_current_text_color:
                    curTextColor = typedArray.getColor(index, Color.WHITE);
                    break;
                case R.styleable.CursorView_incurrent_text_color:
                    inCurTextColor = typedArray.getColor(index, Color.BLACK);
                    break;
                case R.styleable.CursorView_stroke_color:
                    borderPaint.setColor(typedArray.getColor(index, Color.RED));
                    break;
                case R.styleable.CursorView_textsize:
                    textPaint.setTextSize(typedArray.getDimensionPixelSize(index, 20));
                    break;
                case R.styleable.CursorView_stroke_width:
                    strokeWidth = typedArray.getDimensionPixelSize(index, 3);
                    break;
            }
        }
        typedArray.recycle();

        borderPaint.setStrokeWidth(strokeWidth);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    public CursorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setCursorText(ArrayList<String> cursorText) {
        this.cursorText = cursorText;
        invalidate();
    }

    public void setCurrent(int current) {
        this.current = current;
        invalidate();
    }

    public int getCurrent() {
        return current;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        width = 300;
        height = 20;
        if (modeWidth == MeasureSpec.EXACTLY) {
            width = sizeWidth;
        }
        if (modeHeight == MeasureSpec.EXACTLY) {
            height = sizeHeight;
        }
        setMeasuredDimension(width, height);
        radius = height / 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画整个大背景
        RectF rect = new RectF(strokeWidth / 2, strokeWidth / 2, width - strokeWidth / 2, height - strokeWidth / 2);
        canvas.drawRoundRect(rect, radius, radius, borderPaint);
        if (cursorText != null) {
            int size = cursorText.size();
            float everyWidth = width * 1.0f / size;
            //画当前选中时间维度的背景
            RectF currBgRectF = getCurrBgRectF(everyWidth);
            currentRectF = currBgRectF;
            canvas.drawRoundRect(currBgRectF, radius, radius, innerBgPaint);
            //循环画时间维度文本
            for (int i = 0; i < size; i++) {
                textPaint.setShader(null);

                String text = cursorText.get(i);
                Paint.FontMetricsInt metricsInt = textPaint.getFontMetricsInt();
                float textwidth = textPaint.measureText(text);
                float preblank = (everyWidth - textwidth) / 2.0f;
                float x = everyWidth * i + preblank;
                float y = (height - metricsInt.bottom + metricsInt.top) / 2.0f - metricsInt.top;
                if (currBgRectF.left >= x && currBgRectF.left <= x + textwidth) {
                    float split = (currBgRectF.left - x) / textwidth;
                    LinearGradient shader = new LinearGradient(x, 0, x + textwidth,
                            0, new int[]{inCurTextColor, curTextColor}, new float[]{
                            split, split + 0.01f}, Shader.TileMode.CLAMP);
                    textPaint.setShader(shader);
                }
                if (currBgRectF.right >= x && currBgRectF.right <= x + textwidth) {
                    float split = (currBgRectF.right - x) / textwidth;
                    LinearGradient shader = new LinearGradient(x, 0, x + textwidth,
                            0, new int[]{curTextColor, inCurTextColor}, new float[]{
                            split, split + 0.01f}, Shader.TileMode.CLAMP);
                    textPaint.setShader(shader);
                }

                if (currBgRectF.left < x && currBgRectF.right > x + textwidth) {
                    textPaint.setColor(curTextColor);
                } else {
                    textPaint.setColor(inCurTextColor);
                }
                canvas.drawText(text, x, y, textPaint);
            }
        }
    }

    private RectF getCurrBgRectF(float everyWidth) {
        float left = everyWidth * current + offsetX;
        float right = everyWidth * (current + 1) + offsetX;
        if (left < 0) {
            left = 0;
            right = everyWidth;
        }
        if (right > width) {
            left = width - everyWidth;
            right = width;
        }
        return new RectF(left, 0, right, height);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ViewParent parent = null;
        while (true) {
            if (parent != null && parent instanceof ViewPager) {
                break;
            }
            if (parent != null) {
                parent = parent.getParent();
            } else {
                parent = getParent();
            }
        }
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            default:
                parent.requestDisallowInterceptTouchEvent(false);
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    private float downX = 0;
    private float downY = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                isInCurrent = currentRectF.contains(downX, downY);
                break;
            case MotionEvent.ACTION_MOVE:
                if (isInCurrent) {
                    offsetX = x - downX;
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (isInCurrent) {
                    final NextCurrent nextCurrent = computeScrollX(currentRectF.left);
                    final float lastX = offsetX;
                    ValueAnimator anim = ValueAnimator.ofFloat(0, nextCurrent.getX());
                    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            float value = (float) animation.getAnimatedValue();
                            offsetX = lastX + value;
                            invalidate();
                            float fraction = animation.getAnimatedFraction();
                            if (fraction == 1.0f) {
                                current = nextCurrent.getIndex();
                                offsetX = 0;
                                invalidate();
                            }
                        }
                    });
                    anim.setDuration(100);
                    anim.start();
                }
                break;
        }
        return true;
    }

    private NextCurrent computeScrollX(float left) {
        NextCurrent next = new NextCurrent();
        int index = 0;
        if (cursorText != null) {
            int size = cursorText.size();
            float everyWidth = width * 1.0f / size;
            float min = everyWidth + 1;
            for (int i = 0; i < size; i++) {
                float abs = Math.abs(left - everyWidth * i);
                if (abs < min) {
                    min = abs;
                    index = i;
                }
            }
            if (everyWidth * index > left) {
                next.setX(min);
            } else {
                next.setX(-min);
            }
            next.setIndex(index);
        }
        return next;
    }

    private class NextCurrent {
        private int index;
        private float x;

        public int getIndex() {
            return index;
        }

        public void setIndex(int index) {
            this.index = index;
        }

        public float getX() {
            return x;
        }

        public void setX(float x) {
            this.x = x;
        }
    }
}

 

 

本控件支持的特性:

1、上述自定义View代码涉及到了手势滑动相关处理,当前选中的时间域背景跟随手指滑动进行移动,当手指抬起时,计算当前位置,并选择最近的时间域进行回弹(类似ViewPager中page页回弹效果)。

2、手指拖动当前时间域进行滑动过程中,对于不同时间域的文本内容,当内容与时间域背景有重叠时,支持颜色变化。

3、由于我的项目中自定义View是用在ViewPager下,所以有处理滑动冲突,当手指触摸位置为自定义View时请求父View不拦截事件,由当前自定义View处理该事件。

 

当前page页面对应的xml布局文件中的代码部分:

 

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.baicells.omcserver.view.CursorView
        android:id="@+id/cursor"
        android:layout_width="328dp"
        android:layout_height="28dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="16dp"
        app:current_bg_color="@android:color/holo_green_light"
        app:current_text_color="@android:color/white"
        app:incurrent_text_color="@android:color/black"
        app:stroke_color="@android:color/holo_green_light"
        app:stroke_width="2dp"
        app:textsize="14sp" />

 
</LinearLayout>


运行效果截图:

 

 

 

 

小视频:

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值