android自定义圆形日期选择轮盘

开新项目了,一款适用于女性用户的app。其中有一个页面含有一个时间选择器,设计师爸爸给的design是这样的:

 

图中左下角的文字部分先不管,轮盘部分为选择器,通过左右上下滑动来滚动,圆周上的点分别代表女性经期中的四个状态:经期、安全期、易孕期、排卵日。这四个状态分别使用不同颜色的小圆来表示,空心圆表示预测将来的经期,因此总共有5套样式。圆盘45度角的位置上面那颗稍微大一些的圆表示选中的日期,整个可见的区域,一共显示5颗小圆,然后下方和左边略微空出一些,大概就是这么个需求。

分析spec文件,小圆是均匀显示的,这里假定90度的空间里面,共显示5颗圆,下边延展15度不显示,左边延展30度不显示,那么大概的结构就是如下图所示(辅助线):

 

这下清晰多了。

那么现在需要完成哪些工作?

a.根据半径计算圆心的位置,测量整个View的尺寸;

b.画圆弧的小点(dash line),45度角指向当天的指针;

c.画代表每一天状态的圆点,这里需要根据不同的状态来确定画什么样式的圆;

d.为视图添加手势事件,以及回调接口;

大概就是这么个流程,现在按照以上四个步骤一步步来实现。

现在以圆心建立直角坐标系(见上图辅助线,不是canvas的坐标系),前边说过了,圆弧越过xy轴的部分中,左边延展30度,下边延展15度,那么在三角函数中,圆心的位置到视图左边缘(canvas坐标系中x轴的原点)的距离就是 |R* cos(90+30)|,圆心的位置到视图下边缘的距离就是|R*sin(-15)|,如此即可得到圆心位置,而整个视图的尺寸则为  半径+延展出来的距离+状态圆点的半径。

定义如下变量表示角度:

 

    final float ANGLE_90 = 90f;
    float angle = 135f;   //角度 // 左边30度,右边15度
    float leftAngle = 120f;//90+30
    float rightAngle = -15f;//-15
    int wheelRadius;//大轮半径
    int firstRoundRadius;
    int secondRoundRadius;
    float extraX, extraY;//从圆心开始,往左边和往下边偏移的距离
    int minWidth, minHeight;

 

 

 

根据大轮半径计算尺寸,这里最后没有用到context:

 

    void initSize(Context context) {
        //根据半径计算尺寸
        if (angle > 90) {
            extraX = (float) Math.abs(wheelRadius * Math.cos(Math.toRadians(leftAngle)));
            extraY = (float) Math.abs(wheelRadius * Math.sin(Math.toRadians(rightAngle)));
        } else {
            extraX = 0;
            extraY = 0;
        }
        //确定轮盘的圆心位置,这里如果夹角小于90度,则默认圆心在x轴方向上的值为 0
        wheelCentre.x = Math.round(extraX);
        wheelCentre.y = Math.round(wheelRadius + secondRoundRadius + strokeWidth);
        minWidth = Math.round(wheelCentre.x + wheelRadius + secondRoundRadius + strokeWidth);
        minHeight = Math.round(wheelCentre.y + extraY + secondRoundRadius + strokeWidth);
        float arcRadius = wheelRadius + secondRoundRadius;
        rectArc = new RectF(-arcRadius, -arcRadius, arcRadius, arcRadius);
        Log.e("TAG", "wheelCentre.x:" + wheelCentre.x + "   wheelCentre.y:" + wheelCentre.y);
    }

 

这里的minHeight,minWidth即为需要的最小尺寸,得到尺寸后,重写onMeasure方法:

 

 

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        
        if (widthMode != MeasureSpec.EXACTLY) {
            width = 0;
        }
        if (heightMode != MeasureSpec.EXACTLY) {
            height = 0;
        }
        width = Math.max(width, minWidth);
        height = Math.max(height, minHeight);
        setMeasuredDimension(width, height);
    }


这里在xml引用该View的时候,layout_width 和 layout_height 属性如果是wrap_content,则直接将尺寸设置为minWidth和minHeight,所以一般情况下,如果定义好了圆的半径,那就毋须再定义宽高属性。

 

 

现在开始绘制各个部分,这里极为重要的的一个技巧是要充分利用canvas的rotate方法,避免去计算每一个点、每一个小圆的位置,而是在初始位置上画好一个后,旋转一下画布,再接着画下一个,如此循环。

 

分步绘制,首先是dash line,这里要确定总共要画多少个小点,这样就可以得到在总共135度角的圆弧内,每一个小点均分的角度是多少:

 

    int dashNum = 60;
    float dashAngle = angle / dashNum;//每一个dash所分到的角度

 

因为dash line 向左边延展了30度的角,所以这里首先逆时针旋转画布相应的角度,然后在上图中圆弧最顶点的位置(90度的位置)顺时针旋转一个dash的角度,画小椭圆,有多少个小点就重复这一步骤多少次。因为要旋转画布,所以这里需要在操作的时候做好save和restore。

 

    void drawDash(Canvas canvas) {
        canvas.save();
        paint.setColor(dashColor);
        paint.setStyle(Paint.Style.FILL);
        float shiftAngle = (leftAngle - 90);
        canvas.rotate(-shiftAngle, wheelCentre.x, wheelCentre.y);
        for (int i = 0; i < dashNum; i++) {
            RectF rect = new RectF(wheelCentre.x - dashLength / 2, wheelCentre.y - wheelRadius - dashWidth / 2, wheelCentre.x + dashLength / 2, wheelCentre.y - wheelRadius + dashWidth / 2);
            canvas.rotate(dashAngle, wheelCentre.x, wheelCentre.y);
            canvas.drawOval(rect, paint);
        }
        canvas.restore();
    }


接下来绘制45度角位置的指针,同样的原理,直接在直角的位置将画布旋转45度角即可搞定:

 

 

    void drawIndicator(Canvas canvas) {
        canvas.save();
        paint.setColor(indicatorColor);
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.FILL);
        canvas.rotate(45, wheelCentre.x, wheelCentre.y);
        canvas.drawLine(wheelCentre.x, wheelCentre.y - wheelRadius + (firstRoundRadius + indicatorDash), wheelCentre.x, wheelCentre.y - wheelRadius + (firstRoundRadius + indicatorLength + indicatorDash), paint);
        canvas.drawLine(wheelCentre.x, wheelCentre.y - wheelRadius - (firstRoundRadius + indicatorDash), wheelCentre.x, wheelCentre.y - wheelRadius - (firstRoundRadius + indicatorLength + indicatorDash), paint);
        canvas.restore();
    }

 

然后最重要的地方来了,画代表每天状态的小圆,依照以上方法,在对应的位置画出5个圆很简单,但是这里需要根据日期以及每天的状态来确定这个圆是什么样式。通常的做法是使用styleable直接定义5套样式,这里的话我希望视图适用的范围可以更广阔一些,比如说可以定义更多的样式,所以没有使用这样的方式,而是定义了一个内部类来表示不同的样式,这样的话,想要多少个样式都可以。

 

    public static class RoundStyle {
        int bgColor;
        boolean ifStroke;

        public RoundStyle(int bgColor, boolean ifStroke) {
            this.bgColor = bgColor;
            this.ifStroke = ifStroke;
        }
    }
    public DiskView addStyle(int styleType, RoundStyle style) {
        styles.put(styleType, style);
        return this;
    }

 

 

 

ifStroke属性表示是否是空心圆。这里使用一个map来存储样式,其中key值用来表示该样式的类型。

 

    HashMap<Integer, RoundStyle> styles = new HashMap();

 

 

因为除去经期、易孕期排卵期之外的日期都是安全期,这里添加一个默认的样式来表示安全期,这样在定义状态的时候,便不用处理安全期。

 

    public void setSecurityStyle(RoundStyle securityStyle) {
        this.securityStyle = securityStyle;
    }


有了样式,还需要每一天的状态,才能知道每一天该使用哪个样式。这里为了方便,没有直接记录日期,而是使用了一个整形变量 distanceToday 来表示每一天距离今天的天数,今天是0,今天之前为负数,今天之后为正数。type 类型表示所对应的样式(与styles的key值对应)。所有的状态信息都保存在一个列表里边,为了提高后续的计算效率,这里需要对数据进行排序。

 

 

    public static class RoundState {
        public int type;
        public int distanceToday;//距离今天有多少天,今天之前是负数,今天之后是正数

        public RoundState(int type, int distanceToday) {
            this.type = type;
            this.distanceToday = distanceToday;
        }
    }
    public void setStateArray(ArrayList<RoundState> list) {
        roundList = list;
        /**
         * 逆序
         */
        Collections.sort(roundList, new Comparator<RoundState>() {
            @Override
            public int compare(DiskView.RoundState o1, DiskView.RoundState o2) {
                return o1.distanceToday < o2.distanceToday ? 1 : -1;
            }
        });
    }


接下来,根据样式和状态来绘制小圆。

 

这里定义一个int值 shiftValue,表示圆盘转动了多少天,shiftvalue为0 的时候,没有转动,表示今天,design中选中的当天是在45度角的位置(稍微大一些的那个圆),因此,shiftValue 对应着大圆的位置。当圆盘向右转动的时候,显示以往的日期,shiftValue递减;当圆盘向左转动的时候,显示以后的日期,shiftValue递增。

定义如下变量:

 

    int shiftValue = 0;
    float singleAngle = ANGLE_90 / (sumValues - 1);//一天分配的角度
    float curAngle;//当前已经滑动过的度数


这里需要确定画布到底要旋转多少度,前边已经说过了,每个圆点为一天,一天均分的角度为 singleAngle,那么 距离今天转过的角度就是 distanceToday*singleAngle。

 

假定已经转过了 curAngle 这么多的角度,那么,距离选中的那一天转过的角度就是  distanceToday*singleAngle-curAngle。因为选中的当天是位于45度的位置,因此,画布需要旋转的角度就是:

    float rotateAngle = 45 + distanceToday * singleAngle - curAngle;

理清这些问题之后即可绘制小圆:

 

 

    void drawRound(Canvas canvas) {
        /**
         * 可见界面共画5个点,没有事件的点画安全期点,这里过滤出列表中位于可见界面的日期状态
         */
        if (roundList != null && !roundList.isEmpty()) {
            RoundState curList[] = new RoundState[5];
            for (RoundState rs : roundList) {
                if (rs.distanceToday < shiftValue - 2) {
                    break;
                } else if (rs.distanceToday > shiftValue + 2) {
                    continue;
                }
                curList[rs.distanceToday - (shiftValue - 2)] = rs;
            }
            /**
             * 确定可见区域每一天的样式,如果列表中没有这一天的样式,则使用默认的 securityStyle
             */
            for (int i = 0; i < curList.length; i++) {
                int distanceToday, color;
                boolean ifStroke;
                if (curList[i] != null) {
                    distanceToday = curList[i].distanceToday;
                    color = styles.get(curList[i].type).bgColor;
                    ifStroke = styles.get(curList[i].type).ifStroke;
                } else if (securityStyle != null) {
                    distanceToday = shiftValue - 2 + i;
                    color = securityStyle.bgColor;
                    ifStroke = securityStyle.ifStroke;
                } else {
                    continue;
                }
                float rotateAngle = 45 + distanceToday * singleAngle - curAngle;
                canvas.save();
                paint.reset();
                if (ifStroke) {
                    paint.setStyle(Paint.Style.STROKE);
                    paint.setStrokeWidth(strokeWidth);
                } else {
                    paint.setStyle(Paint.Style.FILL);
                }
                paint.setColor(color);
                canvas.rotate(rotateAngle, wheelCentre.x, wheelCentre.y);
                float radius;
                if (distanceToday == shiftValue) {
                    radius = firstRoundRadius;
                } else {
                    radius = secondRoundRadius;
                }
                canvas.drawCircle(wheelCentre.x, wheelCentre.y - wheelRadius, radius, paint);
                canvas.restore();
                if (distanceToday == shiftValue) {
                    drawText(canvas);
                }
            }
        }
    }

 

design上面,如果选中的那一天刚好是今天,则显示“今天”,否则以“MM/dd”格式显示当天的日期,drawText方法如下:

 

 

    void drawText(Canvas canvas) {
        paint.reset();
        paint.setColor(textColor);
        paint.setTextSize(textSize);
        paint.setAlpha((int) (255 * textAlpha));
        String textCurday = valueAdapter.setStrOfSelectValue(shiftValue);
        float width = paint.measureText(textCurday);
        canvas.drawText(textCurday, wheelCentre.x + (float) Math.cos(Math.toRadians(45)) * wheelRadius - width / 2, wheelCentre.y - (float) (Math.sin(Math.toRadians(45))) * wheelRadius + textSize / 2, paint);
    }

 

 

这里需要根据shiftValue设置并且获得当天的日期,定义一个抽象内部类来规范接口:

 

    public static abstract class ValueAdapter<T> {
        private int mShiftValue;

        public int getShiftValue() {
            return mShiftValue;
        }

        public abstract T getSelectValue();

        private void setShiftValue(int shiftValue) {
            mShiftValue = shiftValue;
        }

        protected abstract String setStrOfSelectValue(int shiftValue);
    }

 

 

附带一个适用于本app的Sample类:

 

 

    public class SimpleDateAdapter extends ValueAdapter<Calendar> {
        Calendar calendarSelect;
        SimpleDateFormat sdf;
        String textToday;
        final static long ONE_DAY = 1000 * 3600 * 24;

        public SimpleDateAdapter(Context context) {
            calendarSelect = Calendar.getInstance();
            textToday = context.getString(R.string.discview_today);
            sdf = new SimpleDateFormat("MM/dd");
        }

        @Override
        public Calendar getSelectValue() {
            calendarSelect.setTimeInMillis(calendarNow.getTimeInMillis() + getShiftValue() * ONE_DAY);
            return calendarSelect;
        }

        @Override
        protected String setStrOfSelectValue(int shiftValue) {
            return shiftValue == 0 ? textToday : sdf.format(getSelectValue().getTime());
        }
    }

 

 

 

 

 

 

完成绘制之后,最后一步:定义手势事件。

手势事件要重写onTouchEvent方法,并且使用一个GestrureDetector来判断是怎么滑动的。

 

    ValueAdapter valueAdapter;
    GestureDetector mGestureDetector;
    SimpleOnGestureListener mGestureListener;
    OnValueChangedListener mListener;   
    int minShiftValue = -Integer.MIN_VALUE;//最多可以向今天以前这么多天
    int maxShiftValue = -Integer.MAX_VALUE;//最多可以向今天以后这么多天
    float scrollAngle;//本次滑动的次数
    float sumScrollAngle;//本次累积滑动的度数
    float curAngle;//当前已经滑动过的度数

 

重写onTouchEvent,手指离开屏幕的时候,根据 mGestureListener 的 onFling 方法来判断是否有拖动,如果有拖动,则根据拖动的幅度启动动画;如果没有拖动,结算最终位置:

 

 

 

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        mGestureDetector.onTouchEvent(event);
        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (haveFling) {
                int targetShiftValue = shiftValue + flingShiftValue * statusPN;                
                startAnim(targetShiftValue);
            } else {
                setFinalPosition();
            }
        }
        return true;
    }

 

 

 

 

GestureDetector的实现,最关键的地方是如何计算滑过的角度?这里通过onScroll的e1,e2两个事件的位置来计算单次判定滑过的度数,累加这个度数,就是一次手势事件最终滑过的度数,计算方法参照向量的夹角公式:

得到cos(a)的值后,即可通过Math函数来获取度数,因为Math.acos(a)函数得到的是0~PI,这里需要根据两次点击的位置的斜率来判定最终要将这个角度转换为正值还是负值,这里的判断原则是在x右y上的直角坐标系中,位于(PI/4)到(PI*5/4)的范围内为正值,否则为负值。

lastX,lastY其实就是onScroll方法中的第一个参数e1的getX,getY。

shiftValue的值就是 滑过的总度数/singleAngle,这里增加了一个最大最小判断,为了防止无限滑动,这里为shiftValue设置了一个最大最小值,超过这个值就不再响应。

为了让UI可以响应视图的滑动事件,这里为其添加了外部的监听接口:

 

    public interface OnValueChangedListener {
        void onValueChanged(ValueAdapter adapter);
    }

这个接口直接使用一个ValueAdapter作为参数。

 

手势监听,这里额外定义了两个变量来记录是否有拖动事件以及拖动的幅度,这里根据拖动的速度事先计算好了动画需要越过的天数。

 

    int flingShiftValue = 0;//设置fling转动的shiftValue
    boolean haveFling = false;//是否有fling事件
    int statusPN = 1;//记录滑动的时候是向左还是向右

 

 

 

 

 

其中,onSingleTapUp是非必须的,只是后来测试爸爸说这个玩意儿上面的小圆点不能点击很郁闷。我想都能滑动了为什么还要点击,好吧你这么变态我满足你就是。这里面的roundPosition是一个二维数组,事先计算好了五个圆点的位置,当点击的坐标落在这五个圆点内的时候,就会进行相应的跳转。

 

 

    SimpleOnGestureListener mGestureListener = new SimpleOnGestureListener() {
        int lastShiftValue;
        float lastX, lastY;

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.e("TAG", "MotionEvent_Action:" + e.getAction() + "   X:" + e.getX() + "   Y:" + e.getY());
            for (int i = 0; i < sumValues; i++) {
                if (i != 2 && e.getX() > roundPositions[i][0] && e.getX() < roundPositions[i][2] && e.getY() > roundPositions[i][1] && e.getY() < roundPositions[i][3]) {
                    setTargetShiftValue(shiftValue + (i - 2));
                    return true;
                }
            }
            return true;
        }

        @Override
        public boolean onDown(MotionEvent e) {
            if (animatorSet != null) {
                animatorSet.cancel();
            }
            lastShiftValue = shiftValue;
            sumScrollAngle = 0;
            haveFling = false;
            lastX = e.getX();
            lastY = e.getY();
            return super.onDown(e);
        }

        /**
         * 从x右y上的直角坐标系的原点出发,位于(PI/4)<a<(PI*5/4)之间的为1,否则为-1
         * @param x1
         * @param y1
         * @param x2
         * @param y2
         * @return
         */
        int checkPNValue(float x1, float y1, float x2, float y2) {
            //调整坐标系,参数的坐标系是参考手机上x右y下的坐标系,这里要转化为x右y上的坐标系
            //这里将 y2-y1 的值反向即可.
            if (-(y2 - y1) > x2 - x1) {
                return 1;
            } else {
                return -1;
            }
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            /**
             * 参照向量的夹角公式
             * 正负值的问题怎么处理
             */
//            float cosa = ((e1.getX() - wheelCentre.x) * (e 2.getX() - wheelCentre.x) + (e1.getY() - wheelCentre.y) * (e2.getY() - wheelCentre.y)) / (float) (Math.sqrt(Math.pow(e1.getX() - wheelCentre.x, 2) + Math.pow(e1.getY() - wheelCentre.y, 2)) * Math.sqrt(Math.pow(e2.getX() - wheelCentre.x, 2) + Math.pow(e2.getY() - wheelCentre.y, 2)));
            float cosx = ((lastX - wheelCentre.x) * (e2.getX() - wheelCentre.x) + (lastY - wheelCentre.y) * (e2.getY() - wheelCentre.y)) / (float) (Math.sqrt(Math.pow(lastX - wheelCentre.x, 2) + Math.pow(lastY - wheelCentre.y, 2)) * Math.sqrt(Math.pow(e2.getX() - wheelCentre.x, 2) + Math.pow(e2.getY() - wheelCentre.y, 2)));
            scrollAngle = (float) Math.toDegrees(Math.acos(cosx));
            statusPN = checkPNValue(lastX, lastY, e2.getX(), e2.getY());
            scrollAngle = scrollAngle * statusPN;//转化正负值
            Log.i("TAG", "scrollAngle:" + scrollAngle);
            sumScrollAngle += scrollAngle;
            lastX = e2.getX();
            lastY = e2.getY();
            curAngle += scrollAngle;
            shiftValue = lastShiftValue + (int) (sumScrollAngle / singleAngle);
            onShiftChanged();
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            haveFling = true;
            float max = Math.abs(velocityX) > Math.abs(velocityY) ? velocityX : velocityY;
            if (Math.abs(max) > 20000) {
                flingShiftValue = 15;
            } else if (Math.abs(max) > 15000) {
                flingShiftValue = 10;
            } else if (Math.abs(max) > 10000) {
                flingShiftValue = 7;
            } else if (Math.abs(max) > 7000) {
                flingShiftValue = 5;
            } else if (Math.abs(max) > 5000) {
                flingShiftValue = 3;
            } else if (Math.abs(max) > 2000) {
                flingShiftValue = 2;
            } else {
                flingShiftValue = 0;
                haveFling = false;
            }
            return true;
        }
    };


手指离开屏幕之后,需要结算最终位置,因为各个圆不一定恰好停在各自原来的位置上。这里是通过判断是否滑过了单个圆所分配的角度的一半来决定是往前走一步还是后退一步。

 

 

/**
     * 停止滑动的时候需要调整位置
     *
     * @return
     */
    void setFinalPosition() {
        if (Math.abs(sumScrollAngle % singleAngle) > (singleAngle / 2)) {
            if (sumScrollAngle > 0) {
                shiftValue++;
            } else {
                shiftValue--;
            }
            notifyChanged();
        }
        sumScrollAngle = 0;
        curAngle = shiftValue * singleAngle;
        textAlpha = 1f;
        invalidate();
    }


通知外部刷新 shiftValue:

 

 

    public void notifyChanged() {
        if (shiftValue < minShiftValue) {
            shiftValue = minShiftValue;
            curAngle = shiftValue * singleAngle;
        } else if (shiftValue > maxShiftValue) {
            shiftValue = maxShiftValue;
            curAngle = shiftValue * singleAngle;
        }
        valueAdapter.setShiftValue(shiftValue);
        if (mListener != null && mListener.checkIfValueChanged(shiftValue)) {
            mListener.onValueChanged(valueAdapter);
        }
    }


到此,大部分功能均已实现,下面是动画部分。

 

这里动画的话,最初使用 curAngle 来做属性动画,后来发现由于浮点数的缘故,curAngle对 singleAngle进行整除的时候,容易造成shiftValue的值不稳定,因此这里同时对shiftValue和curAngle做动画,前者精确确定shiftValue的值,后者确定需要滑动的角度。

首先定义两个set方法:

 

    /**
     * 动画属性,不可以删除
     *
     * @param curAngle
     */
    public void setCurAngle(float curAngle) {
        this.curAngle = curAngle;
    }

    /**
     * 动画属性,不可以删除
     *
     * @param shiftValue
     */
    public void setShiftValue(int shiftValue) {
        this.shiftValue = shiftValue;
    }


开始动画,shiftValue和curAngle两个动画共享速率和插值器,当animatorSet结束的时候,刷新组件;然后还需要为animatorAngle设置一个监听来刷新:

 

 

    /**
     * 滑动过的角度,必须是正值,方向由 statusPN 处理
     */
    void startAnim(final int targetValue) {
        final float targetShiftAngle = targetValue * singleAngle;
        final float fromAngle = curAngle;
//        final float toAngle = curAngle + flingTargetAngle * statusPN;
        final float curSumScrollAngle = sumScrollAngle;
        Log.e("TAG", "startAnim_curShiftValue:" + shiftValue + "   targetShiftAngle:" + targetValue + "   fromAngle:" + fromAngle + "   targetShiftAngle:" + targetShiftAngle);
        animatorSet = new AnimatorSet();
        ObjectAnimator animatorAngle, animatorValue;
        animatorAngle = ObjectAnimator.ofFloat(this, "curAngle", fromAngle, targetShiftAngle);
        animatorValue = ObjectAnimator.ofInt(this, "shiftValue", shiftValue, targetValue);
        animatorSet.setDuration(FLING_DURATION);
        animatorSet.setInterpolator(new DecelerateInterpolator(1.2f));
        animatorSet.playTogether(animatorAngle, animatorValue);
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {}

            @Override
            public void onAnimationEnd(Animator animation) {
                curAngle = targetShiftAngle;
                sumScrollAngle = curSumScrollAngle + (targetShiftAngle - fromAngle);
                Log.e("TAG", "curAngle:" + curAngle + "   sumScrollAngle:" + sumScrollAngle + "   d:" + (sumScrollAngle % singleAngle));
                onShiftChanged();
            }

            @Override
            public void onAnimationCancel(Animator animation) {}

            @Override
            public void onAnimationRepeat(Animator animation) {}
        });
        animatorSet.start();
        animatorAngle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                /**
                 * 这里整除的关系,导致 shiftValue 出错
                 */
//                shiftValue = (int) (curAngle / singleAngle);
                Log.e("TAG", "targetValue:" + targetValue + "   shiftValue:" + shiftValue);
                onShiftChanged();
            }
        });
    }

 

 

 

根据滑动的角度调整日期文本的透明度:

 

    void checkTextAlpha() {
        float curShiftAngle = shiftValue * singleAngle;
        float distanceAngle = Math.abs(curShiftAngle - curAngle);
        textAlpha = 1 - distanceAngle / singleAngle;
        /*float delta = Math.abs(sumScrollAngle % singleAngle);
        textAlpha = 1 - delta / singleAngle;*/
    }


刷新组件,这里除了刷新本身之外,还要通知外部日期发生变更:

 

 

    void onShiftChanged() {
        checkTextAlpha();        
        invalidate();
        notifyChanged();
    }

 

 

附上点击跳转的处理:

 

    /**
     * left,top,right,bottom
     */
    float[][] roundPositions = new float[5][4];

 

 

 

 

 

 

    /**
     * 确定圆的位置,实现轮盘无须这么做,主要是为了实现“点击圆点滚动到那一天”的需求
     */
    void initRoundPosition() {
        float detectArea = secondRoundRadius / 2;//这里扩展了点击范围
        double singleRadian = Math.toRadians(singleAngle);
        for (int i = 0; i < sumValues; i++) {
            float centerX = wheelCentre.x + (float) (wheelRadius * Math.sin(i * singleRadian));
            float centerY = wheelCentre.y - (float) (wheelRadius * Math.cos(i * singleRadian));
            roundPositions[i][0] = centerX - detectArea;
            roundPositions[i][1] = centerY - detectArea;
            roundPositions[i][2] = centerX + detectArea;
            roundPositions[i][3] = centerY + detectArea;
        }
        Log.e("TAG", "xxx");
    }


跳转到某一天:

 

 

    public void setTargetShiftValue(int shiftValue) {
        /**
         * 进行任何操作之前,应该先停下动画,不然在cancel之前仍然会修改curAngle的值
         */
        if (animatorSet != null) {
            animatorSet.cancel();
        }
        float toAngle = shiftValue * singleAngle;
        float flingAngle = toAngle - curAngle;
        if (flingAngle > 0) {
            statusPN = 1;//这里 flingTargetAngle 自带符号
        } else {
            statusPN = -1;
            flingAngle = -flingAngle;
        }
        sumScrollAngle = 0;//setFinalPosition 需要使用这个值
        curAngle = this.shiftValue * singleAngle;//重置curAngle到shiftValue,防止因为半途取消导致的curAngle不在正点上
        startAnim(shiftValue);
    }

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值