androidandroid-canvasgradientondraw

SweepGradient Android - How to set start angle for Gradient


I am having a hard time understanding how to use a SweepGradient to show my gradient color from a specific angle on Canvas.

For example: If I have an arc from 1 - 3pm, I would like to provide a Gradient as a Color. The Gradient should start from 2pm.

I have the following code which shows the Color without any gradient applied to it.

 SweepGradient sweepGradient = new
                                SweepGradient(provideRectF().width() / 2, provideRectF().height() / 2,
                                arcColors, new float[]{
                                0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f
                        });

                        Matrix matrix = new Matrix();
                        matrix.postRotate(currentAngle, provideRectF().width() / 2, provideRectF().height() / 2);
                        sweepGradient.setLocalMatrix(matrix);

How can I make my Color show the gradient from 2pm (in terms of angle) for the given arc?


Solution

  • This answer is based on the excellent post on SweepGradient by Attila Tanyi.

    To get a better understanding of SweepGradient, let's first paint the whole screen with a SweepGradient which is centered in the middle of the screen and shows sections corresponding to a clock face (please note that the area between 1pm and 2pm is a solid color since this is part of your setup):

    fullscreen sweep gradient

    The positions:

    // positions for a clock face
    // note: we need an opening position 0.0f as well as a closing position 1.0f, both of which belong to 3 pm
    float[] positions = {0.0f,
                1/12f, 2/12f, 3/12f, 
                4/12f, 5/12f, 6/12f,
                7/12f, 8/12f, 9/12f,
                10/12f, 11/12f, 1.0f};
    

    The colors:

    int yellow = 0xFFFFFF88;
    int blue = 0xFF0088FF;
    int white = 0xFFFFFFFF;
    int black = 0xFF000000;
    
    // provide as many color values as there are positions
    // we want to paint a "normal color" from 1pm to 2pm and then a gradient from 2pm to 3 pm
    int[] colors = { black, // the first value is for 3 pm, the sweep starts here
            yellow, // 4
            white,  // 5
            black,  // 6
            white,  // 7
            yellow, // 8
            blue,   // 9
            black,  // 10
            white,  // 11
            black,  // 12
            blue,   // 1 constant color from 1pm to 2pm
            blue,   // 2
            white // the last value also is at 3 pm, the sweep ends here
    };
    

    Initialising the Path and the Paint:

    private Path circle = new Path();
    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    
    // ...
    paint.setStyle(Paint.Style.FILL_AND_STROKE);
    paint.setStrokeWidth(30);
    

    I experimented with a custom View and configured the gradient in onLayout(). Since the post is about drawing an arc, I tried a full circle next:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // use a square rectangle which does not touch the screen borders:
        float squareMaxWidth = Math.min(right - left, bottom - top) - 20;
        RectF circleRect = new RectF(left + 20, top + 20, left + squareMaxWidth, top + squareMaxWidth);
        // draw a full circle
        circle.addArc(circleRect, 180, 360);
        // calculate its center
        float centerH = (circleRect.right + circleRect.left)*0.5f;
        float centerV = (circleRect.bottom + circleRect.top)*0.5f;
     
        sweepGradient = new SweepGradient(centerH,centerV, colors, positions);
        paint.setShader(sweepGradient);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(circle, paint);
    }
    

    The result:

    full circle with sweep gradient

    Next, let's just draw the border: change the Paint style

    paint.setStyle(Paint.Style.STROKE);
    

    enter image description here

    And finally, just draw an arc from 1 pm to 3 pm. Remember that the angle for 3 pm is zero and that one section on the clock face corresponds to 30 degrees. So in onLayout() we have

    circle.addArc(circleRect, 300, 60);
    

    arc from 1 pm to 3 pm

    Another example: if you want to draw an arc from 4:15 pm to 4:45 pm with a gradient starting at 4:33 pm, you get the following picture (colored arc drawn over a full SweepGradient circle with alternating black and white at each "hour")

    arc from 4:15 pm to 4:45 pm with a gradient starting at 4:33 pm

    The colors:

    int[] colors = {      
            primaryColor,
            primaryColor,
            accentColor };
    

    To calculate the positions one needs to do a little math:

    60 minutes = one hour ~ 1/12f
    3 minutes = one hour / 20 ~ 1/240f
    15 minutes = 5 * 3 minutes ~ 5 / 240f = 1 / 48f
    45 minutes = 3 * 15 minutes ~ 1 / 16f
    33 minutes = 11 * 3 minutes ~ 11 / 240f

    float[] positions = {
            (1/12f + 1/48f),   // 4:15 pm
            (1/12f + 11/240f), // 4:33 pm
            (1/12f + 1/16f)    // 4:45 pm
         };
    

    Similarly, one can calculate the values for the start angle and sweep angle:

    one hour ~ 30 degrees
    one minute ~ 0.5 degrees

    // start at 4:15 pm:
    float startAngle = (30 +  15*0.5f);
    // sweep for 1/2 hour:
    float sweepAngle = 15f;
    circle.addArc(circleRect, startAngle, sweepAngle);
    

    For the use case where the clockface is defined by an arbitrary rectangle, one may want to calculate the angles depending on the coordinates for the hours:

    // coordinates of hours on the frame of a rectangular clockface
    private PointF[] coordinates = new PointF[12];
    
    // angles of hours for a rectangular clockface
    private float[] angles = new float[13];
    
    private void init() {
        redPaint.setARGB(255, 255, 0,0);
        redPaint.setStrokeWidth(6);
    
        backgroundPaint.setARGB(255, 24, 24, 24);
        backgroundPaint.setStrokeWidth(10);
        backgroundPaint.setStyle(Paint.Style.STROKE);
    
        paint.setStyle(Paint.Style.FILL);
    }
    

    Calculate the coordinates and the corresponding angles for the SwipeGradient in onLayout()

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        all.set(left, top, right, bottom);
        centerX = (right + left)*0.5f;
        centerY = (bottom + top)*0.5f;
    
        calculateCoordinates();
        calculateAngles();
    
        sweepGradient = new SweepGradient(centerX,centerY, colors1, angles);
        paint.setShader(sweepGradient);
    }
    
    private void calculateAngles() {
        for(int i = 0; i < coordinates.length; i++) {
            PointF point = coordinates[i];
            double length = Math.sqrt(point.x * point.x + point.y * point.y);
    
            double pX = point.x/ length;
            double pY = point.y/ length;
    
            double rawAngle = Math.atan2(pX, pY)/  (2 * Math.PI);
            // rawAngle is a value between -0.5 and 0.5 where 0.5 corresponds to 6 o'clock and 0.0 to 12 o'clock
            // We need to make 0.0 correspond to 3 o'clock:
            double angle = rawAngle + 0.75;
            if (angle >= 1) angle--;
            
            angles[i] = (float) angle;
        }
        angles[12] = 1.0f;
    }
    
    /**
     * Start at 3 o'clock and proceed clockwise
     */
    private void calculateCoordinates() {
        float halfWidth =(all.right - all.left) * 0.5f;
        float halfHeight =(all.bottom - all.top) * 0.5f;
    
        coordinates[0] = new PointF(halfWidth, 0);
    
        coordinates[1] = new PointF(halfWidth, -0.5f * halfHeight );
    
        coordinates[2] = new PointF(0.5f * halfWidth, - halfHeight);
        coordinates[3] = new PointF(0, - halfHeight);
        coordinates[4] = new PointF(- 0.5f * halfWidth, - halfHeight);
    
        coordinates[5] = new PointF(- halfWidth, -0.5f * halfHeight);
        coordinates[6] = new PointF(- halfWidth, 0);
        coordinates[7] = new PointF(- halfWidth, 0.5f * halfHeight);
    
        coordinates[8] = new PointF(-0.5f * halfWidth, halfHeight);
        coordinates[9] = new PointF(0, halfHeight);
        coordinates[10] = new PointF(0.5f * halfWidth, halfHeight);
    
        coordinates[11] = new PointF(halfWidth, 0.5f * halfHeight);
    }
    

    In onDraw(), draw the clockface covering the whole View, and for each hour, draw a red line connecting the hour to the center:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        canvas.drawRect(all, paint);
    
        for (PointF point : coordinates) {
            drawLine(point, canvas);
        }
        canvas.drawRect(all, backgroundPaint);
    }
    
    private void drawLine(PointF point, Canvas canvas) {
        canvas.drawLine(centerX, centerY, centerX + point.x, centerY + point.y, redPaint);
    }
    
    rectangular clock 300dp x 200dp rectangular clock 300dp x 500dp
    black-and-white SwipeGradient 300dp x 200dp plus red lines connecting hours to center black-and-white SwipeGradient 300dp x 500dp plus red lines connecting hours to center