IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    滚轮选择器扩展 - 角度选择器

    bt发表于 2023-07-24 11:13:00
    love 0

    上一篇实现了竖向的滚轮选择器,造轮子:滚轮选择器实现及原理解析(一)
    基于此,本篇我们实现一个横向的角度选择器,原理相同

    先看效果

    1e4ca10e-fdf5-4ee9-9599-b7504068b143.gif

    功能拆分

    1. 滚动方向调整为横向
    2. 需要显示刻度,且特定位置的刻度加粗加长,并显示对应数字
    3. 中心位置显示当前选中刻度的值,且会遮挡住刻度
    4. 刻度整体需要有渐变效果,越靠近两边透明度越低,越靠近中心透明度也越低

    实现步骤

    这里只考虑思路与实现逻辑,及原竖向逻辑的可扩展性,没有封装
    如果有感兴趣的可以基于我的代码二次封装

    调整方向

    这个很简单了,就是手动工作量的问题
    将所有涉及X,Y相关的命名及赋值的地方互换,width与height互换即可,完成后可以看到直接变成了横向滚动
    逻辑完全相同,只是涉及变量的处理不同

    刻度调整

    理论上指针指到任意刻度都可悬停并返回对应值,也就是说刻度是真实存在的数据,只是部分刻度不绘制对应的值而已
    接下来将itemWidth设置为5dp,并且单边可显示个数增大到50,得到以下的效果,注意需要替换原本绘制文本的逻辑为绘制方块

    // 在原本绘制文本的地方改为绘制宽6高8的方块
    canvas.drawRect(width /2f -3, height/2f - 4, width/2f +3, height/2f +4, linePaint);

    2023-07-24T07:07:23.png

    装点刻度

    对于特定刻度需要使其变得更长,且要有文字提示

    // Acitvity
    // 传入的数据每隔15设置一个字串进去,当然你也可以自行构建bean对象,使用一个boolean值控制是否是刻度点
    String[] data = new String[180];
    for (int i = 0; i < 180; i ++) {
        if (i % 15 ==0) {
            data[i] = i + "";
        } else {
            data[i] = "";
        }
    }
    // AnglePicker.drawItem()
    if (!TextUtils.isEmpty(text)) {
        // 存在文字判断为节点,刻度加长加粗
        canvas.drawRect(width /2f -4, height/2f - 10, width/2f +4, height/2f +10, linePaint);
    } else {
        canvas.drawRect(width /2f -3, height/2f - 4, width/2f +3, height/2f +4, linePaint);
    }
    // 按照原逻辑绘制文本,不过不再绘制在中点了,而是向上平移个50像素使其绘制在刻度上方
    canvas.translate(0, -50);
    canvas.drawText(text, width / 2f - textWidth / 2f, height / 2f - (metrics.top + metrics.bottom) / 2f, paint);

    2023-07-24T07:15:12.png

    当前值的绘制

    中间总有个数字显示当前的刻度,且刻度到中间的时候好像就渐变消失了,先绘制当前值+箭头

    protected void drawCenter(Canvas canvas, int centerPosition) {
        // 绘制三角指示器,这里是demo,感兴趣的同学可以换成任意drawable进行绘制
        path.reset();
        path.moveTo(width / 2f - 10, height / 2f - 80);
        path.lineTo(width / 2f + 10, height / 2f - 80);
        path.lineTo(width / 2f , height / 2f - 50);
        path.close();
        linePaint.setAlpha(255);
        canvas.drawPath(path, linePaint);
        Paint.FontMetrics metrics = paint.getFontMetrics();
        String text = "";
        text = adjustingPosition(centerPosition) + "";
        // 比正常文字大出1.5倍绘制
        paint.setTextSize(textSize * 1.5f);
        paint.setAlpha(255);
        float textWidth = paint.measureText(text);
        // 绘制在屏幕正中心
        canvas.drawText(text, width / 2f - textWidth / 2f, height / 2f - (metrics.top + metrics.bottom) / 2f, paint);
    }

    2023-07-24T07:18:54.png

    重叠处理

    这里中间的数字和刻度重叠了,需要简单处理下

    1. 重叠部分不绘制
      这个很简单,大文字在靠近的时候直接不绘制即可,我这里限制距离8个刻度以内不绘制

      
      // 文字靠近中间8格以内不显示,防止重叠
      if (Math.abs(position - centerPosition) > 8) {
       canvas.drawText(text, width / 2f - textWidth / 2f, height / 2f - (metrics.top + metrics.bottom) / 2f, paint);
      }
    2. 渐变效果
      刨除中间的4格区域内的刻度之外,4-10格刻度使用动态计算alpha实现渐变效果,大功告成
      有了竖向的基本逻辑加持,实现一个横向的角度指示器其实很简单,只需要改改UI即可

      if (Math.abs(position - centerPosition) < 10) {
       // 距离中间最近的10个渐变变化,最中间4个不显示,其余6个等比例alpha
       float lineAlphaPercent = Math.max(0,Math.abs(totalOffset) - (4 * itemWidth)) / (6f * itemWidth);
       linePaint.setAlpha((int) (255 * lineAlphaPercent));
      } else {
       linePaint.setAlpha((int) (255 * (minAlpha + (1 - minAlpha) * percent)));
      }

      2023-07-24T07:23:36.png

    源码

    String[] data = new String[180];
    
    for (int i = 0; i < 180; i ++) {
        if (i % 15 ==0) {
            data[i] = i + "";
        } else {
            data[i] = "";
        }
    }
    ((AnglePicker)findViewById(R.id.picker)).setData(data);
    
        <com.example.myapplication.picker.AnglePicker
            android:id="@+id/picker"
            app:pickerLoop="true"
            app:pickerShowCount="50"
            app:pickerAlphaMin="0.5"
            app:pickerScaleMin="1"
            app:pickerItemWidth="5dp"
            android:textSize="16sp"
            android:textColor="#ffffff"
            android:background="#000000"
            android:layout_width="match_parent"
            android:layout_height="150dp"/>
    
    public class AnglePicker extends View {
    
        /**
         * 3个状态,常规,惯性滚动,滚动
         */
        public static final int SCROLL_STATE_NORMAL = 0;
        public static final int SCROLL_STATE_FLING = 1;
        public static final int SCROLL_STATE_SCROLLING = 2;
        protected Context context;
        /**
         * 轮盘数据源
         */
        protected String[] data = new String[]{};
        protected Paint paint;
        /**
         * 当前滚动距离
         */
        protected float curX;
        /**
         * 每个item默认状态下的高度,item不在中心时该高度会被缩放
         */
        protected int itemWidth;
        /**
         * 单边最大显示个数(包含中心item)
         */
        protected int showCount;
        /**
         * 边缘item最小缩放值
         */
        protected float minScale;
        /**
         * 边缘item最小透明度
         */
        protected float minAlpha;
        /**
         * 是否循环显示
         */
        protected boolean isLoop;
        protected int textSize;
        protected int width;
        protected int height;
        protected float lastX;
        protected int scrollState;
    
        /**
         * 当前选中position
         */
        protected int curPosition;
        protected VelocityTracker velocityTracker = VelocityTracker.obtain();
        protected Scroller scroller;
        protected PickerChangeListener pickerChangeListener;
        private float startX;
        private long startTime;
        private Paint linePaint;
        private Path path;
        private final ValueAnimatorManager manager;
    
        public BasePicker(Context context) {
            this(context, null);
        }
    
        public BasePicker(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public BasePicker(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            this(context, attrs, defStyleAttr, 0);
        }
    
        public BasePicker(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            this.context = context;
    
            init(attrs, defStyleAttr, defStyleRes);
            manager = new PickerAnimManager((key, newValue) -> {
                if (TextUtils.equals(key, KEY_PICKER_ADSORB_ANIM)) {
                    curX = (float) newValue;
                    invalidate();
                } else if (TextUtils.equals(key, KEY_PICKER_SCROLL_ANIM)) {
                    curX = adjustingX((float) newValue);
                    invalidate();
                }
            });
        }
    
        protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            paint = new Paint(Paint.ANTI_ALIAS_FLAG);
            // 字重500,Medium效果
            paint.setFakeBoldText(true);
            scroller = new Scroller(context, new DecelerateInterpolator());
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Picker, defStyleAttr, defStyleRes);
            textSize = typedArray.getDimensionPixelOffset(R.styleable.Picker_android_textSize, 0);
            itemWidth = typedArray.getDimensionPixelOffset(R.styleable.Picker_pickerItemWidth, 0);
            minScale = typedArray.getFloat(R.styleable.Picker_pickerScaleMin, 0);
            minAlpha = typedArray.getFloat(R.styleable.Picker_pickerAlphaMin, 0);
            showCount = typedArray.getInt(R.styleable.Picker_pickerShowCount, 1);
            isLoop = typedArray.getBoolean(R.styleable.Picker_pickerLoop, true);
            paint.setColor(typedArray.getColor(R.styleable.Picker_android_textColor, Color.WHITE));
            typedArray.recycle();
            paint.setTextSize(textSize);
            linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            linePaint.setStyle(Paint.Style.FILL);
            linePaint.setColor(Color.WHITE);
            path = new Path();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            float x = curX;
            int centerPosition = getCenterShowPosition(x);
            float offsetX = adjustingX(x) - itemWidth * centerPosition;
            int max = centerPosition + showCount;
            // 处于正中心时使两侧显示相同个数item,非中心时下方增加1个
            if (offsetX > 0f) {
                max += 1;
            }
            for (int i = centerPosition - showCount + 1; i < max; i++) {
                drawItem(canvas, i, centerPosition, offsetX);
            }
            drawCenter(canvas, centerPosition);
    
        }
    
        /**
         * 获取中心点position,显示在正中心到中心上方itemWidth距离的item会被视为中心
         *
         * @param x 当前x滚动距离
         * @return 中心点的position
         */
        protected int getCenterShowPosition(float x) {
            float newX = adjustingX(x);
            return (int) (newX / itemWidth);
        }
    
        /**
         * @param x 滚动距离Y
         * @return 调整后的Y,其范围在0 ~ itemWidth * count 之间,方便计算
         */
        protected float adjustingX(float x) {
            float newX = x;
            if (isLoop) {
                while (newX < 0 || newX > getTotalX()) {
                    if (newX < 0) {
                        newX += getTotalX();
                    } else {
                        newX -= getTotalX();
                    }
                }
            } else {
                // 非循环时不可滚动超出边界
                newX = Math.min((data.length - 1) * itemWidth, Math.max(0, newX));
            }
    
            return newX;
        }
    
        /**
         * 调整下标,防止越界
         *
         * @param position 下标
         * @return 调整后的下标
         */
        protected int adjustingPosition(int position) {
            int newPosition = position;
            while (newPosition < 0 || newPosition > data.length - 1) {
                if (newPosition < 0) {
                    newPosition += data.length;
                } else {
                    newPosition -= data.length;
                }
            }
            return newPosition;
        }
    
        /**
         * 获取理论上的X整体高度偏移,作为计算参考。
         *
         * @return 整体X偏移量
         */
        protected float getTotalX() {
            return data.length * itemWidth;
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            width = right - left;
            height = bottom - top;
        }
    
        /**
         * 绘制每个item
         *
         * @param canvas         画布
         * @param position       当前position,可能为负数
         * @param centerPosition 中心item的position
         * @param offsetX        中心item的偏移值
         */
        protected void drawItem(Canvas canvas, int position, int centerPosition, float offsetX) {
            paint.setTextSize(textSize);
            // 不循环显示时忽略超出边界的部分
            if (!isLoop && (position < 0 || position >= data.length)) {
                return;
            }
            String text = data[adjustingPosition(position)];
            if (text == null) {
                Log.d("aaa", "aaa");
            }
            float textWidth = paint.measureText(text);
            int count = centerPosition - position;
            float totalOffset = offsetX + count * itemWidth;
            float percent = 1f - Math.abs(totalOffset) / (itemWidth * showCount);
            canvas.save();
            // 先缩放后位移,可以使不同item的间距不同,距离中心越近间距越大
            canvas.scale(minScale + (1 - minScale) * percent, minScale + (1 - minScale) * percent, width / 2f, height / 2f);
            canvas.translate(-totalOffset, 0);
    
    
            if (Math.abs(position - centerPosition) < 10) {
                // 距离中间最近的10个渐变变化,最中间4个不显示,其余6个等比例alpha
                float lineAlphaPercent = Math.max(0,Math.abs(totalOffset) - (4 * itemWidth)) / (6f * itemWidth);
                linePaint.setAlpha((int) (255 * lineAlphaPercent));
            } else {
                linePaint.setAlpha((int) (255 * (minAlpha + (1 - minAlpha) * percent)));
            }
            if (!TextUtils.isEmpty(text)) {
                // 存在文字判断为节点,刻度加长加粗
                canvas.drawRect(width /2f -4, height/2f - 10, width/2f +4, height/2f +10, linePaint);
            } else {
                canvas.drawRect(width /2f -3, height/2f - 4, width/2f +3, height/2f +4, linePaint);
            }
            paint.setAlpha((int) (255 * (minAlpha + (1 - minAlpha) * percent)));
            Paint.FontMetrics metrics = paint.getFontMetrics();
            canvas.translate(0, -50);
            // 文字靠近中间8格以内不显示,防止重叠
            if (Math.abs(position - centerPosition) > 8) {
                canvas.drawText(text, width / 2f - textWidth / 2f, height / 2f - (metrics.top + metrics.bottom) / 2f, paint);
            }
            canvas.restore();
        }
    
        protected void drawCenter(Canvas canvas, int centerPosition) {
            // 绘制三角指示器
            path.reset();
            path.moveTo(width / 2f - 10, height / 2f - 80);
            path.lineTo(width / 2f + 10, height / 2f - 80);
            path.lineTo(width / 2f , height / 2f - 50);
            path.close();
            linePaint.setAlpha(255);
            canvas.drawPath(path, linePaint);
            Paint.FontMetrics metrics = paint.getFontMetrics();
            String text = "";
            text = adjustingPosition(centerPosition) + "";
            paint.setTextSize(textSize * 1.5f);
            paint.setAlpha(255);
            float textWidth = paint.measureText(text);
            canvas.drawText(text, width / 2f - textWidth / 2f, height / 2f - (metrics.top + metrics.bottom) / 2f, paint);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (!isEnabled()) {
                return false;
            }
            // 触摸时取消滚动和吸附
            stopScroll();
            stopAdsorbAnim();
            // 速度监听
            velocityTracker.addMovement(event);
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    // 重置状态
                    scrollState = SCROLL_STATE_NORMAL;
                    lastX = event.getX();
                    startX = event.getX();
                    startTime = System.currentTimeMillis();
                    break;
                case MotionEvent.ACTION_MOVE:
                    curX -= event.getX() - lastX;
                    // 调整y使其不超出边界
                    curX = adjustingX(curX);
                    lastX = event.getX();
                    invalidate();
                    break;
                case MotionEvent.ACTION_UP:
                    if (Math.abs(startX - event.getX()) < 10 && System.currentTimeMillis() - startTime < 100) {
                        setCenterPosition(getNewPosition(curPosition, startX > width / 2f));
                    } else {
                        curX -= event.getX() - lastX;
                        // 调整y使其不超出边界
                        curX = adjustingX(curX);
                        lastX = event.getX();
                        // 计算速度,根据速度决定惯性滚动还是吸附
                        velocityTracker.computeCurrentVelocity(2000, 3000);
                        checkTouch((int) velocityTracker.getXVelocity(), (int) velocityTracker.getYVelocity());
                    }
                    break;
                default:
                    break;
            }
    
            return true;
        }
    
        private int getNewPosition(int position, boolean positionUp) {
            int newPosition = positionUp ? position + 1 : position - 1;
            if (isLoop) {
                return adjustingPosition(newPosition);
            }
            return Math.max(0, Math.min(data.length, newPosition));
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            getParent().requestDisallowInterceptTouchEvent(true);
            return super.dispatchTouchEvent(event);
        }
    
        /**
         * 开始惯性滚动
         *
         * @param xSpeed X轴速度,可为负数
         * @param ySpeed Y轴速度,可为负数
         */
        private void startFling(int xSpeed, int ySpeed) {
            scrollState = SCROLL_STATE_FLING;
            scroller.fling((int) lastX, 0, xSpeed, ySpeed, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            invalidate();
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            // 当开始吸附时舍弃计算值
            if (!manager.isAnimRunning(KEY_PICKER_ADSORB_ANIM) && scrollState == SCROLL_STATE_FLING && scroller.computeScrollOffset()) {
                // 速度低于一定值时执行吸附操作
                if (Math.abs(scroller.getCurrVelocity()) > 100) {
                    curX -= scroller.getCurrX() - lastX;
                    curX = adjustingX(curX);
                    lastX = scroller.getCurrX();
                    // 不循环时限制滚动到边界,且相同值时不多次触发回调
                    if (!isLoop) {
                        int newPosition = -1;
                        if (curX == 0) {
                            newPosition = 0;
                        } else if (curX == itemWidth * (data.length - 1)) {
                            newPosition = data.length - 1;
                        }
                        if (pickerChangeListener != null && newPosition >= 0 && curPosition != newPosition) {
                            curPosition = newPosition;
                            pickerChangeListener.onPickerSelectedChanged(this, curPosition, data[curPosition], true);
                        }
                    }
                    invalidate();
                } else {
                    startAdsorbAnim();
                }
            }
        }
    
        /**
         * 开始吸附动画
         */
        public void startAdsorbAnim() {
            float x = curX;
            int centerPosition = getCenterShowPosition(x);
            float offsetX = adjustingX(x) - itemWidth * centerPosition;
            int newPosition;
            // 超出一半时吸附到下一个item
            if (offsetX >= itemWidth / 2f) {
                manager.playAnim(KEY_PICKER_ADSORB_ANIM, curX, curX + itemWidth - offsetX);
                newPosition = adjustingPosition(centerPosition + 1);
            } else {
                manager.playAnim(KEY_PICKER_ADSORB_ANIM, curX, curX - offsetX);
                newPosition = adjustingPosition(centerPosition);
            }
            if ( curPosition != newPosition) {
                curPosition = newPosition;
            }
            if (pickerChangeListener != null) {
                pickerChangeListener.onPickerSelectedChanged(this, curPosition, data[curPosition], true);
            }
        }
    
        /**
         * 停止吸附
         */
        public void stopAdsorbAnim() {
            manager.stopAnim(KEY_PICKER_ADSORB_ANIM);
        }
    
    
        /**
         * 开始滚动动画,用于手动指定中心position时使用
         *
         * @param endX 滚动的结束值
         */
        protected void startScroll(float endX) {
            float x = adjustingX(curX);
            scrollState = SCROLL_STATE_SCROLLING;
            int min = 0;
            if (isLoop) {
                // 循环时就近选择最近的滚动方式,上下各延长getTotalX距离
                int normal = (int) (endX - x);
                int less = (int) (endX - x - getTotalX());
                int more = (int) (endX - x + getTotalX());
                if (Math.abs(normal) < Math.abs(less)) {
                    min = normal;
                } else {
                    min = less;
                }
                if (Math.abs(more) < Math.abs(min)) {
                    min = more;
                }
            } else {
                min = (int) (endX - x);
            }
            manager.playAnim(KEY_PICKER_SCROLL_ANIM, x, min + x);
        }
    
        /**
         * 停止滚动
         */
        private void stopScroll() {
            manager.stopAnim(KEY_PICKER_SCROLL_ANIM);
        }
    
        /**
         * 根据当前速度决定是否要进行惯性滚动
         *
         * @param speedX X轴速度
         * @param speedY Y轴速度
         */
        protected void checkTouch(int speedX, int speedY) {
            if (Math.abs(speedX) > 100) {
                startFling(speedX, speedY);
            } else {
                startAdsorbAnim();
            }
        }
    
        /**
         * 设置中心显示的item的下标
         *
         * @param position 目标position
         */
        public void setCenterPosition(int position) {
            setCenterPosition(position, true);
        }
    
        /**
         * 设置中心显示的item的下标
         *
         * @param position 目标position
         * @param smooth   是否播放动画
         */
        public void setCenterPosition(int position, boolean smooth) {
            stopAdsorbAnim();
            stopScroll();
            scrollState = SCROLL_STATE_NORMAL;
            if (!smooth) {
                curX = itemWidth * position;
                invalidate();
            } else {
                startScroll(itemWidth * position);
            }
            curPosition = position;
            if (pickerChangeListener != null) {
                pickerChangeListener.onPickerSelectedChanged(this, curPosition, data[curPosition], false);
            }
        }
    
        /**
         * 重新设置数据
         *
         * @param data 新数据
         */
        public void setData(String[] data) {
            this.data = data;
            invalidate();
        }
    
        /**
         * 设置行高
         *
         * @param itemWidth 行高
         */
        public void setitemWidth(int itemWidth) {
            this.itemWidth = itemWidth;
            invalidate();
        }
    
        /**
         * 设置单边显示个数(包含中心item)
         *
         * @param showCount 显示个数
         */
        public void setShowCount(int showCount) {
            this.showCount = showCount;
            invalidate();
    
        }
    
        /**
         * 设置边缘最小缩放
         *
         * @param minScale 最小缩放
         */
        public void setMinScale(float minScale) {
            this.minScale = minScale;
            invalidate();
    
        }
    
        /**
         * 设置边缘最小透明度
         *
         * @param minAlpha 最小透明度
         */
        public void setMinAlpha(float minAlpha) {
            this.minAlpha = minAlpha;
            invalidate();
    
        }
    
        /**
         * 设置文字颜色id
         *
         * @param textColorId 文字颜色id
         */
        public void setTextColorId(int textColorId) {
            if (textColorId > 0) {
                paint.setColor(ContextCompat.getColor(context,textColorId));
            }
            invalidate();
        }
    
        /**
         * 设置中心文字颜色
         *
         * @param color 颜色值int
         */
        public void setTextColor(int color) {
            paint.setColor(color);
            invalidate();
        }
    
        /**
         * 设置中心文字大小
         *
         * @param textSize 文字大小
         */
        public void setTextSize(int textSize) {
            this.textSize = textSize;
            invalidate();
        }
    
    
    
    
        public void setPickerChangeListener(PickerChangeListener pickerChangeListener) {
            this.pickerChangeListener = pickerChangeListener;
        }
    
        /**
         * 获取当前选中item在数组里的下标
         *
         * @return 当前选中下标
         */
        public int getCurPosition() {
            return curPosition;
        }
    }
    


沪ICP备19023445号-2号
友情链接