/** * GraphView * Copyright 2016 Jonas Gehring * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jjoe64.graphview; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; import android.net.Uri; import android.provider.MediaStore; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import com.jjoe64.graphview.series.BaseSeries; import com.jjoe64.graphview.series.Series; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.List; /** * @author jjoe64 */ public class GraphView extends View { /** * Class to wrap style options that are general * to graphs. * * @author jjoe64 */ private static final class Styles { /** * The font size of the title that can be displayed * above the graph. * * @see GraphView#setTitle(String) */ float titleTextSize; /** * The font color of the title that can be displayed * above the graph. * * @see GraphView#setTitle(String) */ int titleColor; } /** * Helper class to detect tap events on the * graph. * * @author jjoe64 */ private class TapDetector { /** * save the time of the last down event */ private long lastDown; /** * point of the tap down event */ private PointF lastPoint; /** * to be called to process the events * * @param event * @return true if there was a tap event. otherwise returns false. */ public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { lastDown = System.currentTimeMillis(); lastPoint = new PointF(event.getX(), event.getY()); } else if (lastDown > 0 && event.getAction() == MotionEvent.ACTION_MOVE) { if (Math.abs(event.getX() - lastPoint.x) > 60 || Math.abs(event.getY() - lastPoint.y) > 60) { lastDown = 0; } } else if (event.getAction() == MotionEvent.ACTION_UP) { if (System.currentTimeMillis() - lastDown < 400) { return true; } } return false; } } /** * our series (this does not contain the series * that can be displayed on the right side. The * right side series is a special feature of * the {@link SecondScale} feature. */ private List<Series> mSeries; /** * the renderer for the grid and labels */ private GridLabelRenderer mGridLabelRenderer; /** * viewport that holds the current bounds of * view. */ private Viewport mViewport; /** * title of the graph that will be shown above */ private String mTitle; /** * wraps the general styles */ private Styles mStyles; /** * feature to have a second scale e.g. on the * right side */ protected SecondScale mSecondScale; /** * tap detector */ private TapDetector mTapDetector; /** * renderer for the legend */ private LegendRenderer mLegendRenderer; /** * paint for the graph title */ private Paint mPaintTitle; private boolean mIsCursorMode; /** * paint for the preview (in the SDK) */ private Paint mPreviewPaint; private CursorMode mCursorMode; /** * Initialize the GraphView view * @param context */ public GraphView(Context context) { super(context); init(); } /** * Initialize the GraphView view. * * @param context * @param attrs */ public GraphView(Context context, AttributeSet attrs) { super(context, attrs); init(); } /** * Initialize the GraphView view * * @param context * @param attrs * @param defStyle */ public GraphView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } /** * initialize the internal objects. * This method has to be called directly * in the constructors. */ protected void init() { mPreviewPaint = new Paint(); mPreviewPaint.setTextAlign(Paint.Align.CENTER); mPreviewPaint.setColor(Color.BLACK); mPreviewPaint.setTextSize(50); mStyles = new Styles(); mViewport = new Viewport(this); mGridLabelRenderer = new GridLabelRenderer(this); mLegendRenderer = new LegendRenderer(this); mSeries = new ArrayList<Series>(); mPaintTitle = new Paint(); mTapDetector = new TapDetector(); loadStyles(); } /** * loads the font */ protected void loadStyles() { mStyles.titleColor = mGridLabelRenderer.getHorizontalLabelsColor(); mStyles.titleTextSize = mGridLabelRenderer.getTextSize(); } /** * @return the renderer for the grid and labels */ public GridLabelRenderer getGridLabelRenderer() { return mGridLabelRenderer; } /** * Add a new series to the graph. This will * automatically redraw the graph. * @param s the series to be added */ public void addSeries(Series s) { s.onGraphViewAttached(this); mSeries.add(s); onDataChanged(false, false); } /** * important: do not do modifications on the list * object that will be returned. * Use {@link #removeSeries(com.jjoe64.graphview.series.Series)} and {@link #addSeries(com.jjoe64.graphview.series.Series)} * * @return all series */ public List<Series> getSeries() { // TODO immutable array return mSeries; } /** * call this to let the graph redraw and * recalculate the viewport. * This will be called when a new series * was added or removed and when data * was appended via {@link com.jjoe64.graphview.series.BaseSeries#appendData(com.jjoe64.graphview.series.DataPointInterface, boolean, int)} * or {@link com.jjoe64.graphview.series.BaseSeries#resetData(com.jjoe64.graphview.series.DataPointInterface[])}. * * @param keepLabelsSize true if you don't want * to recalculate the size of * the labels. It is recommended * to use "true" because this will * improve performance and prevent * a flickering. * @param keepViewport true if you don't want that * the viewport will be recalculated. * It is recommended to use "true" for * performance. */ public void onDataChanged(boolean keepLabelsSize, boolean keepViewport) { // adjustSteps grid system mViewport.calcCompleteRange(); if (mSecondScale != null) { mSecondScale.calcCompleteRange(); } mGridLabelRenderer.invalidate(keepLabelsSize, keepViewport); postInvalidate(); } /** * draw all the stuff on canvas * * @param canvas */ protected void drawGraphElements(Canvas canvas) { // must be in hardware accelerated mode if (android.os.Build.VERSION.SDK_INT >= 11 && !canvas.isHardwareAccelerated()) { // just warn about it, because it is ok when making a snapshot Log.w("GraphView", "GraphView should be used in hardware accelerated mode." + "You can use android:hardwareAccelerated=\"true\" on your activity. Read this for more info:" + "https://developer.android.com/guide/topics/graphics/hardware-accel.html"); } drawTitle(canvas); mViewport.drawFirst(canvas); mGridLabelRenderer.draw(canvas); for (Series s : mSeries) { s.draw(this, canvas, false); } if (mSecondScale != null) { for (Series s : mSecondScale.getSeries()) { s.draw(this, canvas, true); } } if (mCursorMode != null) { mCursorMode.draw(canvas); } mViewport.draw(canvas); mLegendRenderer.draw(canvas); } /** * will be called from Android system. * * @param canvas Canvas */ @Override protected void onDraw(Canvas canvas) { if (isInEditMode()) { canvas.drawColor(Color.rgb(200, 200, 200)); canvas.drawText("GraphView: No Preview available", canvas.getWidth()/2, canvas.getHeight()/2, mPreviewPaint); } else { drawGraphElements(canvas); } } /** * Draws the Graphs title that will be * shown above the viewport. * Will be called by GraphView. * * @param canvas Canvas */ protected void drawTitle(Canvas canvas) { if (mTitle != null && mTitle.length()>0) { mPaintTitle.setColor(mStyles.titleColor); mPaintTitle.setTextSize(mStyles.titleTextSize); mPaintTitle.setTextAlign(Paint.Align.CENTER); float x = canvas.getWidth()/2; float y = mPaintTitle.getTextSize(); canvas.drawText(mTitle, x, y, mPaintTitle); } } /** * Calculates the height of the title. * * @return the actual size of the title. * if there is no title, 0 will be * returned. */ protected int getTitleHeight() { if (mTitle != null && mTitle.length()>0) { return (int) mPaintTitle.getTextSize(); } else { return 0; } } /** * @return the viewport of the Graph. * @see com.jjoe64.graphview.Viewport */ public Viewport getViewport() { return mViewport; } /** * Called by Android system if the size * of the view was changed. Will recalculate * the viewport and labels. * * @param w * @param h * @param oldw * @param oldh */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); onDataChanged(false, false); } /** * @return the space on the left side of the * view from the left border to the * beginning of the graph viewport. */ public int getGraphContentLeft() { int border = getGridLabelRenderer().getStyles().padding; return border + getGridLabelRenderer().getLabelVerticalWidth() + getGridLabelRenderer().getVerticalAxisTitleWidth(); } /** * @return the space on the top of the * view from the top border to the * beginning of the graph viewport. */ public int getGraphContentTop() { int border = getGridLabelRenderer().getStyles().padding + getTitleHeight(); return border; } /** * @return the height of the graph viewport. */ public int getGraphContentHeight() { int border = getGridLabelRenderer().getStyles().padding; int graphheight = getHeight() - (2 * border) - getGridLabelRenderer().getLabelHorizontalHeight() - getTitleHeight(); graphheight -= getGridLabelRenderer().getHorizontalAxisTitleHeight(); return graphheight; } /** * @return the width of the graph viewport. */ public int getGraphContentWidth() { int border = getGridLabelRenderer().getStyles().padding; int graphwidth = getWidth() - (2 * border) - getGridLabelRenderer().getLabelVerticalWidth(); if (mSecondScale != null) { graphwidth -= getGridLabelRenderer().getLabelVerticalSecondScaleWidth(); graphwidth -= mSecondScale.getVerticalAxisTitleTextSize(); } return graphwidth; } /** * will be called from Android system. * * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { boolean b = mViewport.onTouchEvent(event); boolean a = super.onTouchEvent(event); // is it a click? if (mTapDetector.onTouchEvent(event)) { for (Series s : mSeries) { s.onTap(event.getX(), event.getY()); } if (mSecondScale != null) { for (Series s : mSecondScale.getSeries()) { s.onTap(event.getX(), event.getY()); } } } return b || a; } /** * */ @Override public void computeScroll() { super.computeScroll(); mViewport.computeScroll(); } /** * @return the legend renderer. * @see com.jjoe64.graphview.LegendRenderer */ public LegendRenderer getLegendRenderer() { return mLegendRenderer; } /** * use a specific legend renderer * * @param mLegendRenderer the new legend renderer */ public void setLegendRenderer(LegendRenderer mLegendRenderer) { this.mLegendRenderer = mLegendRenderer; } /** * @return the title that will be shown * above the graph. */ public String getTitle() { return mTitle; } /** * Set the title of the graph that will * be shown above the graph's viewport. * * @param mTitle the title * @see #setTitleColor(int) to set the font color * @see #setTitleTextSize(float) to set the font size */ public void setTitle(String mTitle) { this.mTitle = mTitle; } /** * @return the title font size */ public float getTitleTextSize() { return mStyles.titleTextSize; } /** * Set the title's font size * * @param titleTextSize font size * @see #setTitle(String) */ public void setTitleTextSize(float titleTextSize) { mStyles.titleTextSize = titleTextSize; } /** * @return font color of the title */ public int getTitleColor() { return mStyles.titleColor; } /** * Set the title's font color * * @param titleColor font color of the title * @see #setTitle(String) */ public void setTitleColor(int titleColor) { mStyles.titleColor = titleColor; } /** * creates the second scale logic and returns it * * @return second scale object */ public SecondScale getSecondScale() { if (mSecondScale == null) { // this creates the second scale mSecondScale = new SecondScale(this); mSecondScale.setVerticalAxisTitleTextSize(mGridLabelRenderer.mStyles.textSize); } return mSecondScale; } /** * clears the second scale */ public void clearSecondScale() { if (mSecondScale != null) { mSecondScale.removeAllSeries(); mSecondScale = null; } } /** * Removes all series of the graph. */ public void removeAllSeries() { mSeries.clear(); onDataChanged(false, false); } /** * Remove a specific series of the graph. * This will also re-draw the graph, but * without recalculating the viewport and * label sizes. * If you want this, you have to call {@link #onDataChanged(boolean, boolean)} * manually. * * @param series */ public void removeSeries(Series<?> series) { mSeries.remove(series); onDataChanged(false, false); } /** * takes a snapshot and return it as bitmap * * @return snapshot of graph */ public Bitmap takeSnapshot() { Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); draw(canvas); return bitmap; } /** * takes a snapshot, stores it and open the share dialog. * Notice that you need the permission android.permission.WRITE_EXTERNAL_STORAGE * * @param context * @param imageName * @param title */ public void takeSnapshotAndShare(Context context, String imageName, String title) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); Bitmap inImage = takeSnapshot(); inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes); String path = MediaStore.Images.Media.insertImage(context.getContentResolver(), inImage, imageName, null); Intent i = new Intent(Intent.ACTION_SEND); i.setType("image/*"); i.putExtra(Intent.EXTRA_STREAM, Uri.parse(path)); try { context.startActivity(Intent.createChooser(i, title)); } catch (android.content.ActivityNotFoundException ex) { ex.printStackTrace(); } } public void setCursorMode(boolean b) { mIsCursorMode = b; if (mIsCursorMode) { if (mCursorMode == null) { mCursorMode = new CursorMode(this); } } else { mCursorMode = null; invalidate(); } for (Series series : mSeries) { if (series instanceof BaseSeries) { ((BaseSeries) series).clearCursorModeCache(); } } } public CursorMode getCursorMode() { return mCursorMode; } public boolean isCursorMode() { return mIsCursorMode; } }