Home | History | Annotate | Download | only in androidplot
      1 /*
      2  * Copyright 2012 AndroidPlot.com
      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.androidplot;
     18 
     19 import android.content.Context;
     20 import android.graphics.*;
     21 import android.os.Build;
     22 import android.os.Looper;
     23 import android.util.AttributeSet;
     24 import android.util.Log;
     25 import android.view.View;
     26 import com.androidplot.exception.PlotRenderException;
     27 import com.androidplot.ui.*;
     28 import com.androidplot.ui.Formatter;
     29 import com.androidplot.ui.TextOrientationType;
     30 import com.androidplot.ui.widget.TextLabelWidget;
     31 import com.androidplot.ui.widget.Widget;
     32 import com.androidplot.ui.SeriesRenderer;
     33 import com.androidplot.util.Configurator;
     34 import com.androidplot.util.DisplayDimensions;
     35 import com.androidplot.util.PixelUtils;
     36 import com.androidplot.ui.XLayoutStyle;
     37 import com.androidplot.ui.YLayoutStyle;
     38 
     39 import java.util.*;
     40 
     41 /**
     42  * Base class for all other Plot implementations..
     43  */
     44 public abstract class Plot<SeriesType extends Series, FormatterType extends Formatter, RendererType extends SeriesRenderer>
     45         extends View implements Resizable{
     46     private static final String TAG = Plot.class.getName();
     47     private static final String XML_ATTR_PREFIX      = "androidplot";
     48 
     49     private static final String ATTR_TITLE           = "title";
     50     private static final String ATTR_RENDER_MODE     = "renderMode";
     51 
     52     public DisplayDimensions getDisplayDimensions() {
     53         return displayDims;
     54     }
     55 
     56     public enum BorderStyle {
     57         ROUNDED,
     58         SQUARE,
     59         NONE
     60     }
     61 
     62     /**
     63      * The RenderMode used by a Plot to draw it's self onto the screen.  The RenderMode can be set
     64      * in two ways.
     65      *
     66      * In an xml layout:
     67      *
     68      * <code>
     69      * <com.androidplot.xy.XYPlot
     70      * android:id="@+id/mySimpleXYPlot"
     71      * android:layout_width="fill_parent"
     72      * android:layout_height="fill_parent"
     73      * title="@string/sxy_title"
     74      * renderMode="useBackgroundThread"/>
     75      * </code>
     76      *
     77      * Programatically:
     78      *
     79      * <code>
     80      * XYPlot myPlot = new XYPlot(context "MyPlot", Plot.RenderMode.USE_MAIN_THREAD);
     81      * </code>
     82      *
     83      * A Plot's  RenderMode cannot be changed after the plot has been initialized.
     84      * @since 0.5.1
     85      */
     86     public enum RenderMode {
     87         /**
     88          * Use a second thread and an off-screen buffer to do drawing.  This is the preferred method
     89          * of drawing dynamic data and static data that consists of a large number of points.  This mode
     90          * provides more efficient CPU utilization at the cost of increased memory usage.  As of
     91          * version 0.5.1 this is the default RenderMode.
     92          *
     93          * XML value: use_background_thread
     94          * @since 0.5.1
     95          */
     96         USE_BACKGROUND_THREAD,
     97 
     98         /**
     99          * Do everything in the primary thread.  This is the preferred method of drawing static charts
    100          * and dynamic data that consists of a small number of points. This mode uses less memory at
    101          * the cost of poor CPU utilization.
    102          *
    103          * XML value: use_main_thread
    104          * @since 0.5.1
    105          */
    106         USE_MAIN_THREAD
    107     }
    108     private BoxModel boxModel = new BoxModel(3, 3, 3, 3, 3, 3, 3, 3);
    109     private BorderStyle borderStyle = Plot.BorderStyle.SQUARE;
    110     private float borderRadiusX = 15;
    111     private float borderRadiusY = 15;
    112     private boolean drawBorderEnabled = true;
    113     private Paint borderPaint;
    114     private Paint backgroundPaint;
    115     private LayoutManager layoutManager;
    116     private TextLabelWidget titleWidget;
    117     private DisplayDimensions displayDims = new DisplayDimensions();
    118     private RenderMode renderMode = RenderMode.USE_MAIN_THREAD;
    119     private final BufferedCanvas pingPong = new BufferedCanvas();
    120 
    121     // used to get rid of flickering when drawing offScreenBitmap to the visible Canvas.
    122     private final Object renderSynch = new Object();
    123 
    124     /**
    125      * Used for caching renderer instances.  Note that once a renderer is initialized it remains initialized
    126      * for the life of the application; does not and should not be destroyed until the application exits.
    127      */
    128     private LinkedList<RendererType> renderers;
    129 
    130     /**
    131      * Associates lists series and formatter pairs with the class of the Renderer used to render them.
    132      */
    133     private LinkedHashMap<Class, SeriesAndFormatterList<SeriesType,FormatterType>> seriesRegistry;
    134 
    135     private final ArrayList<PlotListener> listeners;
    136 
    137     private Thread renderThread;
    138     private boolean keepRunning = false;
    139     private boolean isIdle = true;
    140 
    141     {
    142         listeners = new ArrayList<PlotListener>();
    143         seriesRegistry = new LinkedHashMap<Class, SeriesAndFormatterList<SeriesType,FormatterType>>();
    144         renderers = new LinkedList<RendererType>();
    145         borderPaint = new Paint();
    146         borderPaint.setColor(Color.rgb(150, 150, 150));
    147         borderPaint.setStyle(Paint.Style.STROKE);
    148         borderPaint.setStrokeWidth(1.0f);
    149         borderPaint.setAntiAlias(true);
    150         backgroundPaint = new Paint();
    151         backgroundPaint.setColor(Color.DKGRAY);
    152         backgroundPaint.setStyle(Paint.Style.FILL);
    153     }
    154 
    155 
    156     /**
    157      *  Any rendering that utilizes a buffer from this class should synchronize rendering on the instance of this class
    158      *  that is being used.
    159      */
    160     private class BufferedCanvas {
    161         private volatile Bitmap bgBuffer;  // all drawing is done on this buffer.
    162         private volatile Bitmap fgBuffer;
    163         private Canvas canvas = new Canvas();
    164 
    165         /**
    166          * Call this method once drawing on a Canvas retrieved by {@link #getCanvas()} to mark
    167          * the buffer as fully rendered.  Failure to call this method will result in nothing being drawn.
    168          */
    169         public synchronized void swap() {
    170             Bitmap tmp = bgBuffer;
    171             bgBuffer = fgBuffer;
    172             fgBuffer = tmp;
    173         }
    174 
    175         public synchronized void resize(int h, int w) {
    176             if (w <= 0 || h <= 0) {
    177                 bgBuffer = null;
    178                 fgBuffer = null;
    179             } else {
    180                 bgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
    181                 fgBuffer = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
    182             }
    183         }
    184 
    185 
    186         /**
    187          * Get a Canvas for drawing.  Actual drawing should be synchronized on the instance
    188          * of BufferedCanvas being used.
    189          * @return The Canvas instance to draw onto.  Returns null if drawing buffers have not
    190          *         been initialized a la {@link #resize(int, int)}.
    191          */
    192         public synchronized Canvas getCanvas() {
    193             if(bgBuffer != null) {
    194                 canvas.setBitmap(bgBuffer);
    195                 return canvas;
    196             } else {
    197                 return null;
    198             }
    199         }
    200 
    201         /**
    202          * @return The most recent fully rendered Bitmsp
    203          */
    204         public Bitmap getBitmap() {
    205             return fgBuffer;
    206         }
    207     }
    208 
    209     /**
    210      * Convenience constructor - wraps {@link #Plot(android.content.Context, String, com.androidplot.Plot.RenderMode)}.
    211      * RenderMode is set to {@link RenderMode#USE_BACKGROUND_THREAD}.
    212      * @param context
    213      * @param title The display title of this Plot.
    214      */
    215     public Plot(Context context, String title) {
    216         this(context, title, RenderMode.USE_MAIN_THREAD);
    217     }
    218 
    219     /**
    220      * Used for programmatic instantiation.
    221      * @param context
    222      * @param title The display title of this Plot.
    223      */
    224     public Plot(Context context, String title, RenderMode mode) {
    225         super(context);
    226         this.renderMode = mode;
    227         init(null, null);
    228         setTitle(title);
    229     }
    230 
    231 
    232     /**
    233      * Required by super-class. Extending class' implementations should add
    234      * the following code immediately before exiting to ensure that loadAttrs
    235      * is called only once by the derived class:
    236      * <code>
    237      * if(getClass().equals(DerivedPlot.class) {
    238      *     loadAttrs(context, attrs);
    239      * }
    240      * </code>
    241      *
    242      * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet)}
    243      * for an example.
    244      * @param context
    245      * @param attrs
    246      */
    247     public Plot(Context context, AttributeSet attrs) {
    248         super(context, attrs);
    249         init(context, attrs);
    250     }
    251 
    252     /**
    253      * Required by super-class. Extending class' implementations should add
    254      * the following code immediately before exiting to ensure that loadAttrs
    255      * is called only once by the derived class:
    256      * <code>
    257      * if(getClass().equals(DerivedPlot.class) {
    258      *     loadAttrs(context, attrs);
    259      * }
    260      * </code>
    261      *
    262      * See {@link com.androidplot.xy.XYPlot#XYPlot(android.content.Context, android.util.AttributeSet, int)}
    263      * for an example.
    264      * @param context
    265      * @param attrs
    266      * @param defStyle
    267      */
    268     public Plot(Context context, AttributeSet attrs, int defStyle) {
    269         super(context, attrs, defStyle);
    270         init(context, attrs);
    271     }
    272 
    273     /**
    274      * Can be overridden by derived classes to control hardware acceleration state.
    275      * Note that this setting is only used on Honeycomb and later environments.
    276      * @return True if hardware acceleration is allowed, false otherwise.
    277      * @since 0.5.1
    278      */
    279     @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    280     protected boolean isHwAccelerationSupported() {
    281         return false;
    282     }
    283 
    284     /**
    285      * Sets the render mode used by the Plot.
    286      * WARNING: This method is not currently designed for general use outside of Configurator.
    287      * Attempting to reassign the render mode at runtime will result in unexpected behavior.
    288      * @param mode
    289      */
    290     public void setRenderMode(RenderMode mode) {
    291         this.renderMode = mode;
    292     }
    293 
    294     /**
    295      * Concrete implementations should do any final setup / initialization
    296      * here.  Immediately following this method's invocation, AndroidPlot assumes
    297      * that the Plot instance is ready for final configuration via the Configurator.
    298      */
    299     protected abstract void onPreInit();
    300 
    301 
    302     private void init(Context context, AttributeSet attrs) {
    303         PixelUtils.init(getContext());
    304         layoutManager = new LayoutManager();
    305         titleWidget = new TextLabelWidget(layoutManager, new SizeMetrics(25,
    306                 SizeLayoutType.ABSOLUTE, 100,
    307                 SizeLayoutType.ABSOLUTE),
    308                 TextOrientationType.HORIZONTAL);
    309         titleWidget.position(0, XLayoutStyle.RELATIVE_TO_CENTER, 0,
    310                 YLayoutStyle.ABSOLUTE_FROM_TOP, AnchorPosition.TOP_MIDDLE);
    311 
    312         onPreInit();
    313         // make sure the title widget is always the topmost widget:
    314         layoutManager.moveToTop(titleWidget);
    315         if(context != null && attrs != null) {
    316             loadAttrs(attrs);
    317         }
    318 
    319         layoutManager.onPostInit();
    320         Log.d(TAG, "AndroidPlot RenderMode: " + renderMode);
    321         if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
    322             renderThread = new Thread(new Runnable() {
    323                 @Override
    324                 public void run() {
    325 
    326                     keepRunning = true;
    327                     while (keepRunning) {
    328                         isIdle = false;
    329                         synchronized (pingPong) {
    330                             Canvas c = pingPong.getCanvas();
    331                             renderOnCanvas(c);
    332                             pingPong.swap();
    333                         }
    334                         synchronized (renderSynch) {
    335                             postInvalidate();
    336                             // prevent this thread from becoming an orphan
    337                             // after the view is destroyed
    338                             if (keepRunning) {
    339                                 try {
    340                                     renderSynch.wait();
    341                                 } catch (InterruptedException e) {
    342                                     keepRunning = false;
    343                                 }
    344                             }
    345                         }
    346                     }
    347                     System.out.println("AndroidPlot render thread finished.");
    348                 }
    349             });
    350         }
    351     }
    352 
    353     /**
    354      * Parse XML Attributes.  Should only be called once and at the end of the base class constructor.
    355      *
    356      * @param attrs
    357      */
    358     private void loadAttrs(AttributeSet attrs) {
    359 
    360         if (attrs != null) {
    361             // filter out androidplot prefixed attrs:
    362             HashMap<String, String> attrHash = new HashMap<String, String>();
    363             for (int i = 0; i < attrs.getAttributeCount(); i++) {
    364                 String attrName = attrs.getAttributeName(i);
    365 
    366                 // case insensitive check to see if this attr begins with our prefix:
    367                 if (attrName.toUpperCase().startsWith(XML_ATTR_PREFIX.toUpperCase())) {
    368                     attrHash.put(attrName.substring(XML_ATTR_PREFIX.length() + 1), attrs.getAttributeValue(i));
    369                 }
    370             }
    371             Configurator.configure(getContext(), this, attrHash);
    372         }
    373     }
    374 
    375     public RenderMode getRenderMode() {
    376         return renderMode;
    377     }
    378 
    379     public synchronized boolean addListener(PlotListener listener) {
    380         return !listeners.contains(listener) && listeners.add(listener);
    381     }
    382 
    383     public synchronized boolean removeListener(PlotListener listener) {
    384         return listeners.remove(listener);
    385     }
    386 
    387     protected void notifyListenersBeforeDraw(Canvas canvas) {
    388         for (PlotListener listener : listeners) {
    389             listener.onBeforeDraw(this, canvas);
    390         }
    391     }
    392 
    393     protected void notifyListenersAfterDraw(Canvas canvas) {
    394         for (PlotListener listener : listeners) {
    395             listener.onAfterDraw(this, canvas);
    396         }
    397     }
    398 
    399     /**
    400      * @param series
    401      */
    402     public synchronized boolean addSeries(SeriesType series, FormatterType formatter) {
    403         Class rendererClass = formatter.getRendererClass();
    404         SeriesAndFormatterList<SeriesType, FormatterType> sfList = seriesRegistry.get(rendererClass);
    405 
    406         // if there is no list for this renderer type, we need to getInstance one:
    407         if(sfList == null) {
    408             // make sure there is not already an instance of this renderer available:
    409             if(getRenderer(rendererClass) == null) {
    410                 renderers.add((RendererType) formatter.getRendererInstance(this));
    411             }
    412             sfList = new SeriesAndFormatterList<SeriesType,FormatterType>();
    413             seriesRegistry.put(rendererClass, sfList);
    414         }
    415 
    416         // if this series implements PlotListener, add it as a listener:
    417         if(series instanceof PlotListener) {
    418             addListener((PlotListener)series);
    419         }
    420 
    421         // do nothing if this series already associated with the renderer:
    422         if(sfList.contains(series)) {
    423             return false;
    424         } else {
    425             sfList.add(series, formatter);
    426             return true;
    427         }
    428     }
    429 
    430     public synchronized boolean removeSeries(SeriesType series, Class rendererClass) {
    431         boolean result = seriesRegistry.get(rendererClass).remove(series);
    432         if(seriesRegistry.get(rendererClass).size() <= 0) {
    433             seriesRegistry.remove(rendererClass);
    434         }
    435 
    436         // if series implements PlotListener, remove it from listeners:
    437         if(series instanceof PlotListener) {
    438             removeListener((PlotListener) series);
    439         }
    440         return result;
    441     }
    442 
    443     /**
    444      * Remove all occorrences of series from all renderers
    445      * @param series
    446      */
    447     public synchronized void removeSeries(SeriesType series) {
    448 
    449         // remove all occurrences of series from all renderers:
    450         for(Class rendererClass : seriesRegistry.keySet()) {
    451             seriesRegistry.get(rendererClass).remove(series);
    452         }
    453 
    454         // remove empty SeriesAndFormatterList instances from the registry:
    455         for(Iterator<SeriesAndFormatterList<SeriesType,FormatterType>> it = seriesRegistry.values().iterator(); it.hasNext();) {
    456             if(it.next().size() <= 0) {
    457                 it.remove();
    458             }
    459         }
    460 
    461         // if series implements PlotListener, remove it from listeners:
    462         if (series instanceof PlotListener) {
    463             removeListener((PlotListener) series);
    464         }
    465     }
    466 
    467     /**
    468      * Remove all series from all renderers
    469      */
    470     public void clear() {
    471         for(Iterator<SeriesAndFormatterList<SeriesType,FormatterType>> it = seriesRegistry.values().iterator(); it.hasNext();) {
    472             it.next();
    473             it.remove();
    474         }
    475     }
    476 
    477     public boolean isEmpty() {
    478         return seriesRegistry.isEmpty();
    479     }
    480 
    481     public FormatterType getFormatter(SeriesType series, Class rendererClass) {
    482         return seriesRegistry.get(rendererClass).getFormatter(series);
    483     }
    484 
    485     public SeriesAndFormatterList<SeriesType,FormatterType> getSeriesAndFormatterListForRenderer(Class rendererClass) {
    486         return seriesRegistry.get(rendererClass);
    487     }
    488 
    489     /**
    490      * Returns a list of all series assigned to the various renderers within the Plot.
    491      * The returned List will contain no duplicates.
    492      * @return
    493      */
    494     public Set<SeriesType> getSeriesSet() {
    495         Set<SeriesType> seriesSet = new LinkedHashSet<SeriesType>();
    496         for (SeriesRenderer renderer : getRendererList()) {
    497             List<SeriesType> seriesList = getSeriesListForRenderer(renderer.getClass());
    498             if (seriesList != null) {
    499                 for (SeriesType series : seriesList) {
    500                     seriesSet.add(series);
    501                 }
    502             }
    503         }
    504         return seriesSet;
    505     }
    506 
    507     public List<SeriesType> getSeriesListForRenderer(Class rendererClass) {
    508         SeriesAndFormatterList<SeriesType,FormatterType> lst = seriesRegistry.get(rendererClass);
    509         if(lst == null) {
    510             return null;
    511         } else {
    512             return lst.getSeriesList();
    513         }
    514     }
    515 
    516     public RendererType getRenderer(Class rendererClass) {
    517         for(RendererType renderer : renderers) {
    518             if(renderer.getClass() == rendererClass) {
    519                 return renderer;
    520             }
    521         }
    522         return null;
    523     }
    524 
    525     public List<RendererType> getRendererList() {
    526         return renderers;
    527     }
    528 
    529     public void setMarkupEnabled(boolean enabled) {
    530         this.layoutManager.setMarkupEnabled(enabled);
    531     }
    532 
    533     /**
    534      * Causes the plot to be redrawn.
    535      * @since 0.5.1
    536      */
    537     public void redraw() {
    538 
    539         if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
    540 
    541             // only enter synchronized block if the call is expected to block OR
    542             // if the render thread is idle, so we know that we won't have to wait to
    543             // obtain a lock.
    544             if (isIdle) {
    545                 synchronized (renderSynch) {
    546                     renderSynch.notify();
    547                 }
    548             }
    549         } else if(renderMode == RenderMode.USE_MAIN_THREAD) {
    550 
    551             // are we on the UI thread?
    552             if (Looper.myLooper() == Looper.getMainLooper()) {
    553                 invalidate();
    554             } else {
    555                 postInvalidate();
    556             }
    557         } else {
    558             throw new IllegalArgumentException("Unsupported Render Mode: " + renderMode);
    559         }
    560     }
    561 
    562     @Override
    563     public synchronized void layout(final DisplayDimensions dims) {
    564         displayDims = dims;
    565         layoutManager.layout(displayDims);
    566     }
    567 
    568     @Override
    569     protected void onDetachedFromWindow() {
    570         synchronized(renderSynch) {
    571             keepRunning = false;
    572             renderSynch.notify();
    573         }
    574     }
    575 
    576 
    577     @Override
    578     protected synchronized void onSizeChanged (int w, int h, int oldw, int oldh) {
    579 
    580         // update pixel conversion values
    581         PixelUtils.init(getContext());
    582 
    583         // disable hardware acceleration if it's not explicitly supported
    584         // by the current Plot implementation. this check only applies to
    585         // honeycomb and later environments.
    586         if (Build.VERSION.SDK_INT >= 11) {
    587             if (!isHwAccelerationSupported() && isHardwareAccelerated()) {
    588                 setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    589             }
    590         }
    591 
    592         // pingPong is only used in background rendering mode.
    593         if(renderMode == RenderMode.USE_BACKGROUND_THREAD) {
    594             pingPong.resize(h, w);
    595         }
    596 
    597         RectF cRect = new RectF(0, 0, w, h);
    598         RectF mRect = boxModel.getMarginatedRect(cRect);
    599         RectF pRect = boxModel.getPaddedRect(mRect);
    600 
    601         layout(new DisplayDimensions(cRect, mRect, pRect));
    602         super.onSizeChanged(w, h, oldw, oldh);
    603         if(renderThread != null && !renderThread.isAlive()) {
    604             renderThread.start();
    605         }
    606     }
    607 
    608     /**
    609      * Called whenever the plot needs to be drawn via the Handler, which invokes invalidate().
    610      * Should never be called directly; use {@link #redraw()} instead.
    611      * @param canvas
    612      */
    613     @Override
    614     protected void onDraw(Canvas canvas) {
    615         if (renderMode == RenderMode.USE_BACKGROUND_THREAD) {
    616             synchronized(pingPong) {
    617                 Bitmap bmp = pingPong.getBitmap();
    618                 if(bmp != null) {
    619                     canvas.drawBitmap(bmp, 0, 0, null);
    620                 }
    621             }
    622         } else if (renderMode == RenderMode.USE_MAIN_THREAD) {
    623             renderOnCanvas(canvas);
    624         } else {
    625             throw new IllegalArgumentException("Unsupported Render Mode: " + renderMode);
    626         }
    627     }
    628 
    629     /**
    630      * Renders the plot onto a canvas.  Used by both main thread to draw directly
    631      * onto the View's canvas as well as by background draw to render onto a
    632      * Bitmap buffer.  At the end of the day this is the main entry for a plot's
    633      * "heavy lifting".
    634      * @param canvas
    635      */
    636     protected synchronized void renderOnCanvas(Canvas canvas) {
    637         try {
    638             // any series interested in synchronizing with plot should
    639             // implement PlotListener.onBeforeDraw(...) and do a read lock from within its
    640             // invocation.  This is the entry point into that call:
    641             notifyListenersBeforeDraw(canvas);
    642             try {
    643                 // need to completely erase what was on the canvas before redrawing, otherwise
    644                 // some odd aliasing artifacts begin to build up around the edges of aa'd entities
    645                 // over time.
    646                 canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    647                 if (backgroundPaint != null) {
    648                     drawBackground(canvas, displayDims.marginatedRect);
    649                 }
    650 
    651                 layoutManager.draw(canvas);
    652 
    653                 if (getBorderPaint() != null) {
    654                     drawBorder(canvas, displayDims.marginatedRect);
    655                 }
    656             } catch (PlotRenderException e) {
    657                 Log.e(TAG, "Exception while rendering Plot.", e);
    658                 e.printStackTrace();
    659             } catch (Exception e) {
    660                 Log.e(TAG, "Exception while rendering Plot.", e);
    661             }
    662         } finally {
    663             isIdle = true;
    664             // any series interested in synchronizing with plot should
    665             // implement PlotListener.onAfterDraw(...) and do a read unlock from within that
    666             // invocation. This is the entry point for that invocation.
    667             notifyListenersAfterDraw(canvas);
    668         }
    669     }
    670 
    671 
    672     /**
    673      * Sets the visual style of the plot's border.
    674      * @param style
    675      * @param radiusX Sets the X radius for BorderStyle.ROUNDED.  Use null for all other styles.
    676      * @param radiusY Sets the Y radius for BorderStyle.ROUNDED.  Use null for all other styles.
    677      */
    678     public void setBorderStyle(BorderStyle style, Float radiusX, Float radiusY) {
    679         if (style == Plot.BorderStyle.ROUNDED) {
    680             if (radiusX == null || radiusY == null){
    681                 throw new IllegalArgumentException("radiusX and radiusY cannot be null when using BorderStyle.ROUNDED");
    682             }
    683             this.borderRadiusX = radiusX;
    684             this.borderRadiusY = radiusY;
    685         }
    686         this.borderStyle = style;
    687     }
    688 
    689     /**
    690      * Draws the plot's outer border.
    691      * @param canvas
    692      * @throws PlotRenderException
    693      */
    694     protected void drawBorder(Canvas canvas, RectF dims) {
    695         switch (borderStyle) {
    696             case ROUNDED:
    697                 canvas.drawRoundRect(dims, borderRadiusX, borderRadiusY, borderPaint);
    698                 break;
    699             case SQUARE:
    700                 canvas.drawRect(dims, borderPaint);
    701                 break;
    702             default:
    703         }
    704     }
    705 
    706     protected void drawBackground(Canvas canvas, RectF dims) {
    707         switch (borderStyle) {
    708             case ROUNDED:
    709                 canvas.drawRoundRect(dims, borderRadiusX, borderRadiusY, backgroundPaint);
    710                 break;
    711             case SQUARE:
    712                 canvas.drawRect(dims, backgroundPaint);
    713                 break;
    714             default:
    715         }
    716     }
    717 
    718     /**
    719      *
    720      * @return The displayed title of this Plot.
    721      */
    722     public String getTitle() {
    723         return getTitleWidget().getText();
    724     }
    725 
    726     /**
    727      *
    728      * @param title  The title to display on this Plot.
    729      */
    730     public void setTitle(String title) {
    731         titleWidget.setText(title);
    732     }
    733 
    734     public LayoutManager getLayoutManager() {
    735         return layoutManager;
    736     }
    737 
    738     public void setLayoutManager(LayoutManager layoutManager) {
    739         this.layoutManager = layoutManager;
    740     }
    741 
    742     public TextLabelWidget getTitleWidget() {
    743         return titleWidget;
    744     }
    745 
    746     public void setTitleWidget(TextLabelWidget titleWidget) {
    747         this.titleWidget = titleWidget;
    748     }
    749 
    750     public Paint getBackgroundPaint() {
    751         return backgroundPaint;
    752     }
    753 
    754     public void setBackgroundPaint(Paint backgroundPaint) {
    755         this.backgroundPaint = backgroundPaint;
    756     }
    757 
    758     /**
    759      * Convenience method - wraps the individual setMarginXXX methods into a single method.
    760      * @param left
    761      * @param top
    762      * @param right
    763      * @param bottom
    764      */
    765     public void setPlotMargins(float left, float top, float right, float bottom) {
    766         setPlotMarginLeft(left);
    767         setPlotMarginTop(top);
    768         setPlotMarginRight(right);
    769         setPlotMarginBottom(bottom);
    770     }
    771 
    772     /**
    773      * Convenience method - wraps the individual setPaddingXXX methods into a single method.
    774      * @param left
    775      * @param top
    776      * @param right
    777      * @param bottom
    778      */
    779     public void setPlotPadding(float left, float top, float right, float bottom) {
    780         setPlotPaddingLeft(left);
    781         setPlotPaddingTop(top);
    782         setPlotPaddingRight(right);
    783         setPlotPaddingBottom(bottom);
    784     }
    785 
    786     public float getPlotMarginTop() {
    787         return boxModel.getMarginTop();
    788     }
    789 
    790     public void setPlotMarginTop(float plotMarginTop) {
    791         boxModel.setMarginTop(plotMarginTop);
    792     }
    793 
    794     public float getPlotMarginBottom() {
    795         return boxModel.getMarginBottom();
    796     }
    797 
    798     public void setPlotMarginBottom(float plotMarginBottom) {
    799         boxModel.setMarginBottom(plotMarginBottom);
    800     }
    801 
    802     public float getPlotMarginLeft() {
    803         return boxModel.getMarginLeft();
    804     }
    805 
    806     public void setPlotMarginLeft(float plotMarginLeft) {
    807         boxModel.setMarginLeft(plotMarginLeft);
    808     }
    809 
    810     public float getPlotMarginRight() {
    811         return boxModel.getMarginRight();
    812     }
    813 
    814     public void setPlotMarginRight(float plotMarginRight) {
    815         boxModel.setMarginRight(plotMarginRight);
    816     }
    817 
    818     public float getPlotPaddingTop() {
    819         return boxModel.getPaddingTop();
    820     }
    821 
    822     public void setPlotPaddingTop(float plotPaddingTop) {
    823         boxModel.setPaddingTop(plotPaddingTop);
    824     }
    825 
    826     public float getPlotPaddingBottom() {
    827         return boxModel.getPaddingBottom();
    828     }
    829 
    830     public void setPlotPaddingBottom(float plotPaddingBottom) {
    831         boxModel.setPaddingBottom(plotPaddingBottom);
    832     }
    833 
    834     public float getPlotPaddingLeft() {
    835         return boxModel.getPaddingLeft();
    836     }
    837 
    838     public void setPlotPaddingLeft(float plotPaddingLeft) {
    839         boxModel.setPaddingLeft(plotPaddingLeft);
    840     }
    841 
    842     public float getPlotPaddingRight() {
    843         return boxModel.getPaddingRight();
    844     }
    845 
    846     public void setPlotPaddingRight(float plotPaddingRight) {
    847         boxModel.setPaddingRight(plotPaddingRight);
    848     }
    849 
    850     public Paint getBorderPaint() {
    851         return borderPaint;
    852     }
    853 
    854     /**
    855      * Set's the paint used to draw the border.  Note that this method
    856      * copies borderPaint and set's the copy's Paint.Style attribute to
    857      * Paint.Style.STROKE.
    858      * @param borderPaint
    859      */
    860     public void setBorderPaint(Paint borderPaint) {
    861         if(borderPaint == null) {
    862             this.borderPaint = null;
    863         } else {
    864             this.borderPaint = new Paint(borderPaint);
    865             this.borderPaint.setStyle(Paint.Style.STROKE);
    866         }
    867     }
    868 }
    869