How would I implement a swipe-based circular control like this?

前端 未结 10 2030
北恋
北恋 2021-01-30 23:52

I am working on an Android application, and I have a TextView where I display a price (for example 50$).

I would like to have a circular control similar to this picture:

相关标签:
10条回答
  • 2021-01-31 00:02

    I wrote this custom FrameLayout to detect circular movement around its center point. I'm using orientation of three points on a plane and the angle between them to determine when the user has made half a circle in one direction and then completes it in the same.

    public class CircularDialView extends FrameLayout implements OnTouchListener {
        private TextView counter;
        private int count = 50;
    
        private PointF startTouch;
        private PointF currentTouch;
        private PointF center;
        private boolean turning;
        private boolean switched = false;
    
        public enum RotationOrientation {
            CW, CCW, LINEAR;
        }
        private RotationOrientation lastRotatationDirection;
    
        public CircularDialView(Context context) {
            super(context);
            init();
        }
    
        public CircularDialView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public CircularDialView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init() {
            this.startTouch = new PointF();
            this.currentTouch = new PointF();
            this.center = new PointF();
            this.turning = false;
    
            this.setBackgroundResource(R.drawable.dial);
            this.counter = new TextView(getContext());
            this.counter.setTextSize(20);
            FrameLayout.LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            params.gravity = Gravity.CENTER;
            addView(this.counter, params);
    
            updateCounter();
            this.setOnTouchListener(this);
        }
    
        private void updateCounter() {
            this.counter.setText(Integer.toString(count));
        }
    
        // need to keep the view square
        @Override
        public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
            super.onMeasure(widthMeasureSpec, widthMeasureSpec);
            center.set(getWidth()/2, getWidth()/2);
        }
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    startTouch.set(event.getX(), event.getY());
                    turning = true;
                    return true; 
                }
                case MotionEvent.ACTION_MOVE: {
                    if(turning) {
                        currentTouch.set(event.getX(), event.getY());
                        RotationOrientation turningDirection = getOrientation(center, startTouch, currentTouch);
    
                        if (lastRotatationDirection != turningDirection) {
                            double angle = getRotationAngle(center, startTouch, currentTouch);
                            Log.d ("Angle", Double.toString(angle));
                            // the touch event has switched its orientation 
                            // and the current touch point is close to the start point
                            // a full cycle has been made
                            if (switched && angle < 10) {
                                if (turningDirection == RotationOrientation.CCW) {
                                    count--;
                                    updateCounter();
                                    switched = false;
                                }
                                else if (turningDirection == RotationOrientation.CW) {
                                    count++;
                                    updateCounter();
                                    switched = false;
                                }
                            }
                            // checking if the angle is big enough is needed to prevent
                            // the user from switching from the start point only
                            else if (!switched && angle > 170) {
                                switched = true;
                            }
                        }
    
                        lastRotatationDirection = turningDirection;
                        return true;
                    }
                }
    
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP: {
                    turning  = false;
                    return true;
                }
            }
    
            return false;
        }
    
    
        // checks the orientation of three points on a plane
        private RotationOrientation getOrientation(PointF a, PointF b, PointF c){
            double face = a.x * b.y + b.x * c.y + c.x * a.y - (c.x * b.y + b.x * a.y + a.x * c.y);
            if (face > 0)
                 return RotationOrientation.CW;
            else if (face < 0)
                 return RotationOrientation.CCW;
            else return RotationOrientation.LINEAR;
        }
    
        // using dot product to calculate the angle between the vectors ab and ac
        public double getRotationAngle(PointF a, PointF b, PointF c){
            double len1 = dist (a, b);
            double len2 = dist (a, c);
            double product = (b.x - a.x) * (c.x - a.x) + (b.y - a.y) * (c.y - a.y);
    
            return Math.toDegrees(Math.acos(product / (len1 * len2)));
        }
    
        // calculates the distance between two points on a plane
        public double dist (PointF a, PointF b) {
            return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
        }
    }
    
    0 讨论(0)
  • 2021-01-31 00:04

    The OvalSeekbar lib does something like that,I suggest you have a look at how the motion events are done in it.Here is the link to its git https://github.com/kshoji/AndroidCustomViews

    0 讨论(0)
  • 2021-01-31 00:09

    you need to use Circular SeekBar you can find sample and librery from here

    some another also may usefull here , this.

    thanx i hope it helps.

    0 讨论(0)
  • 2021-01-31 00:12

    DialView Class :

    public abstract class DialView extends View {
    
        private float centerX;
        private float centerY;
        private float minCircle;
        private float maxCircle;
        private float stepAngle;
    
        public DialView(Context context) {
            super(context);
            stepAngle = 1;
            setOnTouchListener(new OnTouchListener() {
                private float startAngle;
                private boolean isDragging;
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    float touchX = event.getX();
                    float touchY = event.getY();
                    switch (event.getActionMasked()) {
                    case MotionEvent.ACTION_DOWN:
                        startAngle = touchAngle(touchX, touchY);
                        isDragging = isInDiscArea(touchX, touchY);
                        break;
                    case MotionEvent.ACTION_MOVE:
                        if (isDragging) {
                            float touchAngle = touchAngle(touchX, touchY);
                            float deltaAngle = (360 + touchAngle - startAngle + 180) % 360 - 180;
                            if (Math.abs(deltaAngle) > stepAngle) {
                                int offset = (int) deltaAngle / (int) stepAngle;
                                startAngle = touchAngle;
                                onRotate(offset);
                            }
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        isDragging = false;
                        break;
                    }
                    return true;
                }
            });
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            centerX = getMeasuredWidth() / 2f;
            centerY = getMeasuredHeight() / 2f;
            super.onLayout(changed, l, t, r, b);
        }
    
        @SuppressLint("DrawAllocation")
        @Override
        protected void onDraw(Canvas canvas) {
            float radius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2f;
            Paint paint = new Paint();
            paint.setDither(true);
            paint.setAntiAlias(true);
            paint.setStyle(Style.FILL);
            paint.setColor(0xFFFFFFFF);
            paint.setXfermode(null);
            LinearGradient linearGradient = new LinearGradient(
                radius, 0, radius, radius, 0xFFFFFFFF, 0xFFEAEAEA, Shader.TileMode.CLAMP);
            paint.setShader(linearGradient);
            canvas.drawCircle(centerX, centerY, maxCircle * radius, paint);
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
            canvas.drawCircle(centerX, centerY, minCircle * radius, paint);
            paint.setXfermode(null);
            paint.setShader(null);
            paint.setColor(0x15000000);
            for (int i = 0, n =  360 / (int) stepAngle; i < n; i++) {
                double rad = Math.toRadians((int) stepAngle * i);
                int startX = (int) (centerX + minCircle * radius * Math.cos(rad));
                int startY = (int) (centerY + minCircle * radius * Math.sin(rad));
                int stopX = (int) (centerX + maxCircle * radius * Math.cos(rad));
                int stopY = (int) (centerY + maxCircle * radius * Math.sin(rad));
                canvas.drawLine(startX, startY, stopX, stopY, paint);
            }
            super.onDraw(canvas);
        }
    
        /**
         * Define the step angle in degrees for which the
         * dial will call {@link #onRotate(int)} event
         * @param angle : angle between each position
         */
        public void setStepAngle(float angle) {
            stepAngle = Math.abs(angle % 360);
        }
    
        /**
         * Define the draggable disc area with relative circle radius
         * based on min(width, height) dimension (0 = center, 1 = border)
         * @param radius1 : internal or external circle radius
         * @param radius2 : internal or external circle radius
         */
        public void setDiscArea(float radius1, float radius2) {
            radius1 = Math.max(0, Math.min(1, radius1));
            radius2 = Math.max(0, Math.min(1, radius2));
            minCircle = Math.min(radius1, radius2);
            maxCircle = Math.max(radius1, radius2);
        }
    
        /**
         * Check if touch event is located in disc area
         * @param touchX : X position of the finger in this view
         * @param touchY : Y position of the finger in this view
         */
        private boolean isInDiscArea(float touchX, float touchY) {
            float dX2 = (float) Math.pow(centerX - touchX, 2);
            float dY2 = (float) Math.pow(centerY - touchY, 2);
            float distToCenter = (float) Math.sqrt(dX2 + dY2);
            float baseDist = Math.min(centerX, centerY);
            float minDistToCenter = minCircle * baseDist;
            float maxDistToCenter = maxCircle * baseDist;
            return distToCenter >= minDistToCenter && distToCenter <= maxDistToCenter;
        }
    
        /**
         * Compute a touch angle in degrees from center
         * North = 0, East = 90, West = -90, South = +/-180
         * @param touchX : X position of the finger in this view
         * @param touchY : Y position of the finger in this view
         * @return angle
         */
        private float touchAngle(float touchX, float touchY) {
            float dX = touchX - centerX;
            float dY = centerY - touchY;
            return (float) (270 - Math.toDegrees(Math.atan2(dY, dX))) % 360 - 180;
        }
    
        protected abstract void onRotate(int offset);
    
    }
    

    Use it :

    public class DialActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle state) {
            setContentView(new RelativeLayout(this) {
                private int value = 0;
                private TextView textView;
                {
                    addView(new DialView(getContext()) {
                        {
                            // a step every 20°
                            setStepAngle(20f);
                            // area from 30% to 90%
                            setDiscArea(.30f, .90f);
                        }
                        @Override
                        protected void onRotate(int offset) {
                            textView.setText(String.valueOf(value += offset));
                        }
                    }, new RelativeLayout.LayoutParams(0, 0) {
                        {
                            width = MATCH_PARENT;
                            height = MATCH_PARENT;
                            addRule(RelativeLayout.CENTER_IN_PARENT);
                        }
                    });
                    addView(textView = new TextView(getContext()) {
                        {
                            setText(Integer.toString(value));
                            setTextColor(Color.WHITE);
                            setTextSize(30);
                        }
                    }, new RelativeLayout.LayoutParams(0, 0) {
                        {
                            width = WRAP_CONTENT;
                            height = WRAP_CONTENT;
                            addRule(RelativeLayout.CENTER_IN_PARENT);
                        }
                    });
                }
            });
            super.onCreate(state);
        }
    
    }
    

    Result :

    result

    0 讨论(0)
  • 2021-01-31 00:13

    I've just written the following code and only tested it theoretically.

    private final double stepSizeAngle = Math.PI / 10f; //Angle diff to increase/decrease dial by 1$
    private final double dialStartValue = 50.0;
    
    //Center of your dial
    private float dialCenterX = 500;
    private float dialCenterY = 500;
    
    private float fingerStartDiffX;
    private float fingerStartDiffY;
    
    private double currentDialValueExact = dialStartValue;
    
    
    public boolean onTouchEvent(MotionEvent event) {
        int eventaction = event.getAction();
    
        switch (eventaction) {
            case MotionEvent.ACTION_DOWN: 
                //Vector between startpoint and center
                fingerStartDiffX = event.getX() - dialCenterX;
                fingerStartDiffY = event.getY() - dialCenterY;
                break;
    
            case MotionEvent.ACTION_MOVE:
                //Vector between current point and center
                float xDiff = event.getX() - dialCenterX;
                float yDiff = event.getY() - dialCenterY;
    
                //Range from -PI to +PI
                double alpha = Math.atan2(fingerStartDiffY, yDiff) - Math.atan2(fingerStartDiffX, xDiff);
    
                //calculate exact difference between last move and current move.
                //This will take positive and negative direction into account.
                double dialIncrease = alpha / stepSizeAngle;        
                currentDialValueExact += dialIncrease;
    
                //Round down if we're above the start value and up if we are below
                setDialValue((int)(currentDialValueExact > dialStartValue ? Math.floor(currentDialValueExact) : Math.ceil(currentDialValueExact));
    
                //set fingerStartDiff to the current position to allow multiple rounds on the dial
                fingerStartDiffX = xDiff;
                fingerStartDiffY = yDiff;
                break;
        }
    
        // tell the system that we handled the event and no further processing is required
        return true; 
    }
    
    private void setDialValue(int value) {
        //assign value
    }
    

    If you would like to change the direction, simply do alpha = -alpha.

    0 讨论(0)
  • 2021-01-31 00:19

    Perhaps you could look into the onTouchEvent(MotionEvent) of the view. Keep a track of the x and y coordinates as you move the finger. Notice the pattern of the coordinate changes as you move the finger. You can use that to achieve the increase/decrease in the price. See this link.

    0 讨论(0)
提交回复
热议问题