فهرست منبع

Legend updates in PieChart

Berislav 9 سال پیش
والد
کامیت
0f310c2c4b

+ 1 - 1
.idea/misc.xml

@@ -37,7 +37,7 @@
     <ConfirmationsSetting value="0" id="Add" />
     <ConfirmationsSetting value="0" id="Remove" />
   </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
   <component name="ProjectType">

+ 3 - 3
app/build.gradle

@@ -7,11 +7,11 @@ def libraryArtifactId = 'android-graphs'
 def libraryVersion = '0.0.6'
 
 android {
-    compileSdkVersion 26
+    compileSdkVersion 25
     buildToolsVersion "25.0.3"
     defaultConfig {
         minSdkVersion 19
-        targetSdkVersion 26
+        targetSdkVersion 25
         versionCode 1
         versionName libraryVersion
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -29,7 +29,7 @@ dependencies {
     androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
         exclude group: 'com.android.support', module: 'support-annotations'
     })
-    compile 'com.android.support:appcompat-v7:26.+'
+    compile 'com.android.support:appcompat-v7:25.+'
     testCompile 'junit:junit:4.12'
 }
 

+ 332 - 0
app/src/main/java/hr/tacitus/graphs/PieChartView.java

@@ -0,0 +1,332 @@
+package hr.tacitus.graphs;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.support.annotation.Nullable;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Bero on 7.7.2017..
+ */
+
+public class PieChartView extends View {
+
+    class PieChartItem {
+        float value;
+        // color for this pie chart item
+        Paint paint;
+        // text
+        String description;
+
+        // cached value
+        int descriptionWidth;
+
+        float getAngle() {
+            return value * 3.6f;
+        }
+    }
+
+    private static float MAX_LEGEND_WIDTH_PERCENTAGE = 35f;
+    private static float MAX_LEGEND_WIDTH_PX = 300;
+    private static float LEGEND_PADDING_HORIZONTAL = 20f;
+    private static float LEGEND_PADDING_TOP = 20f;
+
+    private float titleTextSizeSp = 16.f;
+
+    private boolean showLegend = true;
+    private float legendTextSizeSp = 10.f;
+    private float legendSquareSize = 20f;
+    private float legendWidth;
+
+    private float chartPadding = 64.f;
+    private float chartTextSizeSp = 10.f;
+
+
+    private String title = "This is a title";
+
+    private List<Integer> colors;
+
+    private TextPaint titlePaint;
+    private TextPaint valuePaint;
+    private TextPaint legendDescriptionPaint;
+    private StaticLayout titleTextLayout;
+
+    private RectF titleRect;
+    private RectF legendRect;
+    private RectF pieChartRect;
+
+    private List<PieChartItem> items = new ArrayList<>();
+
+    private boolean readyToDraw = false;
+
+    public PieChartView(Context context) {
+        super(context);
+        init(null);
+    }
+
+    public PieChartView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(attrs);
+    }
+
+    public PieChartView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(attrs);
+    }
+
+    private void readAttributes(AttributeSet attributeSet) {
+        TypedArray a = getContext().obtainStyledAttributes(attributeSet, R.styleable.PieChartView);
+        if (a != null) {
+            try {
+                title = a.getString(R.styleable.PieChartView_title);
+                showLegend = a.getBoolean(R.styleable.PieChartView_showLegend, true);
+            } finally {
+                a.recycle();
+            }
+        }
+    }
+
+    @Nullable
+    private RectF getLegendRect() {
+        if (showLegend) {
+            float left = titleRect.right - legendWidth;
+            float right = titleRect.right;
+            float top = titleRect.bottom;
+            float bottom = getBottom();
+
+            return new RectF(left, top, right, bottom);
+        }
+        return null;
+    }
+
+    private RectF getPieChartRect() {
+        float left = titleRect.left + chartPadding;
+        float top = titleRect.top + chartPadding;
+        float right = titleRect.right - chartPadding;
+        if (showLegend)
+            right -= legendRect.width();
+        float bottom = getBottom() - getPaddingLeft() - chartPadding;
+        RectF rectF = new RectF(left, top, right, bottom);
+        // width must be equal to height
+        if (rectF.width() != rectF.height()) {
+            float width = Math.min(rectF.width(), rectF.height());
+            left = rectF.centerX() - width / 2;
+            right = left + width;
+            top = rectF.centerY() - width / 2;
+            bottom = top + width;
+
+            rectF = new RectF(left, top, right, bottom);
+        }
+
+        return rectF;
+    }
+
+    private RectF getTitleRect() {
+        return new RectF(getPaddingLeft(), getPaddingTop(),
+                getWidth() - getPaddingLeft() - getPaddingRight(), 32.f);
+    }
+
+    private void initTitleText() {
+        // int width = (int) titlePaint.measureText(title);
+        titleTextLayout = new StaticLayout(title, titlePaint, (int) titleRect.width(),
+                Layout.Alignment.ALIGN_CENTER, 1.0f, 0, false);
+    }
+
+    private void init(AttributeSet attributeSet) {
+        if (attributeSet != null)
+            readAttributes(attributeSet);
+
+        titlePaint = new TextPaint();
+        titlePaint.setAntiAlias(true);
+        titlePaint.setTextSize(Utils.spsToPixels(titleTextSizeSp, getResources()));
+        titlePaint.setColor(Color.BLACK);
+
+        valuePaint = new TextPaint();
+        valuePaint.setAntiAlias(true);
+        valuePaint.setTextSize(Utils.spsToPixels(chartTextSizeSp, getResources()));
+        valuePaint.setColor(Color.BLACK);
+
+        legendDescriptionPaint = new TextPaint();
+        legendDescriptionPaint.setAntiAlias(true);
+        legendDescriptionPaint.setTextSize(Utils.spsToPixels(legendTextSizeSp, getResources()));
+    }
+
+    private void initRectangles() {
+        titleRect = getTitleRect();
+        if (showLegend) {
+            updateLegendWidth();
+        }
+        legendRect = getLegendRect();
+        pieChartRect = getPieChartRect();
+    }
+
+    private void drawTitle(Canvas canvas) {
+        if (titleTextLayout != null)
+            titleTextLayout.draw(canvas);
+    }
+
+    private void drawChart(Canvas canvas) {
+
+        float startAngle = -90.f;
+        for (PieChartItem item : items) {
+            canvas.drawArc(pieChartRect, startAngle, item.getAngle(), true, item.paint);
+            startAngle += item.getAngle();
+        }
+
+        startAngle = 90.f;
+        for (PieChartItem item : items) {
+            float angleMiddle = startAngle - item.getAngle() / 2;
+            float r = pieChartRect.width() / 2;
+            float x = (float) Math.cos(Math.toRadians(angleMiddle)) * r;
+            float y = (float) Math.sin(Math.toRadians(angleMiddle)) * r;
+
+            String text = Double.toString(item.value);
+            // Depending on quadrant, adjust text.
+
+
+            float textWidth = valuePaint.measureText(text);
+            float left = x + pieChartRect.centerX();
+            float top = pieChartRect.centerY() - y;
+
+            // RectF textRect = new RectF(left, top, left + textWidth, top + valuePaint.getTextSize());
+            canvas.drawText(Integer.toHexString(item.paint.getColor()), left, top, valuePaint);
+
+            startAngle -= item.getAngle();
+        }
+    }
+
+    private void drawLegend(Canvas canvas) {
+        Paint p = new Paint();
+        p.setColor(Color.RED);
+        canvas.drawRect(legendRect, p);
+
+        float left = legendRect.left + LEGEND_PADDING_HORIZONTAL;
+        float top = legendRect.top + LEGEND_PADDING_TOP;
+
+        // Draw color explanations
+        for (int i = 0; i < items.size(); ++i) {
+            PieChartItem item = items.get(i);
+
+            canvas.drawRect(new RectF(left, top, left + legendSquareSize, top + legendSquareSize), item.paint);
+            // canvas.drawText(Double.toString(item.value), left + 30f, top + 20f, valuePaint);
+            StaticLayout staticLayout = new StaticLayout(item.description,
+                    valuePaint, Utils.textWidth(item.description, valuePaint, true),
+                    Layout.Alignment.ALIGN_NORMAL, 0, 0, false);
+
+            canvas.save();
+            canvas.translate(left + (legendSquareSize * 1.5f), top);
+            staticLayout.draw(canvas);
+
+            canvas.restore();
+
+            top += legendSquareSize * 1.5f;
+        }
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        if (!readyToDraw)
+            return;
+
+        drawTitle(canvas);
+        drawChart(canvas);
+        if (showLegend)
+            drawLegend(canvas);
+    }
+
+    @Override
+    public void onSizeChanged(int xNew, int yNew, int xOld, int yOld) {
+        super.onSizeChanged(xNew, yNew, xOld, yOld);
+
+        initRectangles();
+        initTitleText();
+
+        readyToDraw = true;
+    }
+
+    private void initColors(int count) {
+        colors = new ArrayList<>();
+
+        // 3 sets of 2 main colors - rb, rg, bg
+        int maxValue = 220;
+        int perSetGain = (int) (maxValue / (Math.ceil(count / 6.) + 1));
+
+        float r = maxValue, g = 0, b = 0;
+        for (int i = 0; i < count; ++i) {
+            int color = Color.rgb((int) r, (int) g, (int) b);
+            colors.add(color);
+
+            r = Color.green(color);
+            g = Color.blue(color);
+            b = Color.red(color);
+
+            if (i != 0 && (i + 1) % 3 == 0) {
+                if (i == 2) {
+                    g = maxValue;
+                } else {
+                    r -= perSetGain;
+                }
+            }
+        }
+    }
+
+    private void createItems(float[] values) {
+        for (int i = 0; i < values.length; ++i) {
+            PieChartItem pieChartItem = new PieChartItem();
+            pieChartItem.value = values[i];
+            pieChartItem.paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+            pieChartItem.paint.setColor(colors.get(i));
+            pieChartItem.description = Double.toString(values[i]);
+
+            pieChartItem.descriptionWidth = Utils.textWidth(pieChartItem.description, legendDescriptionPaint, true);
+
+            items.add(pieChartItem);
+        }
+    }
+
+    private float getMaxLegendWidth() {
+        float width = getWidth();
+        float maxPercWidth = width * MAX_LEGEND_WIDTH_PERCENTAGE;
+        return Math.min(maxPercWidth, MAX_LEGEND_WIDTH_PX);
+    }
+
+    /**
+     * Calculates required legend width.
+     */
+    private void updateLegendWidth() {
+        float maxWidth = getMaxLegendWidth();
+
+        legendWidth = 0;
+        for (PieChartItem item : items) {
+            float width = item.descriptionWidth + 2 * LEGEND_PADDING_HORIZONTAL + legendSquareSize * 1.5f;
+            if (width > legendWidth)
+                legendWidth = width;
+
+            if (legendWidth >= maxWidth) {
+                legendWidth = maxWidth;
+                break;
+            }
+        }
+    }
+
+    public void setValues(float[] values) {
+        items.clear();
+
+        initColors(values.length);
+        createItems(values);
+        initRectangles();
+        invalidate();
+    }
+
+}

+ 54 - 0
app/src/main/java/hr/tacitus/graphs/Utils.java

@@ -0,0 +1,54 @@
+package hr.tacitus.graphs;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.TypedValue;
+
+/**
+ * Created by Bero on 8.7.2017..
+ */
+
+public class Utils {
+
+    public static float dipsToPixels(float dips) {
+        return dipsToPixels(dips, Resources.getSystem());
+    }
+
+    public static float dipsToPixels(float dips, Resources resources) {
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dips, resources.getDisplayMetrics());
+    }
+
+    public static float spsToPixels(float sps, Resources resources) {
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sps, resources.getDisplayMetrics());
+    }
+
+    public static Rect textBounds(String text, Paint paint) {
+        Rect bounds = new Rect();
+        paint.getTextBounds(text, 0, text.length(), bounds);
+        return bounds;
+    }
+
+    public static int textHeight(String text, Paint paint) {
+        return textBounds(text, paint).height();
+    }
+
+    /**
+     *
+     * https://stackoverflow.com/questions/7549182/android-paint-measuretext-vs-gettextbounds
+     *
+     * @param text content
+     * @param paint text paint
+     * @param useMeasureText measureText returns slightly different result, including so called
+     *                       Glyphs's AdvanceX value. So measureText() >(=?) getTextBounds' width().
+     * @return text width
+     */
+    public static int textWidth(String text, Paint paint, boolean useMeasureText) {
+        if (useMeasureText)
+            return (int)Math.ceil(paint.measureText(text));
+        return textBounds(text, paint).width();
+    }
+
+}

+ 5 - 0
app/src/main/res/values/attrs.xml

@@ -10,4 +10,9 @@
         <attr name="showHorizontalValues" format="boolean" />
         <attr name="noDataText" format="string" />
     </declare-styleable>
+
+    <declare-styleable name="PieChartView">
+        <attr name="title" format="string" />
+        <attr name="showLegend" format="boolean" />
+    </declare-styleable>
 </resources>

+ 3 - 3
graphsapp/build.gradle

@@ -1,13 +1,13 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 26
+    compileSdkVersion 25
     buildToolsVersion "25.0.3"
 
     defaultConfig {
         applicationId "hr.tacitus.graphsapp"
         minSdkVersion 19
-        targetSdkVersion 26
+        targetSdkVersion 25
         versionCode 1
         versionName "1.0"
 
@@ -27,7 +27,7 @@ dependencies {
     androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
         exclude group: 'com.android.support', module: 'support-annotations'
     })
-    compile 'com.android.support:appcompat-v7:26.+'
+    compile 'com.android.support:appcompat-v7:25.+'
     compile 'com.android.support.constraint:constraint-layout:1.0.2'
     testCompile 'junit:junit:4.12'
     compile project(':app')

+ 13 - 3
graphsapp/src/main/java/hr/tacitus/graphsapp/MainActivity.java

@@ -6,19 +6,28 @@ import android.view.View;
 
 import hr.tacitus.graphs.BarGraphAdapter;
 import hr.tacitus.graphs.BarGraphView;
+import hr.tacitus.graphs.PieChartView;
+
 import java.text.DecimalFormat;
 
 public class MainActivity extends AppCompatActivity {
 
     private BarGraphView bgv;
+    private PieChartView pieChartView;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
 
-        bgv = (BarGraphView) findViewById(R.id.bgvGraf);
-        updateAdapter();
+//        bgv = (BarGraphView) findViewById(R.id.bgvGraf);
+//        updateAdapter();
+
+        // float[] values = {12.5f, 50.0f, 25f, 12.5f};
+        float[] values = {10f, 5f, 15f, 10f, 10f, 5f, 15f, 5f, 5f, 5f, 5f, 5f, 5f, };
+        pieChartView = (PieChartView) findViewById(R.id.pieChart);
+        pieChartView.setValues(values);
+
     }
 
     public void toggleAdapter(View view) {
@@ -60,6 +69,7 @@ public class MainActivity extends AppCompatActivity {
     }
 
     private void updateAdapter() {
-        bgv.setAdapter(bgv.getAdapter() == null ? createSampleAdapter() : null);
+        if (bgv != null)
+            bgv.setAdapter(bgv.getAdapter() == null ? createSampleAdapter() : null);
     }
 }

+ 16 - 11
graphsapp/src/main/res/layout/activity_main.xml

@@ -19,19 +19,24 @@
             android:text="Toggle Adapter" />
     </LinearLayout>
 
-
-    <hr.tacitus.graphs.BarGraphView
+    <hr.tacitus.graphs.PieChartView
         android:layout_below="@id/toolbar"
-        android:id="@+id/bgvGraf"
-        android:layout_width="fill_parent"
-        android:layout_height="fill_parent"
-        android:paddingTop="42dp"
-        android:paddingLeft="6dp"
-        android:paddingBottom="6dp"
-        android:paddingRight="6dp"
-
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:title="Expenses share in total costs"
+        android:id="@+id/pieChart"
+        />
 
 
-        />
+    <!--<hr.tacitus.graphs.BarGraphView-->
+        <!--android:layout_below="@id/toolbar"-->
+        <!--android:id="@+id/bgvGraf"-->
+        <!--android:layout_width="0dp"-->
+        <!--android:layout_height="0dp"-->
+        <!--android:paddingTop="42dp"-->
+        <!--android:paddingLeft="6dp"-->
+        <!--android:paddingBottom="6dp"-->
+        <!--android:paddingRight="6dp"-->
+        <!--/>-->
 
 </RelativeLayout>