Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.deskclock.widget;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.content.Context;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Canvas;
     23 import android.graphics.Color;
     24 import android.graphics.Paint;
     25 import android.util.AttributeSet;
     26 import android.util.Property;
     27 import android.view.Gravity;
     28 import android.view.View;
     29 
     30 import com.android.deskclock.R;
     31 
     32 /**
     33  * A {@link View} that draws primitive circles.
     34  */
     35 public class CircleView extends View {
     36 
     37     /**
     38      * A Property wrapper around the fillColor functionality handled by the
     39      * {@link #setFillColor(int)} and {@link #getFillColor()} methods.
     40      */
     41     public final static Property<CircleView, Integer> FILL_COLOR =
     42             new Property<CircleView, Integer>(Integer.class, "fillColor") {
     43         @Override
     44         public Integer get(CircleView view) {
     45             return view.getFillColor();
     46         }
     47 
     48         @Override
     49         public void set(CircleView view, Integer value) {
     50             view.setFillColor(value);
     51         }
     52     };
     53 
     54     /**
     55      * A Property wrapper around the radius functionality handled by the
     56      * {@link #setRadius(float)} and {@link #getRadius()} methods.
     57      */
     58     public final static Property<CircleView, Float> RADIUS =
     59             new Property<CircleView, Float>(Float.class, "radius") {
     60         @Override
     61         public Float get(CircleView view) {
     62             return view.getRadius();
     63         }
     64 
     65         @Override
     66         public void set(CircleView view, Float value) {
     67             view.setRadius(value);
     68         }
     69     };
     70 
     71     /**
     72      * The {@link Paint} used to draw the circle.
     73      */
     74     private final Paint mCirclePaint = new Paint();
     75 
     76     private int mGravity;
     77     private float mCenterX;
     78     private float mCenterY;
     79     private float mRadius;
     80 
     81     public CircleView(Context context) {
     82         this(context, null /* attrs */);
     83     }
     84 
     85     public CircleView(Context context, AttributeSet attrs) {
     86         this(context, attrs, 0 /* defStyleAttr */);
     87     }
     88 
     89     public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
     90         super(context, attrs, defStyleAttr);
     91 
     92         final TypedArray a = context.obtainStyledAttributes(
     93                 attrs, R.styleable.CircleView, defStyleAttr, 0 /* defStyleRes */);
     94 
     95         mGravity = a.getInt(R.styleable.CircleView_android_gravity, Gravity.NO_GRAVITY);
     96         mCenterX = a.getDimension(R.styleable.CircleView_centerX, 0.0f);
     97         mCenterY = a.getDimension(R.styleable.CircleView_centerY, 0.0f);
     98         mRadius = a.getDimension(R.styleable.CircleView_radius, 0.0f);
     99 
    100         mCirclePaint.setColor(a.getColor(R.styleable.CircleView_fillColor, Color.WHITE));
    101 
    102         a.recycle();
    103     }
    104 
    105     @Override
    106     public void onRtlPropertiesChanged(int layoutDirection) {
    107         super.onRtlPropertiesChanged(layoutDirection);
    108 
    109         if (mGravity != Gravity.NO_GRAVITY) {
    110             applyGravity(mGravity, layoutDirection);
    111         }
    112     }
    113 
    114     @Override
    115     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    116         super.onLayout(changed, left, top, right, bottom);
    117 
    118         if (mGravity != Gravity.NO_GRAVITY) {
    119             applyGravity(mGravity, getLayoutDirection());
    120         }
    121     }
    122 
    123     @Override
    124     protected void onDraw(Canvas canvas) {
    125         super.onDraw(canvas);
    126 
    127         // draw the circle, duh
    128         canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint);
    129     }
    130 
    131     @Override
    132     public boolean hasOverlappingRendering() {
    133         // only if we have a background, which we shouldn't...
    134         return getBackground() != null && getBackground().getCurrent() != null;
    135     }
    136 
    137     /**
    138      * @return the current {@link Gravity} used to align/size the circle
    139      */
    140     public final int getGravity() {
    141         return mGravity;
    142     }
    143 
    144     /**
    145      * Describes how to align/size the circle relative to the view's bounds. Defaults to
    146      * {@link Gravity#NO_GRAVITY}.
    147      * <p/>
    148      * Note: using {@link #setCenterX(float)}, {@link #setCenterY(float)}, or
    149      * {@link #setRadius(float)} will automatically clear any conflicting gravity bits.
    150      *
    151      * @param gravity the {@link Gravity} flags to use
    152      * @return this object, allowing calls to methods in this class to be chained
    153      * @see R.styleable#CircleView_android_gravity
    154      */
    155     public CircleView setGravity(int gravity) {
    156         if (mGravity != gravity) {
    157             mGravity = gravity;
    158 
    159             if (gravity != Gravity.NO_GRAVITY && isLayoutDirectionResolved()) {
    160                 applyGravity(gravity, getLayoutDirection());
    161             }
    162         }
    163         return this;
    164     }
    165 
    166     /**
    167      * @return the ARGB color used to fill the circle
    168      */
    169     public final int getFillColor() {
    170         return mCirclePaint.getColor();
    171     }
    172 
    173     /**
    174      * Sets the ARGB color used to fill the circle and invalidates only the affected area.
    175      *
    176      * @param color the ARGB color to use
    177      * @return this object, allowing calls to methods in this class to be chained
    178      * @see R.styleable#CircleView_fillColor
    179      */
    180     public CircleView setFillColor(int color) {
    181         if (mCirclePaint.getColor() != color) {
    182             mCirclePaint.setColor(color);
    183 
    184             // invalidate the current area
    185             invalidate(mCenterX, mCenterY, mRadius);
    186         }
    187         return this;
    188     }
    189 
    190     /**
    191      * @return the x-coordinate of the center of the circle
    192      */
    193     public final float getCenterX() {
    194         return mCenterX;
    195     }
    196 
    197     /**
    198      * Sets the x-coordinate for the center of the circle and invalidates only the affected area.
    199      *
    200      * @param centerX the x-coordinate to use, relative to the view's bounds
    201      * @return this object, allowing calls to methods in this class to be chained
    202      * @see R.styleable#CircleView_centerX
    203      */
    204     public CircleView setCenterX(float centerX) {
    205         final float oldCenterX = mCenterX;
    206         if (oldCenterX != centerX) {
    207             mCenterX = centerX;
    208 
    209             // invalidate the old/new areas
    210             invalidate(oldCenterX, mCenterY, mRadius);
    211             invalidate(centerX, mCenterY, mRadius);
    212         }
    213 
    214         // clear the horizontal gravity flags
    215         mGravity &= ~Gravity.HORIZONTAL_GRAVITY_MASK;
    216 
    217         return this;
    218     }
    219 
    220     /**
    221      * @return the y-coordinate of the center of the circle
    222      */
    223     public final float getCenterY() {
    224         return mCenterY;
    225     }
    226 
    227     /**
    228      * Sets the y-coordinate for the center of the circle and invalidates only the affected area.
    229      *
    230      * @param centerY the y-coordinate to use, relative to the view's bounds
    231      * @return this object, allowing calls to methods in this class to be chained
    232      * @see R.styleable#CircleView_centerY
    233      */
    234     public CircleView setCenterY(float centerY) {
    235         final float oldCenterY = mCenterY;
    236         if (oldCenterY != centerY) {
    237             mCenterY = centerY;
    238 
    239             // invalidate the old/new areas
    240             invalidate(mCenterX, oldCenterY, mRadius);
    241             invalidate(mCenterX, centerY, mRadius);
    242         }
    243 
    244         // clear the vertical gravity flags
    245         mGravity &= ~Gravity.VERTICAL_GRAVITY_MASK;
    246 
    247         return this;
    248     }
    249 
    250     /**
    251      * @return the radius of the circle
    252      */
    253     public final float getRadius() {
    254         return mRadius;
    255     }
    256 
    257     /**
    258      * Sets the radius of the circle and invalidates only the affected area.
    259      *
    260      * @param radius the radius to use
    261      * @return this object, allowing calls to methods in this class to be chained
    262      * @see R.styleable#CircleView_radius
    263      */
    264     public CircleView setRadius(float radius) {
    265         final float oldRadius = mRadius;
    266         if (oldRadius != radius) {
    267             mRadius = radius;
    268 
    269             // invalidate the old/new areas
    270             invalidate(mCenterX, mCenterY, oldRadius);
    271             if (radius > oldRadius) {
    272                 invalidate(mCenterX, mCenterY, radius);
    273             }
    274         }
    275 
    276         // clear the fill gravity flags
    277         if ((mGravity & Gravity.FILL_HORIZONTAL) == Gravity.FILL_HORIZONTAL) {
    278             mGravity &= ~Gravity.FILL_HORIZONTAL;
    279         }
    280         if ((mGravity & Gravity.FILL_VERTICAL) == Gravity.FILL_VERTICAL) {
    281             mGravity &= ~Gravity.FILL_VERTICAL;
    282         }
    283 
    284         return this;
    285     }
    286 
    287     /**
    288      * Invalidates the rectangular area that circumscribes the circle defined by {@code centerX},
    289      * {@code centerY}, and {@code radius}.
    290      */
    291     private void invalidate(float centerX, float centerY, float radius) {
    292         invalidate((int) (centerX - radius - 0.5f), (int) (centerY - radius - 0.5f),
    293                 (int) (centerX + radius + 0.5f), (int) (centerY + radius + 0.5f));
    294     }
    295 
    296     /**
    297      * Applies the specified {@code gravity} and {@code layoutDirection}, adjusting the alignment
    298      * and size of the circle depending on the resolved {@link Gravity} flags. Also invalidates the
    299      * affected area if necessary.
    300      *
    301      * @param gravity the {@link Gravity} the {@link Gravity} flags to use
    302      * @param layoutDirection the layout direction used to resolve the absolute gravity
    303      */
    304     @SuppressLint("RtlHardcoded")
    305     private void applyGravity(int gravity, int layoutDirection) {
    306         final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
    307 
    308         final float oldRadius = mRadius;
    309         final float oldCenterX = mCenterX;
    310         final float oldCenterY = mCenterY;
    311 
    312         switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
    313             case Gravity.LEFT:
    314                 mCenterX = 0.0f;
    315                 break;
    316             case Gravity.CENTER_HORIZONTAL:
    317             case Gravity.FILL_HORIZONTAL:
    318                 mCenterX = getWidth() / 2.0f;
    319                 break;
    320             case Gravity.RIGHT:
    321                 mCenterX = getWidth();
    322                 break;
    323         }
    324 
    325         switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
    326             case Gravity.TOP:
    327                 mCenterY = 0.0f;
    328                 break;
    329             case Gravity.CENTER_VERTICAL:
    330             case Gravity.FILL_VERTICAL:
    331                 mCenterY = getHeight() / 2.0f;
    332                 break;
    333             case Gravity.BOTTOM:
    334                 mCenterY = getHeight();
    335                 break;
    336         }
    337 
    338         switch (absoluteGravity & Gravity.FILL) {
    339             case Gravity.FILL:
    340                 mRadius = Math.min(getWidth(), getHeight()) / 2.0f;
    341                 break;
    342             case Gravity.FILL_HORIZONTAL:
    343                 mRadius = getWidth() / 2.0f;
    344                 break;
    345             case Gravity.FILL_VERTICAL:
    346                 mRadius = getHeight() / 2.0f;
    347                 break;
    348         }
    349 
    350         if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != mRadius) {
    351             invalidate(oldCenterX, oldCenterY, oldRadius);
    352             invalidate(mCenterX, mCenterY, mRadius);
    353         }
    354     }
    355 }
    356