diff --git a/MPChartLib/.gitignore b/MPChartLib/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/MPChartLib/.gitignore @@ -0,0 +1 @@ +/build diff --git a/MPChartLib/build.gradle b/MPChartLib/build.gradle new file mode 100644 index 0000000..1ff8302 --- /dev/null +++ b/MPChartLib/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.library' + +group='com.github.philjay' + +android { + compileSdkVersion 30 + buildToolsVersion '28.0.3' + defaultConfig { + minSdkVersion 14 + targetSdkVersion 30 + versionCode 3 + versionName '3.1.0' + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + testOptions { + unitTests.returnDefaultValues = true // this prevents "not mocked" error + } +} + +dependencies { + implementation 'androidx.annotation:annotation:1.0.0' + testImplementation 'junit:junit:4.12' +} + +task sourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs + classifier = 'sources' +} + +task javadoc(type: Javadoc) { + options.charSet = 'UTF-8' + failOnError false + source = android.sourceSets.main.java.sourceFiles + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} diff --git a/MPChartLib/ic_launcher-web.png b/MPChartLib/ic_launcher-web.png new file mode 100644 index 0000000..ef55922 Binary files /dev/null and b/MPChartLib/ic_launcher-web.png differ diff --git a/MPChartLib/pom.xml b/MPChartLib/pom.xml new file mode 100644 index 0000000..24675aa --- /dev/null +++ b/MPChartLib/pom.xml @@ -0,0 +1,89 @@ + + + + + 4.0.0 + 1.4.2-SNAPSHOT + com.github.mikephil + MPAndroidChart + MPAndroidChart + A simple Android chart view/graph view library, supporting line- bar- and piecharts as well as scaling, dragging and animations + https://github.com/PhilJay/MPAndroidChart + apklib + + + + UTF-8 + + + + src + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + 3.9.0-rc.2 + true + + + + + + true + + + + + + + + com.google.android + android + provided + 4.1.1.4 + + + + + + https://github.com/PhilJay/MPAndroidChart/issues + GitHub Issues + + + + + Apache License Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + https://github.com/PhilJay/MPAndroidChart + scm:git:git://github.com/PhilJay/MPAndroidChart.git + scm:git:git@github.com:PhilJay/MPAndroidChart.git + + + + + Philipp Jahoda + philjay.librarysup@gmail.com + http://stackoverflow.com/users/1590502/philipp-jahoda + PhilJay + + + diff --git a/MPChartLib/src/main/AndroidManifest.xml b/MPChartLib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d75f87c --- /dev/null +++ b/MPChartLib/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/animation/ChartAnimator.java b/MPChartLib/src/main/java/com/github/mikephil/charting/animation/ChartAnimator.java new file mode 100644 index 0000000..e5b82db --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/animation/ChartAnimator.java @@ -0,0 +1,207 @@ +package com.github.mikephil.charting.animation; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import androidx.annotation.RequiresApi; + +import com.github.mikephil.charting.animation.Easing.EasingFunction; + +/** + * Object responsible for all animations in the Chart. Animations require API level 11. + * + * @author Philipp Jahoda + * @author Mick Ashton + */ +public class ChartAnimator { + + /** object that is updated upon animation update */ + private AnimatorUpdateListener mListener; + + /** The phase of drawn values on the y-axis. 0 - 1 */ + @SuppressWarnings("WeakerAccess") + protected float mPhaseY = 1f; + + /** The phase of drawn values on the x-axis. 0 - 1 */ + @SuppressWarnings("WeakerAccess") + protected float mPhaseX = 1f; + + public ChartAnimator() { } + + @RequiresApi(11) + public ChartAnimator(AnimatorUpdateListener listener) { + mListener = listener; + } + + @RequiresApi(11) + private ObjectAnimator xAnimator(int duration, EasingFunction easing) { + + ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "phaseX", 0f, 1f); + animatorX.setInterpolator(easing); + animatorX.setDuration(duration); + + return animatorX; + } + + @RequiresApi(11) + private ObjectAnimator yAnimator(int duration, EasingFunction easing) { + + ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "phaseY", 0f, 1f); + animatorY.setInterpolator(easing); + animatorY.setDuration(duration); + + return animatorY; + } + + /** + * Animates values along the X axis, in a linear fashion. + * + * @param durationMillis animation duration + */ + @RequiresApi(11) + public void animateX(int durationMillis) { + animateX(durationMillis, Easing.Linear); + } + + /** + * Animates values along the X axis. + * + * @param durationMillis animation duration + * @param easing EasingFunction + */ + @RequiresApi(11) + public void animateX(int durationMillis, EasingFunction easing) { + + ObjectAnimator animatorX = xAnimator(durationMillis, easing); + animatorX.addUpdateListener(mListener); + animatorX.start(); + } + + /** + * Animates values along both the X and Y axes, in a linear fashion. + * + * @param durationMillisX animation duration along the X axis + * @param durationMillisY animation duration along the Y axis + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY) { + animateXY(durationMillisX, durationMillisY, Easing.Linear, Easing.Linear); + } + + /** + * Animates values along both the X and Y axes. + * + * @param durationMillisX animation duration along the X axis + * @param durationMillisY animation duration along the Y axis + * @param easing EasingFunction for both axes + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easing) { + + ObjectAnimator xAnimator = xAnimator(durationMillisX, easing); + ObjectAnimator yAnimator = yAnimator(durationMillisY, easing); + + if (durationMillisX > durationMillisY) { + xAnimator.addUpdateListener(mListener); + } else { + yAnimator.addUpdateListener(mListener); + } + + xAnimator.start(); + yAnimator.start(); + } + + /** + * Animates values along both the X and Y axes. + * + * @param durationMillisX animation duration along the X axis + * @param durationMillisY animation duration along the Y axis + * @param easingX EasingFunction for the X axis + * @param easingY EasingFunction for the Y axis + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX, + EasingFunction easingY) { + + ObjectAnimator xAnimator = xAnimator(durationMillisX, easingX); + ObjectAnimator yAnimator = yAnimator(durationMillisY, easingY); + + if (durationMillisX > durationMillisY) { + xAnimator.addUpdateListener(mListener); + } else { + yAnimator.addUpdateListener(mListener); + } + + xAnimator.start(); + yAnimator.start(); + } + + /** + * Animates values along the Y axis, in a linear fashion. + * + * @param durationMillis animation duration + */ + @RequiresApi(11) + public void animateY(int durationMillis) { + animateY(durationMillis, Easing.Linear); + } + + /** + * Animates values along the Y axis. + * + * @param durationMillis animation duration + * @param easing EasingFunction + */ + @RequiresApi(11) + public void animateY(int durationMillis, EasingFunction easing) { + + ObjectAnimator animatorY = yAnimator(durationMillis, easing); + animatorY.addUpdateListener(mListener); + animatorY.start(); + } + + /** + * Gets the Y axis phase of the animation. + * + * @return float value of {@link #mPhaseY} + */ + public float getPhaseY() { + return mPhaseY; + } + + /** + * Sets the Y axis phase of the animation. + * + * @param phase float value between 0 - 1 + */ + public void setPhaseY(float phase) { + if (phase > 1f) { + phase = 1f; + } else if (phase < 0f) { + phase = 0f; + } + mPhaseY = phase; + } + + /** + * Gets the X axis phase of the animation. + * + * @return float value of {@link #mPhaseX} + */ + public float getPhaseX() { + return mPhaseX; + } + + /** + * Sets the X axis phase of the animation. + * + * @param phase float value between 0 - 1 + */ + public void setPhaseX(float phase) { + if (phase > 1f) { + phase = 1f; + } else if (phase < 0f) { + phase = 0f; + } + mPhaseX = phase; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/animation/Easing.java b/MPChartLib/src/main/java/com/github/mikephil/charting/animation/Easing.java new file mode 100644 index 0000000..acb7dcc --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/animation/Easing.java @@ -0,0 +1,309 @@ +package com.github.mikephil.charting.animation; + +import android.animation.TimeInterpolator; +import androidx.annotation.RequiresApi; + +/** + * Easing options. + * + * @author Daniel Cohen Gindi + * @author Mick Ashton + */ +@SuppressWarnings("WeakerAccess") +@RequiresApi(11) +public class Easing { + + public interface EasingFunction extends TimeInterpolator { + @Override + float getInterpolation(float input); + } + + private static final float DOUBLE_PI = 2f * (float) Math.PI; + + @SuppressWarnings("unused") + public static final EasingFunction Linear = new EasingFunction() { + public float getInterpolation(float input) { + return input; + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInQuad = new EasingFunction() { + public float getInterpolation(float input) { + return input * input; + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutQuad = new EasingFunction() { + public float getInterpolation(float input) { + return -input * (input - 2f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutQuad = new EasingFunction() { + public float getInterpolation(float input) { + input *= 2f; + + if (input < 1f) { + return 0.5f * input * input; + } + + return -0.5f * ((--input) * (input - 2f) - 1f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInCubic = new EasingFunction() { + public float getInterpolation(float input) { + return (float) Math.pow(input, 3); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutCubic = new EasingFunction() { + public float getInterpolation(float input) { + input--; + return (float) Math.pow(input, 3) + 1f; + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutCubic = new EasingFunction() { + public float getInterpolation(float input) { + input *= 2f; + if (input < 1f) { + return 0.5f * (float) Math.pow(input, 3); + } + input -= 2f; + return 0.5f * ((float) Math.pow(input, 3) + 2f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInQuart = new EasingFunction() { + + public float getInterpolation(float input) { + return (float) Math.pow(input, 4); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutQuart = new EasingFunction() { + public float getInterpolation(float input) { + input--; + return -((float) Math.pow(input, 4) - 1f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutQuart = new EasingFunction() { + public float getInterpolation(float input) { + input *= 2f; + if (input < 1f) { + return 0.5f * (float) Math.pow(input, 4); + } + input -= 2f; + return -0.5f * ((float) Math.pow(input, 4) - 2f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInSine = new EasingFunction() { + public float getInterpolation(float input) { + return -(float) Math.cos(input * (Math.PI / 2f)) + 1f; + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutSine = new EasingFunction() { + public float getInterpolation(float input) { + return (float) Math.sin(input * (Math.PI / 2f)); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutSine = new EasingFunction() { + public float getInterpolation(float input) { + return -0.5f * ((float) Math.cos(Math.PI * input) - 1f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInExpo = new EasingFunction() { + public float getInterpolation(float input) { + return (input == 0) ? 0f : (float) Math.pow(2f, 10f * (input - 1f)); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutExpo = new EasingFunction() { + public float getInterpolation(float input) { + return (input == 1f) ? 1f : (-(float) Math.pow(2f, -10f * (input + 1f))); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutExpo = new EasingFunction() { + public float getInterpolation(float input) { + if (input == 0) { + return 0f; + } else if (input == 1f) { + return 1f; + } + + input *= 2f; + if (input < 1f) { + return 0.5f * (float) Math.pow(2f, 10f * (input - 1f)); + } + return 0.5f * (-(float) Math.pow(2f, -10f * --input) + 2f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInCirc = new EasingFunction() { + public float getInterpolation(float input) { + return -((float) Math.sqrt(1f - input * input) - 1f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutCirc = new EasingFunction() { + public float getInterpolation(float input) { + input--; + return (float) Math.sqrt(1f - input * input); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutCirc = new EasingFunction() { + public float getInterpolation(float input) { + input *= 2f; + if (input < 1f) { + return -0.5f * ((float) Math.sqrt(1f - input * input) - 1f); + } + return 0.5f * ((float) Math.sqrt(1f - (input -= 2f) * input) + 1f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInElastic = new EasingFunction() { + public float getInterpolation(float input) { + if (input == 0) { + return 0f; + } else if (input == 1) { + return 1f; + } + + float p = 0.3f; + float s = p / DOUBLE_PI * (float) Math.asin(1f); + return -((float) Math.pow(2f, 10f * (input -= 1f)) + *(float) Math.sin((input - s) * DOUBLE_PI / p)); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutElastic = new EasingFunction() { + public float getInterpolation(float input) { + if (input == 0) { + return 0f; + } else if (input == 1) { + return 1f; + } + + float p = 0.3f; + float s = p / DOUBLE_PI * (float) Math.asin(1f); + return 1f + + (float) Math.pow(2f, -10f * input) + * (float) Math.sin((input - s) * DOUBLE_PI / p); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutElastic = new EasingFunction() { + public float getInterpolation(float input) { + if (input == 0) { + return 0f; + } + + input *= 2f; + if (input == 2) { + return 1f; + } + + float p = 1f / 0.45f; + float s = 0.45f / DOUBLE_PI * (float) Math.asin(1f); + if (input < 1f) { + return -0.5f + * ((float) Math.pow(2f, 10f * (input -= 1f)) + * (float) Math.sin((input * 1f - s) * DOUBLE_PI * p)); + } + return 1f + 0.5f + * (float) Math.pow(2f, -10f * (input -= 1f)) + * (float) Math.sin((input * 1f - s) * DOUBLE_PI * p); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInBack = new EasingFunction() { + public float getInterpolation(float input) { + final float s = 1.70158f; + return input * input * ((s + 1f) * input - s); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutBack = new EasingFunction() { + public float getInterpolation(float input) { + final float s = 1.70158f; + input--; + return (input * input * ((s + 1f) * input + s) + 1f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutBack = new EasingFunction() { + public float getInterpolation(float input) { + float s = 1.70158f; + input *= 2f; + if (input < 1f) { + return 0.5f * (input * input * (((s *= (1.525f)) + 1f) * input - s)); + } + return 0.5f * ((input -= 2f) * input * (((s *= (1.525f)) + 1f) * input + s) + 2f); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInBounce = new EasingFunction() { + public float getInterpolation(float input) { + return 1f - EaseOutBounce.getInterpolation(1f - input); + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseOutBounce = new EasingFunction() { + public float getInterpolation(float input) { + float s = 7.5625f; + if (input < (1f / 2.75f)) { + return s * input * input; + } else if (input < (2f / 2.75f)) { + return s * (input -= (1.5f / 2.75f)) * input + 0.75f; + } else if (input < (2.5f / 2.75f)) { + return s * (input -= (2.25f / 2.75f)) * input + 0.9375f; + } + return s * (input -= (2.625f / 2.75f)) * input + 0.984375f; + } + }; + + @SuppressWarnings("unused") + public static final EasingFunction EaseInOutBounce = new EasingFunction() { + public float getInterpolation(float input) { + if (input < 0.5f) { + return EaseInBounce.getInterpolation(input * 2f) * 0.5f; + } + return EaseOutBounce.getInterpolation(input * 2f - 1f) * 0.5f + 0.5f; + } + }; + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/AbstractBuffer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/AbstractBuffer.java new file mode 100644 index 0000000..958d12a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/AbstractBuffer.java @@ -0,0 +1,91 @@ + +package com.github.mikephil.charting.buffer; + +import java.util.List; + +/** + * Buffer class to boost performance while drawing. Concept: Replace instead of + * recreate. + * + * @author Philipp Jahoda + * @param The data the buffer accepts to be fed with. + */ +public abstract class AbstractBuffer { + + /** index in the buffer */ + protected int index = 0; + + /** float-buffer that holds the data points to draw, order: x,y,x,y,... */ + public final float[] buffer; + + /** animation phase x-axis */ + protected float phaseX = 1f; + + /** animation phase y-axis */ + protected float phaseY = 1f; + + /** indicates from which x-index the visible data begins */ + protected int mFrom = 0; + + /** indicates to which x-index the visible data ranges */ + protected int mTo = 0; + + /** + * Initialization with buffer-size. + * + * @param size + */ + public AbstractBuffer(int size) { + index = 0; + buffer = new float[size]; + } + + /** limits the drawing on the x-axis */ + public void limitFrom(int from) { + if (from < 0) + from = 0; + mFrom = from; + } + + /** limits the drawing on the x-axis */ + public void limitTo(int to) { + if (to < 0) + to = 0; + mTo = to; + } + + /** + * Resets the buffer index to 0 and makes the buffer reusable. + */ + public void reset() { + index = 0; + } + + /** + * Returns the size (length) of the buffer array. + * + * @return + */ + public int size() { + return buffer.length; + } + + /** + * Set the phases used for animations. + * + * @param phaseX + * @param phaseY + */ + public void setPhases(float phaseX, float phaseY) { + this.phaseX = phaseX; + this.phaseY = phaseY; + } + + /** + * Builds up the buffer with the provided data and resets the buffer-index + * after feed-completion. This needs to run FAST. + * + * @param data + */ + public abstract void feed(T data); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/BarBuffer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/BarBuffer.java new file mode 100644 index 0000000..136a231 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/BarBuffer.java @@ -0,0 +1,130 @@ + +package com.github.mikephil.charting.buffer; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; + +public class BarBuffer extends AbstractBuffer { + + protected int mDataSetIndex = 0; + protected int mDataSetCount = 1; + protected boolean mContainsStacks = false; + protected boolean mInverted = false; + + /** width of the bar on the x-axis, in values (not pixels) */ + protected float mBarWidth = 1f; + + public BarBuffer(int size, int dataSetCount, boolean containsStacks) { + super(size); + this.mDataSetCount = dataSetCount; + this.mContainsStacks = containsStacks; + } + + public void setBarWidth(float barWidth) { + this.mBarWidth = barWidth; + } + + public void setDataSet(int index) { + this.mDataSetIndex = index; + } + + public void setInverted(boolean inverted) { + this.mInverted = inverted; + } + + protected void addBar(float left, float top, float right, float bottom) { + + buffer[index++] = left; + buffer[index++] = top; + buffer[index++] = right; + buffer[index++] = bottom; + } + + @Override + public void feed(IBarDataSet data) { + + float size = data.getEntryCount() * phaseX; + float barWidthHalf = mBarWidth / 2f; + + for (int i = 0; i < size; i++) { + + BarEntry e = data.getEntryForIndex(i); + + if(e == null) + continue; + + float x = e.getX(); + float y = e.getY(); + float[] vals = e.getYVals(); + + if (!mContainsStacks || vals == null) { + + float left = x - barWidthHalf; + float right = x + barWidthHalf; + float bottom, top; + + if (mInverted) { + bottom = y >= 0 ? y : 0; + top = y <= 0 ? y : 0; + } else { + top = y >= 0 ? y : 0; + bottom = y <= 0 ? y : 0; + } + + // multiply the height of the rect with the phase + if (top > 0) + top *= phaseY; + else + bottom *= phaseY; + + addBar(left, top, right, bottom); + + } else { + + float posY = 0f; + float negY = -e.getNegativeSum(); + float yStart = 0f; + + // fill the stack + for (int k = 0; k < vals.length; k++) { + + float value = vals[k]; + + if (value == 0.0f && (posY == 0.0f || negY == 0.0f)) { + // Take care of the situation of a 0.0 value, which overlaps a non-zero bar + y = value; + yStart = y; + } else if (value >= 0.0f) { + y = posY; + yStart = posY + value; + posY = yStart; + } else { + y = negY; + yStart = negY + Math.abs(value); + negY += Math.abs(value); + } + + float left = x - barWidthHalf; + float right = x + barWidthHalf; + float bottom, top; + + if (mInverted) { + bottom = y >= yStart ? y : yStart; + top = y <= yStart ? y : yStart; + } else { + top = y >= yStart ? y : yStart; + bottom = y <= yStart ? y : yStart; + } + + // multiply the height of the rect with the phase + top *= phaseY; + bottom *= phaseY; + + addBar(left, top, right, bottom); + } + } + } + + reset(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/HorizontalBarBuffer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/HorizontalBarBuffer.java new file mode 100644 index 0000000..6b63bc3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/HorizontalBarBuffer.java @@ -0,0 +1,94 @@ + +package com.github.mikephil.charting.buffer; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; + +public class HorizontalBarBuffer extends BarBuffer { + + public HorizontalBarBuffer(int size, int dataSetCount, boolean containsStacks) { + super(size, dataSetCount, containsStacks); + } + + @Override + public void feed(IBarDataSet data) { + + float size = data.getEntryCount() * phaseX; + float barWidthHalf = mBarWidth / 2f; + + for (int i = 0; i < size; i++) { + + BarEntry e = data.getEntryForIndex(i); + + if(e == null) + continue; + + float x = e.getX(); + float y = e.getY(); + float[] vals = e.getYVals(); + + if (!mContainsStacks || vals == null) { + + float bottom = x - barWidthHalf; + float top = x + barWidthHalf; + float left, right; + if (mInverted) { + left = y >= 0 ? y : 0; + right = y <= 0 ? y : 0; + } else { + right = y >= 0 ? y : 0; + left = y <= 0 ? y : 0; + } + + // multiply the height of the rect with the phase + if (right > 0) + right *= phaseY; + else + left *= phaseY; + + addBar(left, top, right, bottom); + + } else { + + float posY = 0f; + float negY = -e.getNegativeSum(); + float yStart = 0f; + + // fill the stack + for (int k = 0; k < vals.length; k++) { + + float value = vals[k]; + + if (value >= 0f) { + y = posY; + yStart = posY + value; + posY = yStart; + } else { + y = negY; + yStart = negY + Math.abs(value); + negY += Math.abs(value); + } + + float bottom = x - barWidthHalf; + float top = x + barWidthHalf; + float left, right; + if (mInverted) { + left = y >= yStart ? y : yStart; + right = y <= yStart ? y : yStart; + } else { + right = y >= yStart ? y : yStart; + left = y <= yStart ? y : yStart; + } + + // multiply the height of the rect with the phase + right *= phaseY; + left *= phaseY; + + addBar(left, top, right, bottom); + } + } + } + + reset(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarChart.java new file mode 100644 index 0000000..2ba15c9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarChart.java @@ -0,0 +1,258 @@ +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.highlight.BarHighlighter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.renderer.BarChartRenderer; + +/** + * Chart that draws bars. + * + * @author Philipp Jahoda + */ +public class BarChart extends BarLineChartBase implements BarDataProvider { + + /** + * flag that indicates whether the highlight should be full-bar oriented, or single-value? + */ + protected boolean mHighlightFullBarEnabled = false; + + /** + * if set to true, all values are drawn above their bars, instead of below their top + */ + private boolean mDrawValueAboveBar = true; + + /** + * if set to true, a grey area is drawn behind each bar that indicates the maximum value + */ + private boolean mDrawBarShadow = false; + + private boolean mFitBars = false; + + public BarChart(Context context) { + super(context); + } + + public BarChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BarChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new BarChartRenderer(this, mAnimator, mViewPortHandler); + + setHighlighter(new BarHighlighter(this)); + + getXAxis().setSpaceMin(0.5f); + getXAxis().setSpaceMax(0.5f); + } + + @Override + protected void calcMinMax() { + + if (mFitBars) { + mXAxis.calculate(mData.getXMin() - mData.getBarWidth() / 2f, mData.getXMax() + mData.getBarWidth() / 2f); + } else { + mXAxis.calculate(mData.getXMin(), mData.getXMax()); + } + + // calculate axis range (min / max) according to provided data + mAxisLeft.calculate(mData.getYMin(YAxis.AxisDependency.LEFT), mData.getYMax(YAxis.AxisDependency.LEFT)); + mAxisRight.calculate(mData.getYMin(YAxis.AxisDependency.RIGHT), mData.getYMax(YAxis.AxisDependency + .RIGHT)); + } + + /** + * Returns the Highlight object (contains x-index and DataSet index) of the selected value at the given touch + * point + * inside the BarChart. + * + * @param x + * @param y + * @return + */ + @Override + public Highlight getHighlightByTouchPoint(float x, float y) { + + if (mData == null) { + Log.e(LOG_TAG, "Can't select by touch. No data set."); + return null; + } else { + Highlight h = getHighlighter().getHighlight(x, y); + if (h == null || !isHighlightFullBarEnabled()) return h; + + // For isHighlightFullBarEnabled, remove stackIndex + return new Highlight(h.getX(), h.getY(), + h.getXPx(), h.getYPx(), + h.getDataSetIndex(), -1, h.getAxis()); + } + } + + /** + * Returns the bounding box of the specified Entry in the specified DataSet. Returns null if the Entry could not be + * found in the charts data. Performance-intensive code should use void getBarBounds(BarEntry, RectF) instead. + * + * @param e + * @return + */ + public RectF getBarBounds(BarEntry e) { + + RectF bounds = new RectF(); + getBarBounds(e, bounds); + + return bounds; + } + + /** + * The passed outputRect will be assigned the values of the bounding box of the specified Entry in the specified DataSet. + * The rect will be assigned Float.MIN_VALUE in all locations if the Entry could not be found in the charts data. + * + * @param e + * @return + */ + public void getBarBounds(BarEntry e, RectF outputRect) { + + RectF bounds = outputRect; + + IBarDataSet set = mData.getDataSetForEntry(e); + + if (set == null) { + bounds.set(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); + return; + } + + float y = e.getY(); + float x = e.getX(); + + float barWidth = mData.getBarWidth(); + + float left = x - barWidth / 2f; + float right = x + barWidth / 2f; + float top = y >= 0 ? y : 0; + float bottom = y <= 0 ? y : 0; + + bounds.set(left, top, right, bottom); + + getTransformer(set.getAxisDependency()).rectValueToPixel(outputRect); + } + + /** + * If set to true, all values are drawn above their bars, instead of below their top. + * + * @param enabled + */ + public void setDrawValueAboveBar(boolean enabled) { + mDrawValueAboveBar = enabled; + } + + /** + * returns true if drawing values above bars is enabled, false if not + * + * @return + */ + public boolean isDrawValueAboveBarEnabled() { + return mDrawValueAboveBar; + } + + /** + * If set to true, a grey area is drawn behind each bar that indicates the maximum value. Enabling his will reduce + * performance by about 50%. + * + * @param enabled + */ + public void setDrawBarShadow(boolean enabled) { + mDrawBarShadow = enabled; + } + + /** + * returns true if drawing shadows (maxvalue) for each bar is enabled, false if not + * + * @return + */ + public boolean isDrawBarShadowEnabled() { + return mDrawBarShadow; + } + + /** + * Set this to true to make the highlight operation full-bar oriented, false to make it highlight single values (relevant + * only for stacked). If enabled, highlighting operations will highlight the whole bar, even if only a single stack entry + * was tapped. + * Default: false + * + * @param enabled + */ + public void setHighlightFullBarEnabled(boolean enabled) { + mHighlightFullBarEnabled = enabled; + } + + /** + * @return true the highlight operation is be full-bar oriented, false if single-value + */ + @Override + public boolean isHighlightFullBarEnabled() { + return mHighlightFullBarEnabled; + } + + /** + * Highlights the value at the given x-value in the given DataSet. Provide + * -1 as the dataSetIndex to undo all highlighting. + * + * @param x + * @param dataSetIndex + * @param stackIndex the index inside the stack - only relevant for stacked entries + */ + public void highlightValue(float x, int dataSetIndex, int stackIndex) { + highlightValue(new Highlight(x, dataSetIndex, stackIndex), false); + } + + @Override + public BarData getBarData() { + return mData; + } + + /** + * Adds half of the bar width to each side of the x-axis range in order to allow the bars of the barchart to be + * fully displayed. + * Default: false + * + * @param enabled + */ + public void setFitBars(boolean enabled) { + mFitBars = enabled; + } + + /** + * Groups all BarDataSet objects this data object holds together by modifying the x-value of their entries. + * Previously set x-values of entries will be overwritten. Leaves space between bars and groups as specified + * by the parameters. + * Calls notifyDataSetChanged() afterwards. + * + * @param fromX the starting point on the x-axis where the grouping should begin + * @param groupSpace the space between groups of bars in values (not pixels) e.g. 0.8f for bar width 1f + * @param barSpace the space between individual bars in values (not pixels) e.g. 0.1f for bar width 1f + */ + public void groupBars(float fromX, float groupSpace, float barSpace) { + + if (getBarData() == null) { + throw new RuntimeException("You need to set data for the chart before grouping bars."); + } else { + getBarData().groupBars(fromX, groupSpace, barSpace); + notifyDataSetChanged(); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarLineChartBase.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarLineChartBase.java new file mode 100644 index 0000000..0926dff --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarLineChartBase.java @@ -0,0 +1,1674 @@ + +package com.github.mikephil.charting.charts; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.github.mikephil.charting.components.XAxis.XAxisPosition; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.ChartHighlighter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; +import com.github.mikephil.charting.jobs.AnimatedMoveViewJob; +import com.github.mikephil.charting.jobs.AnimatedZoomJob; +import com.github.mikephil.charting.jobs.MoveViewJob; +import com.github.mikephil.charting.jobs.ZoomJob; +import com.github.mikephil.charting.listener.BarLineChartTouchListener; +import com.github.mikephil.charting.listener.OnDrawListener; +import com.github.mikephil.charting.renderer.XAxisRenderer; +import com.github.mikephil.charting.renderer.YAxisRenderer; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; + +/** + * Base-class of LineChart, BarChart, ScatterChart and CandleStickChart. + * + * @author Philipp Jahoda + */ +@SuppressLint("RtlHardcoded") +public abstract class BarLineChartBase>> + extends Chart implements BarLineScatterCandleBubbleDataProvider { + + /** + * the maximum number of entries to which values will be drawn + * (entry numbers greater than this value will cause value-labels to disappear) + */ + protected int mMaxVisibleCount = 100; + + /** + * flag that indicates if auto scaling on the y axis is enabled + */ + protected boolean mAutoScaleMinMaxEnabled = false; + + /** + * flag that indicates if pinch-zoom is enabled. if true, both x and y axis + * can be scaled with 2 fingers, if false, x and y axis can be scaled + * separately + */ + protected boolean mPinchZoomEnabled = false; + + /** + * flag that indicates if double tap zoom is enabled or not + */ + protected boolean mDoubleTapToZoomEnabled = true; + + /** + * flag that indicates if highlighting per dragging over a fully zoomed out + * chart is enabled + */ + protected boolean mHighlightPerDragEnabled = true; + + /** + * if true, dragging is enabled for the chart + */ + private boolean mDragXEnabled = true; + private boolean mDragYEnabled = true; + + private boolean mScaleXEnabled = true; + private boolean mScaleYEnabled = true; + + /** + * paint object for the (by default) lightgrey background of the grid + */ + protected Paint mGridBackgroundPaint; + + protected Paint mBorderPaint; + + /** + * flag indicating if the grid background should be drawn or not + */ + protected boolean mDrawGridBackground = false; + + protected boolean mDrawBorders = false; + + protected boolean mClipValuesToContent = false; + + protected boolean mClipDataToContent = true; + + /** + * Sets the minimum offset (padding) around the chart, defaults to 15 + */ + protected float mMinOffset = 15.f; + + /** + * flag indicating if the chart should stay at the same position after a rotation. Default is false. + */ + protected boolean mKeepPositionOnRotation = false; + + /** + * the listener for user drawing on the chart + */ + protected OnDrawListener mDrawListener; + + /** + * the object representing the labels on the left y-axis + */ + protected YAxis mAxisLeft; + + /** + * the object representing the labels on the right y-axis + */ + protected YAxis mAxisRight; + + protected YAxisRenderer mAxisRendererLeft; + protected YAxisRenderer mAxisRendererRight; + + protected Transformer mLeftAxisTransformer; + protected Transformer mRightAxisTransformer; + + protected XAxisRenderer mXAxisRenderer; + + // /** the approximator object used for data filtering */ + // private Approximator mApproximator; + + public BarLineChartBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public BarLineChartBase(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BarLineChartBase(Context context) { + super(context); + } + + @Override + protected void init() { + super.init(); + + mAxisLeft = new YAxis(AxisDependency.LEFT); + mAxisRight = new YAxis(AxisDependency.RIGHT); + + mLeftAxisTransformer = new Transformer(mViewPortHandler); + mRightAxisTransformer = new Transformer(mViewPortHandler); + + mAxisRendererLeft = new YAxisRenderer(mViewPortHandler, mAxisLeft, mLeftAxisTransformer); + mAxisRendererRight = new YAxisRenderer(mViewPortHandler, mAxisRight, mRightAxisTransformer); + + mXAxisRenderer = new XAxisRenderer(mViewPortHandler, mXAxis, mLeftAxisTransformer); + + setHighlighter(new ChartHighlighter(this)); + + mChartTouchListener = new BarLineChartTouchListener(this, mViewPortHandler.getMatrixTouch(), 3f); + + mGridBackgroundPaint = new Paint(); + mGridBackgroundPaint.setStyle(Style.FILL); + // mGridBackgroundPaint.setColor(Color.WHITE); + mGridBackgroundPaint.setColor(Color.rgb(240, 240, 240)); // light + // grey + + mBorderPaint = new Paint(); + mBorderPaint.setStyle(Style.STROKE); + mBorderPaint.setColor(Color.BLACK); + mBorderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + } + + // for performance tracking + private long totalTime = 0; + private long drawCycles = 0; + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mData == null) + return; + + long starttime = System.currentTimeMillis(); + + // execute all drawing commands + drawGridBackground(canvas); + + if (mAutoScaleMinMaxEnabled) { + autoScale(); + } + + if (mAxisLeft.isEnabled()) + mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted()); + + if (mAxisRight.isEnabled()) + mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted()); + + if (mXAxis.isEnabled()) + mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false); + + mXAxisRenderer.renderAxisLine(canvas); + mAxisRendererLeft.renderAxisLine(canvas); + mAxisRendererRight.renderAxisLine(canvas); + + if (mXAxis.isDrawGridLinesBehindDataEnabled()) + mXAxisRenderer.renderGridLines(canvas); + + if (mAxisLeft.isDrawGridLinesBehindDataEnabled()) + mAxisRendererLeft.renderGridLines(canvas); + + if (mAxisRight.isDrawGridLinesBehindDataEnabled()) + mAxisRendererRight.renderGridLines(canvas); + + if (mXAxis.isEnabled() && mXAxis.isDrawLimitLinesBehindDataEnabled()) + mXAxisRenderer.renderLimitLines(canvas); + + if (mAxisLeft.isEnabled() && mAxisLeft.isDrawLimitLinesBehindDataEnabled()) + mAxisRendererLeft.renderLimitLines(canvas); + + if (mAxisRight.isEnabled() && mAxisRight.isDrawLimitLinesBehindDataEnabled()) + mAxisRendererRight.renderLimitLines(canvas); + + int clipRestoreCount = canvas.save(); + + if (isClipDataToContentEnabled()) { + // make sure the data cannot be drawn outside the content-rect + canvas.clipRect(mViewPortHandler.getContentRect()); + } + + mRenderer.drawData(canvas); + + if (!mXAxis.isDrawGridLinesBehindDataEnabled()) + mXAxisRenderer.renderGridLines(canvas); + + if (!mAxisLeft.isDrawGridLinesBehindDataEnabled()) + mAxisRendererLeft.renderGridLines(canvas); + + if (!mAxisRight.isDrawGridLinesBehindDataEnabled()) + mAxisRendererRight.renderGridLines(canvas); + + // if highlighting is enabled + if (valuesToHighlight()) + mRenderer.drawHighlighted(canvas, mIndicesToHighlight); + + // Removes clipping rectangle + canvas.restoreToCount(clipRestoreCount); + + mRenderer.drawExtras(canvas); + + if (mXAxis.isEnabled() && !mXAxis.isDrawLimitLinesBehindDataEnabled()) + mXAxisRenderer.renderLimitLines(canvas); + + if (mAxisLeft.isEnabled() && !mAxisLeft.isDrawLimitLinesBehindDataEnabled()) + mAxisRendererLeft.renderLimitLines(canvas); + + if (mAxisRight.isEnabled() && !mAxisRight.isDrawLimitLinesBehindDataEnabled()) + mAxisRendererRight.renderLimitLines(canvas); + + mXAxisRenderer.renderAxisLabels(canvas); + mAxisRendererLeft.renderAxisLabels(canvas); + mAxisRendererRight.renderAxisLabels(canvas); + + if (isClipValuesToContentEnabled()) { + clipRestoreCount = canvas.save(); + canvas.clipRect(mViewPortHandler.getContentRect()); + + mRenderer.drawValues(canvas); + + canvas.restoreToCount(clipRestoreCount); + } else { + mRenderer.drawValues(canvas); + } + + mLegendRenderer.renderLegend(canvas); + + drawDescription(canvas); + + drawMarkers(canvas); + + if (mLogEnabled) { + long drawtime = (System.currentTimeMillis() - starttime); + totalTime += drawtime; + drawCycles += 1; + long average = totalTime / drawCycles; + Log.i(LOG_TAG, "Drawtime: " + drawtime + " ms, average: " + average + " ms, cycles: " + + drawCycles); + } + } + + /** + * RESET PERFORMANCE TRACKING FIELDS + */ + public void resetTracking() { + totalTime = 0; + drawCycles = 0; + } + + protected void prepareValuePxMatrix() { + + if (mLogEnabled) + Log.i(LOG_TAG, "Preparing Value-Px Matrix, xmin: " + mXAxis.mAxisMinimum + ", xmax: " + + mXAxis.mAxisMaximum + ", xdelta: " + mXAxis.mAxisRange); + + mRightAxisTransformer.prepareMatrixValuePx(mXAxis.mAxisMinimum, + mXAxis.mAxisRange, + mAxisRight.mAxisRange, + mAxisRight.mAxisMinimum); + mLeftAxisTransformer.prepareMatrixValuePx(mXAxis.mAxisMinimum, + mXAxis.mAxisRange, + mAxisLeft.mAxisRange, + mAxisLeft.mAxisMinimum); + } + + protected void prepareOffsetMatrix() { + + mRightAxisTransformer.prepareMatrixOffset(mAxisRight.isInverted()); + mLeftAxisTransformer.prepareMatrixOffset(mAxisLeft.isInverted()); + } + + @Override + public void notifyDataSetChanged() { + + if (mData == null) { + if (mLogEnabled) + Log.i(LOG_TAG, "Preparing... DATA NOT SET."); + return; + } else { + if (mLogEnabled) + Log.i(LOG_TAG, "Preparing..."); + } + + if (mRenderer != null) + mRenderer.initBuffers(); + + calcMinMax(); + + mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted()); + mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted()); + mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false); + + if (mLegend != null) + mLegendRenderer.computeLegend(mData); + + calculateOffsets(); + } + + /** + * Performs auto scaling of the axis by recalculating the minimum and maximum y-values based on the entries currently in view. + */ + protected void autoScale() { + + final float fromX = getLowestVisibleX(); + final float toX = getHighestVisibleX(); + + mData.calcMinMaxY(fromX, toX); + + mXAxis.calculate(mData.getXMin(), mData.getXMax()); + + // calculate axis range (min / max) according to provided data + + if (mAxisLeft.isEnabled()) + mAxisLeft.calculate(mData.getYMin(AxisDependency.LEFT), + mData.getYMax(AxisDependency.LEFT)); + + if (mAxisRight.isEnabled()) + mAxisRight.calculate(mData.getYMin(AxisDependency.RIGHT), + mData.getYMax(AxisDependency.RIGHT)); + + calculateOffsets(); + } + + @Override + protected void calcMinMax() { + + mXAxis.calculate(mData.getXMin(), mData.getXMax()); + + // calculate axis range (min / max) according to provided data + mAxisLeft.calculate(mData.getYMin(AxisDependency.LEFT), mData.getYMax(AxisDependency.LEFT)); + mAxisRight.calculate(mData.getYMin(AxisDependency.RIGHT), mData.getYMax(AxisDependency + .RIGHT)); + } + + protected void calculateLegendOffsets(RectF offsets) { + + offsets.left = 0.f; + offsets.right = 0.f; + offsets.top = 0.f; + offsets.bottom = 0.f; + + if (mLegend == null || !mLegend.isEnabled() || mLegend.isDrawInsideEnabled()) + return; + + switch (mLegend.getOrientation()) { + case VERTICAL: + + switch (mLegend.getHorizontalAlignment()) { + case LEFT: + offsets.left += Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()) + + mLegend.getXOffset(); + break; + + case RIGHT: + offsets.right += Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()) + + mLegend.getXOffset(); + break; + + case CENTER: + + switch (mLegend.getVerticalAlignment()) { + case TOP: + offsets.top += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + break; + + case BOTTOM: + offsets.bottom += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + break; + + default: + break; + } + } + + break; + + case HORIZONTAL: + + switch (mLegend.getVerticalAlignment()) { + case TOP: + offsets.top += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + + + break; + + case BOTTOM: + offsets.bottom += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + + + break; + + default: + break; + } + break; + } + } + + private RectF mOffsetsBuffer = new RectF(); + + @Override + public void calculateOffsets() { + + if (!mCustomViewPortEnabled) { + + float offsetLeft = 0f, offsetRight = 0f, offsetTop = 0f, offsetBottom = 0f; + + calculateLegendOffsets(mOffsetsBuffer); + + offsetLeft += mOffsetsBuffer.left; + offsetTop += mOffsetsBuffer.top; + offsetRight += mOffsetsBuffer.right; + offsetBottom += mOffsetsBuffer.bottom; + + // offsets for y-labels + if (mAxisLeft.needsOffset()) { + offsetLeft += mAxisLeft.getRequiredWidthSpace(mAxisRendererLeft + .getPaintAxisLabels()); + } + + if (mAxisRight.needsOffset()) { + offsetRight += mAxisRight.getRequiredWidthSpace(mAxisRendererRight + .getPaintAxisLabels()); + } + + if (mXAxis.isEnabled() && mXAxis.isDrawLabelsEnabled()) { + + float xLabelHeight = mXAxis.mLabelRotatedHeight + mXAxis.getYOffset(); + + // offsets for x-labels + if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { + + offsetBottom += xLabelHeight; + + } else if (mXAxis.getPosition() == XAxisPosition.TOP) { + + offsetTop += xLabelHeight; + + } else if (mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + + offsetBottom += xLabelHeight; + offsetTop += xLabelHeight; + } + } + + offsetTop += getExtraTopOffset(); + offsetRight += getExtraRightOffset(); + offsetBottom += getExtraBottomOffset(); + offsetLeft += getExtraLeftOffset(); + + float minOffset = Utils.convertDpToPixel(mMinOffset); + + mViewPortHandler.restrainViewPort( + Math.max(minOffset, offsetLeft), + Math.max(minOffset, offsetTop), + Math.max(minOffset, offsetRight), + Math.max(minOffset, offsetBottom)); + + if (mLogEnabled) { + Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop + + ", offsetRight: " + offsetRight + ", offsetBottom: " + offsetBottom); + Log.i(LOG_TAG, "Content: " + mViewPortHandler.getContentRect().toString()); + } + } + + prepareOffsetMatrix(); + prepareValuePxMatrix(); + } + + /** + * draws the grid background + */ + protected void drawGridBackground(Canvas c) { + + if (mDrawGridBackground) { + + // draw the grid background + c.drawRect(mViewPortHandler.getContentRect(), mGridBackgroundPaint); + } + + if (mDrawBorders) { + c.drawRect(mViewPortHandler.getContentRect(), mBorderPaint); + } + } + + /** + * Returns the Transformer class that contains all matrices and is + * responsible for transforming values into pixels on the screen and + * backwards. + * + * @return + */ + public Transformer getTransformer(AxisDependency which) { + if (which == AxisDependency.LEFT) + return mLeftAxisTransformer; + else + return mRightAxisTransformer; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + if (mChartTouchListener == null || mData == null) + return false; + + // check if touch gestures are enabled + if (!mTouchEnabled) + return false; + else + return mChartTouchListener.onTouch(this, event); + } + + @Override + public void computeScroll() { + + if (mChartTouchListener instanceof BarLineChartTouchListener) + ((BarLineChartTouchListener) mChartTouchListener).computeScroll(); + } + + /** + * ################ ################ ################ ################ + */ + /** + * CODE BELOW THIS RELATED TO SCALING AND GESTURES AND MODIFICATION OF THE + * VIEWPORT + */ + + protected Matrix mZoomMatrixBuffer = new Matrix(); + + /** + * Zooms in by 1.4f, into the charts center. + */ + public void zoomIn() { + + MPPointF center = mViewPortHandler.getContentCenter(); + + mViewPortHandler.zoomIn(center.x, -center.y, mZoomMatrixBuffer); + mViewPortHandler.refresh(mZoomMatrixBuffer, this, false); + + MPPointF.recycleInstance(center); + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + calculateOffsets(); + postInvalidate(); + } + + /** + * Zooms out by 0.7f, from the charts center. + */ + public void zoomOut() { + + MPPointF center = mViewPortHandler.getContentCenter(); + + mViewPortHandler.zoomOut(center.x, -center.y, mZoomMatrixBuffer); + mViewPortHandler.refresh(mZoomMatrixBuffer, this, false); + + MPPointF.recycleInstance(center); + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + calculateOffsets(); + postInvalidate(); + } + + /** + * Zooms out to original size. + */ + public void resetZoom() { + + mViewPortHandler.resetZoom(mZoomMatrixBuffer); + mViewPortHandler.refresh(mZoomMatrixBuffer, this, false); + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + calculateOffsets(); + postInvalidate(); + } + + /** + * Zooms in or out by the given scale factor. x and y are the coordinates + * (in pixels) of the zoom center. + * + * @param scaleX if < 1f --> zoom out, if > 1f --> zoom in + * @param scaleY if < 1f --> zoom out, if > 1f --> zoom in + * @param x + * @param y + */ + public void zoom(float scaleX, float scaleY, float x, float y) { + + mViewPortHandler.zoom(scaleX, scaleY, x, -y, mZoomMatrixBuffer); + mViewPortHandler.refresh(mZoomMatrixBuffer, this, false); + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + calculateOffsets(); + postInvalidate(); + } + + /** + * Zooms in or out by the given scale factor. + * x and y are the values (NOT PIXELS) of the zoom center.. + * + * @param scaleX + * @param scaleY + * @param xValue + * @param yValue + * @param axis the axis relative to which the zoom should take place + */ + public void zoom(float scaleX, float scaleY, float xValue, float yValue, AxisDependency axis) { + + Runnable job = ZoomJob.getInstance(mViewPortHandler, scaleX, scaleY, xValue, yValue, getTransformer(axis), axis, this); + addViewportJob(job); + } + + /** + * Zooms to the center of the chart with the given scale factor. + * + * @param scaleX + * @param scaleY + */ + public void zoomToCenter(float scaleX, float scaleY) { + + MPPointF center = getCenterOffsets(); + + Matrix save = mZoomMatrixBuffer; + mViewPortHandler.zoom(scaleX, scaleY, center.x, -center.y, save); + mViewPortHandler.refresh(save, this, false); + } + + /** + * Zooms by the specified scale factor to the specified values on the specified axis. + * + * @param scaleX + * @param scaleY + * @param xValue + * @param yValue + * @param axis + * @param duration + */ + @TargetApi(11) + public void zoomAndCenterAnimated(float scaleX, float scaleY, float xValue, float yValue, AxisDependency axis, + long duration) { + + MPPointD origin = getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), axis); + + Runnable job = AnimatedZoomJob.getInstance(mViewPortHandler, this, getTransformer(axis), getAxis(axis), mXAxis + .mAxisRange, scaleX, scaleY, mViewPortHandler.getScaleX(), mViewPortHandler.getScaleY(), + xValue, yValue, (float) origin.x, (float) origin.y, duration); + addViewportJob(job); + + MPPointD.recycleInstance(origin); + } + + protected Matrix mFitScreenMatrixBuffer = new Matrix(); + + /** + * Resets all zooming and dragging and makes the chart fit exactly it's + * bounds. + */ + public void fitScreen() { + Matrix save = mFitScreenMatrixBuffer; + mViewPortHandler.fitScreen(save); + mViewPortHandler.refresh(save, this, false); + + calculateOffsets(); + postInvalidate(); + } + + /** + * Sets the minimum scale factor value to which can be zoomed out. 1f = + * fitScreen + * + * @param scaleX + * @param scaleY + */ + public void setScaleMinima(float scaleX, float scaleY) { + mViewPortHandler.setMinimumScaleX(scaleX); + mViewPortHandler.setMinimumScaleY(scaleY); + } + + /** + * Sets the size of the area (range on the x-axis) that should be maximum + * visible at once (no further zooming out allowed). If this is e.g. set to + * 10, no more than a range of 10 on the x-axis can be viewed at once without + * scrolling. + * + * @param maxXRange The maximum visible range of x-values. + */ + public void setVisibleXRangeMaximum(float maxXRange) { + float xScale = mXAxis.mAxisRange / (maxXRange); + mViewPortHandler.setMinimumScaleX(xScale); + } + + /** + * Sets the size of the area (range on the x-axis) that should be minimum + * visible at once (no further zooming in allowed). If this is e.g. set to + * 10, no less than a range of 10 on the x-axis can be viewed at once without + * scrolling. + * + * @param minXRange The minimum visible range of x-values. + */ + public void setVisibleXRangeMinimum(float minXRange) { + float xScale = mXAxis.mAxisRange / (minXRange); + mViewPortHandler.setMaximumScaleX(xScale); + } + + /** + * Limits the maximum and minimum x range that can be visible by pinching and zooming. e.g. minRange=10, maxRange=100 the + * smallest range to be displayed at once is 10, and no more than a range of 100 values can be viewed at once without + * scrolling + * + * @param minXRange + * @param maxXRange + */ + public void setVisibleXRange(float minXRange, float maxXRange) { + float minScale = mXAxis.mAxisRange / minXRange; + float maxScale = mXAxis.mAxisRange / maxXRange; + mViewPortHandler.setMinMaxScaleX(minScale, maxScale); + } + + /** + * Sets the size of the area (range on the y-axis) that should be maximum + * visible at once. + * + * @param maxYRange the maximum visible range on the y-axis + * @param axis the axis for which this limit should apply + */ + public void setVisibleYRangeMaximum(float maxYRange, AxisDependency axis) { + float yScale = getAxisRange(axis) / maxYRange; + mViewPortHandler.setMinimumScaleY(yScale); + } + + /** + * Sets the size of the area (range on the y-axis) that should be minimum visible at once, no further zooming in possible. + * + * @param minYRange + * @param axis the axis for which this limit should apply + */ + public void setVisibleYRangeMinimum(float minYRange, AxisDependency axis) { + float yScale = getAxisRange(axis) / minYRange; + mViewPortHandler.setMaximumScaleY(yScale); + } + + /** + * Limits the maximum and minimum y range that can be visible by pinching and zooming. + * + * @param minYRange + * @param maxYRange + * @param axis + */ + public void setVisibleYRange(float minYRange, float maxYRange, AxisDependency axis) { + float minScale = getAxisRange(axis) / minYRange; + float maxScale = getAxisRange(axis) / maxYRange; + mViewPortHandler.setMinMaxScaleY(minScale, maxScale); + } + + + /** + * Moves the left side of the current viewport to the specified x-position. + * This also refreshes the chart by calling invalidate(). + * + * @param xValue + */ + public void moveViewToX(float xValue) { + + Runnable job = MoveViewJob.getInstance(mViewPortHandler, xValue, 0f, + getTransformer(AxisDependency.LEFT), this); + + addViewportJob(job); + } + + /** + * This will move the left side of the current viewport to the specified + * x-value on the x-axis, and center the viewport to the specified y value on the y-axis. + * This also refreshes the chart by calling invalidate(). + * + * @param xValue + * @param yValue + * @param axis - which axis should be used as a reference for the y-axis + */ + public void moveViewTo(float xValue, float yValue, AxisDependency axis) { + + float yInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + + Runnable job = MoveViewJob.getInstance(mViewPortHandler, xValue, yValue + yInView / 2f, + getTransformer(axis), this); + + addViewportJob(job); + } + + /** + * This will move the left side of the current viewport to the specified x-value + * and center the viewport to the y value animated. + * This also refreshes the chart by calling invalidate(). + * + * @param xValue + * @param yValue + * @param axis + * @param duration the duration of the animation in milliseconds + */ + @TargetApi(11) + public void moveViewToAnimated(float xValue, float yValue, AxisDependency axis, long duration) { + + MPPointD bounds = getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), axis); + + float yInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + + Runnable job = AnimatedMoveViewJob.getInstance(mViewPortHandler, xValue, yValue + yInView / 2f, + getTransformer(axis), this, (float) bounds.x, (float) bounds.y, duration); + + addViewportJob(job); + + MPPointD.recycleInstance(bounds); + } + + /** + * Centers the viewport to the specified y value on the y-axis. + * This also refreshes the chart by calling invalidate(). + * + * @param yValue + * @param axis - which axis should be used as a reference for the y-axis + */ + public void centerViewToY(float yValue, AxisDependency axis) { + + float valsInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + + Runnable job = MoveViewJob.getInstance(mViewPortHandler, 0f, yValue + valsInView / 2f, + getTransformer(axis), this); + + addViewportJob(job); + } + + /** + * This will move the center of the current viewport to the specified + * x and y value. + * This also refreshes the chart by calling invalidate(). + * + * @param xValue + * @param yValue + * @param axis - which axis should be used as a reference for the y axis + */ + public void centerViewTo(float xValue, float yValue, AxisDependency axis) { + + float yInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + float xInView = getXAxis().mAxisRange / mViewPortHandler.getScaleX(); + + Runnable job = MoveViewJob.getInstance(mViewPortHandler, + xValue - xInView / 2f, yValue + yInView / 2f, + getTransformer(axis), this); + + addViewportJob(job); + } + + /** + * This will move the center of the current viewport to the specified + * x and y value animated. + * + * @param xValue + * @param yValue + * @param axis + * @param duration the duration of the animation in milliseconds + */ + @TargetApi(11) + public void centerViewToAnimated(float xValue, float yValue, AxisDependency axis, long duration) { + + MPPointD bounds = getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), axis); + + float yInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + float xInView = getXAxis().mAxisRange / mViewPortHandler.getScaleX(); + + Runnable job = AnimatedMoveViewJob.getInstance(mViewPortHandler, + xValue - xInView / 2f, yValue + yInView / 2f, + getTransformer(axis), this, (float) bounds.x, (float) bounds.y, duration); + + addViewportJob(job); + + MPPointD.recycleInstance(bounds); + } + + /** + * flag that indicates if a custom viewport offset has been set + */ + private boolean mCustomViewPortEnabled = false; + + /** + * Sets custom offsets for the current ViewPort (the offsets on the sides of + * the actual chart window). Setting this will prevent the chart from + * automatically calculating it's offsets. Use resetViewPortOffsets() to + * undo this. ONLY USE THIS WHEN YOU KNOW WHAT YOU ARE DOING, else use + * setExtraOffsets(...). + * + * @param left + * @param top + * @param right + * @param bottom + */ + public void setViewPortOffsets(final float left, final float top, + final float right, final float bottom) { + + mCustomViewPortEnabled = true; + post(new Runnable() { + + @Override + public void run() { + + mViewPortHandler.restrainViewPort(left, top, right, bottom); + prepareOffsetMatrix(); + prepareValuePxMatrix(); + } + }); + } + + /** + * Resets all custom offsets set via setViewPortOffsets(...) method. Allows + * the chart to again calculate all offsets automatically. + */ + public void resetViewPortOffsets() { + mCustomViewPortEnabled = false; + calculateOffsets(); + } + + /** + * ################ ################ ################ ################ + */ + /** CODE BELOW IS GETTERS AND SETTERS */ + + /** + * Returns the range of the specified axis. + * + * @param axis + * @return + */ + protected float getAxisRange(AxisDependency axis) { + if (axis == AxisDependency.LEFT) + return mAxisLeft.mAxisRange; + else + return mAxisRight.mAxisRange; + } + + /** + * Sets the OnDrawListener + * + * @param drawListener + */ + public void setOnDrawListener(OnDrawListener drawListener) { + this.mDrawListener = drawListener; + } + + /** + * Gets the OnDrawListener. May be null. + * + * @return + */ + public OnDrawListener getDrawListener() { + return mDrawListener; + } + + protected float[] mGetPositionBuffer = new float[2]; + + /** + * Returns a recyclable MPPointF instance. + * Returns the position (in pixels) the provided Entry has inside the chart + * view or null, if the provided Entry is null. + * + * @param e + * @return + */ + public MPPointF getPosition(Entry e, AxisDependency axis) { + + if (e == null) + return null; + + mGetPositionBuffer[0] = e.getX(); + mGetPositionBuffer[1] = e.getY(); + + getTransformer(axis).pointValuesToPixel(mGetPositionBuffer); + + return MPPointF.getInstance(mGetPositionBuffer[0], mGetPositionBuffer[1]); + } + + /** + * sets the number of maximum visible drawn values on the chart only active + * when setDrawValues() is enabled + * + * @param count + */ + public void setMaxVisibleValueCount(int count) { + this.mMaxVisibleCount = count; + } + + public int getMaxVisibleCount() { + return mMaxVisibleCount; + } + + /** + * Set this to true to allow highlighting per dragging over the chart + * surface when it is fully zoomed out. Default: true + * + * @param enabled + */ + public void setHighlightPerDragEnabled(boolean enabled) { + mHighlightPerDragEnabled = enabled; + } + + public boolean isHighlightPerDragEnabled() { + return mHighlightPerDragEnabled; + } + + /** + * Sets the color for the background of the chart-drawing area (everything + * behind the grid lines). + * + * @param color + */ + public void setGridBackgroundColor(int color) { + mGridBackgroundPaint.setColor(color); + } + + /** + * Set this to true to enable dragging (moving the chart with the finger) + * for the chart (this does not effect scaling). + * + * @param enabled + */ + public void setDragEnabled(boolean enabled) { + this.mDragXEnabled = enabled; + this.mDragYEnabled = enabled; + } + + /** + * Returns true if dragging is enabled for the chart, false if not. + * + * @return + */ + public boolean isDragEnabled() { + return mDragXEnabled || mDragYEnabled; + } + + /** + * Set this to true to enable dragging on the X axis + * + * @param enabled + */ + public void setDragXEnabled(boolean enabled) { + this.mDragXEnabled = enabled; + } + + /** + * Returns true if dragging on the X axis is enabled for the chart, false if not. + * + * @return + */ + public boolean isDragXEnabled() { + return mDragXEnabled; + } + + /** + * Set this to true to enable dragging on the Y axis + * + * @param enabled + */ + public void setDragYEnabled(boolean enabled) { + this.mDragYEnabled = enabled; + } + + /** + * Returns true if dragging on the Y axis is enabled for the chart, false if not. + * + * @return + */ + public boolean isDragYEnabled() { + return mDragYEnabled; + } + + /** + * Set this to true to enable scaling (zooming in and out by gesture) for + * the chart (this does not effect dragging) on both X- and Y-Axis. + * + * @param enabled + */ + public void setScaleEnabled(boolean enabled) { + this.mScaleXEnabled = enabled; + this.mScaleYEnabled = enabled; + } + + public void setScaleXEnabled(boolean enabled) { + mScaleXEnabled = enabled; + } + + public void setScaleYEnabled(boolean enabled) { + mScaleYEnabled = enabled; + } + + public boolean isScaleXEnabled() { + return mScaleXEnabled; + } + + public boolean isScaleYEnabled() { + return mScaleYEnabled; + } + + /** + * Set this to true to enable zooming in by double-tap on the chart. + * Default: enabled + * + * @param enabled + */ + public void setDoubleTapToZoomEnabled(boolean enabled) { + mDoubleTapToZoomEnabled = enabled; + } + + /** + * Returns true if zooming via double-tap is enabled false if not. + * + * @return + */ + public boolean isDoubleTapToZoomEnabled() { + return mDoubleTapToZoomEnabled; + } + + /** + * set this to true to draw the grid background, false if not + * + * @param enabled + */ + public void setDrawGridBackground(boolean enabled) { + mDrawGridBackground = enabled; + } + + /** + * When enabled, the borders rectangle will be rendered. + * If this is enabled, there is no point drawing the axis-lines of x- and y-axis. + * + * @param enabled + */ + public void setDrawBorders(boolean enabled) { + mDrawBorders = enabled; + } + + /** + * When enabled, the borders rectangle will be rendered. + * If this is enabled, there is no point drawing the axis-lines of x- and y-axis. + * + * @return + */ + public boolean isDrawBordersEnabled() { + return mDrawBorders; + } + + /** + * When enabled, the values will be clipped to contentRect, + * otherwise they can bleed outside the content rect. + * + * @param enabled + */ + public void setClipValuesToContent(boolean enabled) { + mClipValuesToContent = enabled; + } + + /** + * When disabled, the data and/or highlights will not be clipped to contentRect. Disabling this option can + * be useful, when the data lies fully within the content rect, but is drawn in such a way (such as thick lines) + * that there is unwanted clipping. + * + * @param enabled + */ + public void setClipDataToContent(boolean enabled) { + mClipDataToContent = enabled; + } + + /** + * When enabled, the values will be clipped to contentRect, + * otherwise they can bleed outside the content rect. + * + * @return + */ + public boolean isClipValuesToContentEnabled() { + return mClipValuesToContent; + } + + /** + * When disabled, the data and/or highlights will not be clipped to contentRect. Disabling this option can + * be useful, when the data lies fully within the content rect, but is drawn in such a way (such as thick lines) + * that there is unwanted clipping. + * + * @return + */ + public boolean isClipDataToContentEnabled() { + return mClipDataToContent; + } + + /** + * Sets the width of the border lines in dp. + * + * @param width + */ + public void setBorderWidth(float width) { + mBorderPaint.setStrokeWidth(Utils.convertDpToPixel(width)); + } + + /** + * Sets the color of the chart border lines. + * + * @param color + */ + public void setBorderColor(int color) { + mBorderPaint.setColor(color); + } + + /** + * Gets the minimum offset (padding) around the chart, defaults to 15.f + */ + public float getMinOffset() { + return mMinOffset; + } + + /** + * Sets the minimum offset (padding) around the chart, defaults to 15.f + */ + public void setMinOffset(float minOffset) { + mMinOffset = minOffset; + } + + /** + * Returns true if keeping the position on rotation is enabled and false if not. + */ + public boolean isKeepPositionOnRotation() { + return mKeepPositionOnRotation; + } + + /** + * Sets whether the chart should keep its position (zoom / scroll) after a rotation (orientation change) + */ + public void setKeepPositionOnRotation(boolean keepPositionOnRotation) { + mKeepPositionOnRotation = keepPositionOnRotation; + } + + /** + * Returns a recyclable MPPointD instance + * Returns the x and y values in the chart at the given touch point + * (encapsulated in a MPPointD). This method transforms pixel coordinates to + * coordinates / values in the chart. This is the opposite method to + * getPixelForValues(...). + * + * @param x + * @param y + * @return + */ + public MPPointD getValuesByTouchPoint(float x, float y, AxisDependency axis) { + MPPointD result = MPPointD.getInstance(0, 0); + getValuesByTouchPoint(x, y, axis, result); + return result; + } + + public void getValuesByTouchPoint(float x, float y, AxisDependency axis, MPPointD outputPoint) { + getTransformer(axis).getValuesByTouchPoint(x, y, outputPoint); + } + + /** + * Returns a recyclable MPPointD instance + * Transforms the given chart values into pixels. This is the opposite + * method to getValuesByTouchPoint(...). + * + * @param x + * @param y + * @return + */ + public MPPointD getPixelForValues(float x, float y, AxisDependency axis) { + return getTransformer(axis).getPixelForValues(x, y); + } + + /** + * returns the Entry object displayed at the touched position of the chart + * + * @param x + * @param y + * @return + */ + public Entry getEntryByTouchPoint(float x, float y) { + Highlight h = getHighlightByTouchPoint(x, y); + if (h != null) { + return mData.getEntryForHighlight(h); + } + return null; + } + + /** + * returns the DataSet object displayed at the touched position of the chart + * + * @param x + * @param y + * @return + */ + public IBarLineScatterCandleBubbleDataSet getDataSetByTouchPoint(float x, float y) { + Highlight h = getHighlightByTouchPoint(x, y); + if (h != null) { + return mData.getDataSetByIndex(h.getDataSetIndex()); + } + return null; + } + + /** + * buffer for storing lowest visible x point + */ + protected MPPointD posForGetLowestVisibleX = MPPointD.getInstance(0, 0); + + /** + * Returns the lowest x-index (value on the x-axis) that is still visible on + * the chart. + * + * @return + */ + @Override + public float getLowestVisibleX() { + getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), posForGetLowestVisibleX); + float result = (float) Math.max(mXAxis.mAxisMinimum, posForGetLowestVisibleX.x); + return result; + } + + /** + * buffer for storing highest visible x point + */ + protected MPPointD posForGetHighestVisibleX = MPPointD.getInstance(0, 0); + + /** + * Returns the highest x-index (value on the x-axis) that is still visible + * on the chart. + * + * @return + */ + @Override + public float getHighestVisibleX() { + getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), posForGetHighestVisibleX); + float result = (float) Math.min(mXAxis.mAxisMaximum, posForGetHighestVisibleX.x); + return result; + } + + /** + * Returns the range visible on the x-axis. + * + * @return + */ + public float getVisibleXRange() { + return Math.abs(getHighestVisibleX() - getLowestVisibleX()); + } + + /** + * returns the current x-scale factor + */ + public float getScaleX() { + if (mViewPortHandler == null) + return 1f; + else + return mViewPortHandler.getScaleX(); + } + + /** + * returns the current y-scale factor + */ + public float getScaleY() { + if (mViewPortHandler == null) + return 1f; + else + return mViewPortHandler.getScaleY(); + } + + /** + * if the chart is fully zoomed out, return true + * + * @return + */ + public boolean isFullyZoomedOut() { + return mViewPortHandler.isFullyZoomedOut(); + } + + /** + * Returns the left y-axis object. In the horizontal bar-chart, this is the + * top axis. + * + * @return + */ + public YAxis getAxisLeft() { + return mAxisLeft; + } + + /** + * Returns the right y-axis object. In the horizontal bar-chart, this is the + * bottom axis. + * + * @return + */ + public YAxis getAxisRight() { + return mAxisRight; + } + + /** + * Returns the y-axis object to the corresponding AxisDependency. In the + * horizontal bar-chart, LEFT == top, RIGHT == BOTTOM + * + * @param axis + * @return + */ + public YAxis getAxis(AxisDependency axis) { + if (axis == AxisDependency.LEFT) + return mAxisLeft; + else + return mAxisRight; + } + + @Override + public boolean isInverted(AxisDependency axis) { + return getAxis(axis).isInverted(); + } + + /** + * If set to true, both x and y axis can be scaled simultaneously with 2 fingers, if false, + * x and y axis can be scaled separately. default: false + * + * @param enabled + */ + public void setPinchZoom(boolean enabled) { + mPinchZoomEnabled = enabled; + } + + /** + * returns true if pinch-zoom is enabled, false if not + * + * @return + */ + public boolean isPinchZoomEnabled() { + return mPinchZoomEnabled; + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the x-axis. + * + * @param offset + */ + public void setDragOffsetX(float offset) { + mViewPortHandler.setDragOffsetX(offset); + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the y-axis. + * + * @param offset + */ + public void setDragOffsetY(float offset) { + mViewPortHandler.setDragOffsetY(offset); + } + + /** + * Returns true if both drag offsets (x and y) are zero or smaller. + * + * @return + */ + public boolean hasNoDragOffset() { + return mViewPortHandler.hasNoDragOffset(); + } + + public XAxisRenderer getRendererXAxis() { + return mXAxisRenderer; + } + + /** + * Sets a custom XAxisRenderer and overrides the existing (default) one. + * + * @param xAxisRenderer + */ + public void setXAxisRenderer(XAxisRenderer xAxisRenderer) { + mXAxisRenderer = xAxisRenderer; + } + + public YAxisRenderer getRendererLeftYAxis() { + return mAxisRendererLeft; + } + + /** + * Sets a custom axis renderer for the left axis and overwrites the existing one. + * + * @param rendererLeftYAxis + */ + public void setRendererLeftYAxis(YAxisRenderer rendererLeftYAxis) { + mAxisRendererLeft = rendererLeftYAxis; + } + + public YAxisRenderer getRendererRightYAxis() { + return mAxisRendererRight; + } + + /** + * Sets a custom axis renderer for the right acis and overwrites the existing one. + * + * @param rendererRightYAxis + */ + public void setRendererRightYAxis(YAxisRenderer rendererRightYAxis) { + mAxisRendererRight = rendererRightYAxis; + } + + @Override + public float getYChartMax() { + return Math.max(mAxisLeft.mAxisMaximum, mAxisRight.mAxisMaximum); + } + + @Override + public float getYChartMin() { + return Math.min(mAxisLeft.mAxisMinimum, mAxisRight.mAxisMinimum); + } + + /** + * Returns true if either the left or the right or both axes are inverted. + * + * @return + */ + public boolean isAnyAxisInverted() { + if (mAxisLeft.isInverted()) + return true; + if (mAxisRight.isInverted()) + return true; + return false; + } + + /** + * Flag that indicates if auto scaling on the y axis is enabled. This is + * especially interesting for charts displaying financial data. + * + * @param enabled the y axis automatically adjusts to the min and max y + * values of the current x axis range whenever the viewport + * changes + */ + public void setAutoScaleMinMaxEnabled(boolean enabled) { + mAutoScaleMinMaxEnabled = enabled; + } + + /** + * @return true if auto scaling on the y axis is enabled. + * @default false + */ + public boolean isAutoScaleMinMaxEnabled() { + return mAutoScaleMinMaxEnabled; + } + + @Override + public void setPaint(Paint p, int which) { + super.setPaint(p, which); + + switch (which) { + case PAINT_GRID_BACKGROUND: + mGridBackgroundPaint = p; + break; + } + } + + @Override + public Paint getPaint(int which) { + Paint p = super.getPaint(which); + if (p != null) + return p; + + switch (which) { + case PAINT_GRID_BACKGROUND: + return mGridBackgroundPaint; + } + + return null; + } + + protected float[] mOnSizeChangedBuffer = new float[2]; + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + + // Saving current position of chart. + mOnSizeChangedBuffer[0] = mOnSizeChangedBuffer[1] = 0; + + if (mKeepPositionOnRotation) { + mOnSizeChangedBuffer[0] = mViewPortHandler.contentLeft(); + mOnSizeChangedBuffer[1] = mViewPortHandler.contentTop(); + getTransformer(AxisDependency.LEFT).pixelsToValue(mOnSizeChangedBuffer); + } + + //Superclass transforms chart. + super.onSizeChanged(w, h, oldw, oldh); + + if (mKeepPositionOnRotation) { + + //Restoring old position of chart. + getTransformer(AxisDependency.LEFT).pointValuesToPixel(mOnSizeChangedBuffer); + mViewPortHandler.centerViewPort(mOnSizeChangedBuffer, this); + } else { + mViewPortHandler.refresh(mViewPortHandler.getMatrixTouch(), this, true); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BubbleChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BubbleChart.java new file mode 100644 index 0000000..23dac57 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BubbleChart.java @@ -0,0 +1,43 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.util.AttributeSet; + +import com.github.mikephil.charting.data.BubbleData; +import com.github.mikephil.charting.interfaces.dataprovider.BubbleDataProvider; +import com.github.mikephil.charting.renderer.BubbleChartRenderer; + +/** + * The BubbleChart. Draws bubbles. Bubble chart implementation: Copyright 2015 + * Pierre-Marc Airoldi Licensed under Apache License 2.0. In the BubbleChart, it + * is the area of the bubble, not the radius or diameter of the bubble that + * conveys the data. + * + * @author Philipp Jahoda + */ +public class BubbleChart extends BarLineChartBase implements BubbleDataProvider { + + public BubbleChart(Context context) { + super(context); + } + + public BubbleChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BubbleChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new BubbleChartRenderer(this, mAnimator, mViewPortHandler); + } + + public BubbleData getBubbleData() { + return mData; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CandleStickChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CandleStickChart.java new file mode 100644 index 0000000..fa36e35 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CandleStickChart.java @@ -0,0 +1,44 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.util.AttributeSet; + +import com.github.mikephil.charting.data.CandleData; +import com.github.mikephil.charting.interfaces.dataprovider.CandleDataProvider; +import com.github.mikephil.charting.renderer.CandleStickChartRenderer; + +/** + * Financial chart type that draws candle-sticks (OHCL chart). + * + * @author Philipp Jahoda + */ +public class CandleStickChart extends BarLineChartBase implements CandleDataProvider { + + public CandleStickChart(Context context) { + super(context); + } + + public CandleStickChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CandleStickChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new CandleStickChartRenderer(this, mAnimator, mViewPortHandler); + + getXAxis().setSpaceMin(0.5f); + getXAxis().setSpaceMax(0.5f); + } + + @Override + public CandleData getCandleData() { + return mData; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/Chart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/Chart.java new file mode 100644 index 0000000..773389f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/Chart.java @@ -0,0 +1,1835 @@ + +package com.github.mikephil.charting.charts; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.ContentValues; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore.Images; + +import androidx.annotation.RequiresApi; + +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.animation.Easing.EasingFunction; +import com.github.mikephil.charting.components.Description; +import com.github.mikephil.charting.components.IMarker; +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.formatter.DefaultValueFormatter; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.ChartHighlighter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.highlight.IHighlighter; +import com.github.mikephil.charting.interfaces.dataprovider.ChartInterface; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.listener.ChartTouchListener; +import com.github.mikephil.charting.listener.OnChartGestureListener; +import com.github.mikephil.charting.listener.OnChartValueSelectedListener; +import com.github.mikephil.charting.renderer.DataRenderer; +import com.github.mikephil.charting.renderer.LegendRenderer; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + +/** + * Baseclass of all Chart-Views. + * + * @author Philipp Jahoda + */ +public abstract class Chart>> extends + ViewGroup + implements ChartInterface { + + public static final String LOG_TAG = "MPAndroidChart"; + + /** + * flag that indicates if logging is enabled or not + */ + protected boolean mLogEnabled = false; + + /** + * object that holds all data that was originally set for the chart, before + * it was modified or any filtering algorithms had been applied + */ + protected T mData = null; + + /** + * Flag that indicates if highlighting per tap (touch) is enabled + */ + protected boolean mHighLightPerTapEnabled = true; + + /** + * If set to true, chart continues to scroll after touch up + */ + private boolean mDragDecelerationEnabled = true; + + /** + * Deceleration friction coefficient in [0 ; 1] interval, higher values + * indicate that speed will decrease slowly, for example if it set to 0, it + * will stop immediately. 1 is an invalid value, and will be converted to + * 0.999f automatically. + */ + private float mDragDecelerationFrictionCoef = 0.9f; + + /** + * default value-formatter, number of digits depends on provided chart-data + */ + protected DefaultValueFormatter mDefaultValueFormatter = new DefaultValueFormatter(0); + + /** + * paint object used for drawing the description text in the bottom right + * corner of the chart + */ + protected Paint mDescPaint; + + /** + * paint object for drawing the information text when there are no values in + * the chart + */ + protected Paint mInfoPaint; + + /** + * the object representing the labels on the x-axis + */ + protected XAxis mXAxis; + + /** + * if true, touch gestures are enabled on the chart + */ + protected boolean mTouchEnabled = true; + + /** + * the object responsible for representing the description text + */ + protected Description mDescription; + + /** + * the legend object containing all data associated with the legend + */ + protected Legend mLegend; + + /** + * listener that is called when a value on the chart is selected + */ + protected OnChartValueSelectedListener mSelectionListener; + + protected ChartTouchListener mChartTouchListener; + + /** + * text that is displayed when the chart is empty + */ + private String mNoDataText = "No chart data available."; + + /** + * Gesture listener for custom callbacks when making gestures on the chart. + */ + private OnChartGestureListener mGestureListener; + + protected LegendRenderer mLegendRenderer; + + /** + * object responsible for rendering the data + */ + protected DataRenderer mRenderer; + + protected IHighlighter mHighlighter; + + /** + * object that manages the bounds and drawing constraints of the chart + */ + protected ViewPortHandler mViewPortHandler = new ViewPortHandler(); + + /** + * object responsible for animations + */ + protected ChartAnimator mAnimator; + + /** + * Extra offsets to be appended to the viewport + */ + private float mExtraTopOffset = 0.f, + mExtraRightOffset = 0.f, + mExtraBottomOffset = 0.f, + mExtraLeftOffset = 0.f; + + /** + * default constructor for initialization in code + */ + public Chart(Context context) { + super(context); + init(); + } + + /** + * constructor for initialization in xml + */ + public Chart(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + /** + * even more awesome constructor + */ + public Chart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + /** + * initialize all paints and stuff + */ + protected void init() { + + setWillNotDraw(false); + // setLayerType(View.LAYER_TYPE_HARDWARE, null); + + mAnimator = new ChartAnimator(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + // ViewCompat.postInvalidateOnAnimation(Chart.this); + postInvalidate(); + } + }); + + // initialize the utils + Utils.init(getContext()); + mMaxHighlightDistance = Utils.convertDpToPixel(500f); + + mDescription = new Description(); + mLegend = new Legend(); + + mLegendRenderer = new LegendRenderer(mViewPortHandler, mLegend); + + mXAxis = new XAxis(); + + mDescPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mInfoPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mInfoPaint.setColor(Color.rgb(247, 189, 51)); // orange + mInfoPaint.setTextAlign(Align.CENTER); + mInfoPaint.setTextSize(Utils.convertDpToPixel(12f)); + + if (mLogEnabled) + Log.i("", "Chart.init()"); + } + + // public void initWithDummyData() { + // ColorTemplate template = new ColorTemplate(); + // template.addColorsForDataSets(ColorTemplate.COLORFUL_COLORS, + // getContext()); + // + // setColorTemplate(template); + // setDrawYValues(false); + // + // ArrayList xVals = new ArrayList(); + // Calendar calendar = Calendar.getInstance(); + // for (int i = 0; i < 12; i++) { + // xVals.add(calendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, + // Locale.getDefault())); + // } + // + // ArrayList dataSets = new ArrayList(); + // for (int i = 0; i < 3; i++) { + // + // ArrayList yVals = new ArrayList(); + // + // for (int j = 0; j < 12; j++) { + // float val = (float) (Math.random() * 100); + // yVals.add(new Entry(val, j)); + // } + // + // DataSet set = new DataSet(yVals, "DataSet " + i); + // dataSets.add(set); // add the datasets + // } + // // create a data object with the datasets + // ChartData data = new ChartData(xVals, dataSets); + // setData(data); + // invalidate(); + // } + + /** + * Sets a new data object for the chart. The data object contains all values + * and information needed for displaying. + * + * @param data + */ + public void setData(T data) { + + mData = data; + mOffsetsCalculated = false; + + if (data == null) { + return; + } + + // calculate how many digits are needed + setupDefaultFormatter(data.getYMin(), data.getYMax()); + + for (IDataSet set : mData.getDataSets()) { + if (set.needsFormatter() || set.getValueFormatter() == mDefaultValueFormatter) + set.setValueFormatter(mDefaultValueFormatter); + } + + // let the chart know there is new data + notifyDataSetChanged(); + + if (mLogEnabled) + Log.i(LOG_TAG, "Data is set."); + } + + /** + * Clears the chart from all data (sets it to null) and refreshes it (by + * calling invalidate()). + */ + public void clear() { + mData = null; + mOffsetsCalculated = false; + mIndicesToHighlight = null; + mChartTouchListener.setLastHighlighted(null); + invalidate(); + } + + /** + * Removes all DataSets (and thereby Entries) from the chart. Does not set the data object to null. Also refreshes the + * chart by calling invalidate(). + */ + public void clearValues() { + mData.clearValues(); + invalidate(); + } + + /** + * Returns true if the chart is empty (meaning it's data object is either + * null or contains no entries). + * + * @return + */ + public boolean isEmpty() { + + if (mData == null) + return true; + else { + + if (mData.getEntryCount() <= 0) + return true; + else + return false; + } + } + + /** + * Lets the chart know its underlying data has changed and performs all + * necessary recalculations. It is crucial that this method is called + * everytime data is changed dynamically. Not calling this method can lead + * to crashes or unexpected behaviour. + */ + public abstract void notifyDataSetChanged(); + + /** + * Calculates the offsets of the chart to the border depending on the + * position of an eventual legend or depending on the length of the y-axis + * and x-axis labels and their position + */ + protected abstract void calculateOffsets(); + + /** + * Calculates the y-min and y-max value and the y-delta and x-delta value + */ + protected abstract void calcMinMax(); + + /** + * Calculates the required number of digits for the values that might be + * drawn in the chart (if enabled), and creates the default-value-formatter + */ + protected void setupDefaultFormatter(float min, float max) { + + float reference = 0f; + + if (mData == null || mData.getEntryCount() < 2) { + + reference = Math.max(Math.abs(min), Math.abs(max)); + } else { + reference = Math.abs(max - min); + } + + int digits = Utils.getDecimals(reference); + + // setup the formatter with a new number of digits + mDefaultValueFormatter.setup(digits); + } + + /** + * flag that indicates if offsets calculation has already been done or not + */ + private boolean mOffsetsCalculated = false; + + @Override + protected void onDraw(Canvas canvas) { + // super.onDraw(canvas); + + if (mData == null) { + + boolean hasText = !TextUtils.isEmpty(mNoDataText); + + if (hasText) { + MPPointF pt = getCenter(); + + switch (mInfoPaint.getTextAlign()) { + case LEFT: + pt.x = 0; + canvas.drawText(mNoDataText, pt.x, pt.y, mInfoPaint); + break; + + case RIGHT: + pt.x *= 2.0; + canvas.drawText(mNoDataText, pt.x, pt.y, mInfoPaint); + break; + + default: + canvas.drawText(mNoDataText, pt.x, pt.y, mInfoPaint); + break; + } + } + + return; + } + + if (!mOffsetsCalculated) { + + calculateOffsets(); + mOffsetsCalculated = true; + } + } + + /** + * Draws the description text in the bottom right corner of the chart (per default) + */ + protected void drawDescription(Canvas c) { + + // check if description should be drawn + if (mDescription != null && mDescription.isEnabled()) { + + MPPointF position = mDescription.getPosition(); + + mDescPaint.setTypeface(mDescription.getTypeface()); + mDescPaint.setTextSize(mDescription.getTextSize()); + mDescPaint.setColor(mDescription.getTextColor()); + mDescPaint.setTextAlign(mDescription.getTextAlign()); + + float x, y; + + // if no position specified, draw on default position + if (position == null) { + x = getWidth() - mViewPortHandler.offsetRight() - mDescription.getXOffset(); + y = getHeight() - mViewPortHandler.offsetBottom() - mDescription.getYOffset(); + } else { + x = position.x; + y = position.y; + } + + c.drawText(mDescription.getText(), x, y, mDescPaint); + } + } + + /** + * ################ ################ ################ ################ + */ + /** BELOW THIS CODE FOR HIGHLIGHTING */ + + /** + * array of Highlight objects that reference the highlighted slices in the + * chart + */ + protected Highlight[] mIndicesToHighlight; + + /** + * The maximum distance in dp away from an entry causing it to highlight. + */ + protected float mMaxHighlightDistance = 0f; + + @Override + public float getMaxHighlightDistance() { + return mMaxHighlightDistance; + } + + /** + * Sets the maximum distance in screen dp a touch can be away from an entry to cause it to get highlighted. + * Default: 500dp + * + * @param distDp + */ + public void setMaxHighlightDistance(float distDp) { + mMaxHighlightDistance = Utils.convertDpToPixel(distDp); + } + + /** + * Returns the array of currently highlighted values. This might a null or + * empty array if nothing is highlighted. + * + * @return + */ + public Highlight[] getHighlighted() { + return mIndicesToHighlight; + } + + /** + * Returns true if values can be highlighted via tap gesture, false if not. + * + * @return + */ + public boolean isHighlightPerTapEnabled() { + return mHighLightPerTapEnabled; + } + + /** + * Set this to false to prevent values from being highlighted by tap gesture. + * Values can still be highlighted via drag or programmatically. Default: true + * + * @param enabled + */ + public void setHighlightPerTapEnabled(boolean enabled) { + mHighLightPerTapEnabled = enabled; + } + + /** + * Returns true if there are values to highlight, false if there are no + * values to highlight. Checks if the highlight array is null, has a length + * of zero or if the first object is null. + * + * @return + */ + public boolean valuesToHighlight() { + return mIndicesToHighlight == null || mIndicesToHighlight.length <= 0 + || mIndicesToHighlight[0] == null ? false + : true; + } + + /** + * Sets the last highlighted value for the touchlistener. + * + * @param highs + */ + protected void setLastHighlighted(Highlight[] highs) { + + if (highs == null || highs.length <= 0 || highs[0] == null) { + mChartTouchListener.setLastHighlighted(null); + } else { + mChartTouchListener.setLastHighlighted(highs[0]); + } + } + + /** + * Highlights the values at the given indices in the given DataSets. Provide + * null or an empty array to undo all highlighting. This should be used to + * programmatically highlight values. + * This method *will not* call the listener. + * + * @param highs + */ + public void highlightValues(Highlight[] highs) { + + // set the indices to highlight + mIndicesToHighlight = highs; + + setLastHighlighted(highs); + + // redraw the chart + invalidate(); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * This method will call the listener. + * + * @param x The x-value to highlight + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + */ + public void highlightValue(float x, int dataSetIndex, int dataIndex) { + highlightValue(x, dataSetIndex, dataIndex, true); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * This method will call the listener. + * + * @param x The x-value to highlight + * @param dataSetIndex The dataset index to search in + */ + public void highlightValue(float x, int dataSetIndex) { + highlightValue(x, dataSetIndex, -1, true); + } + + /** + * Highlights the value at the given x-value and y-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * This method will call the listener. + * + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + */ + public void highlightValue(float x, float y, int dataSetIndex, int dataIndex) { + highlightValue(x, y, dataSetIndex, dataIndex, true); + } + + /** + * Highlights the value at the given x-value and y-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * This method will call the listener. + * + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + */ + public void highlightValue(float x, float y, int dataSetIndex) { + highlightValue(x, y, dataSetIndex, -1, true); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * + * @param x The x-value to highlight + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + * @param callListener Should the listener be called for this change + */ + public void highlightValue(float x, int dataSetIndex, int dataIndex, boolean callListener) { + highlightValue(x, Float.NaN, dataSetIndex, dataIndex, callListener); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * + * @param x The x-value to highlight + * @param dataSetIndex The dataset index to search in + * @param callListener Should the listener be called for this change + */ + public void highlightValue(float x, int dataSetIndex, boolean callListener) { + highlightValue(x, Float.NaN, dataSetIndex, -1, callListener); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + * @param callListener Should the listener be called for this change + */ + public void highlightValue(float x, float y, int dataSetIndex, int dataIndex, boolean callListener) { + + if (dataSetIndex < 0 || dataSetIndex >= mData.getDataSetCount()) { + highlightValue(null, callListener); + } else { + highlightValue(new Highlight(x, y, dataSetIndex, dataIndex), callListener); + } + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + * @param callListener Should the listener be called for this change + */ + public void highlightValue(float x, float y, int dataSetIndex, boolean callListener) { + highlightValue(x, y, dataSetIndex, -1, callListener); + } + + /** + * Highlights the values represented by the provided Highlight object + * This method *will not* call the listener. + * + * @param highlight contains information about which entry should be highlighted + */ + public void highlightValue(Highlight highlight) { + highlightValue(highlight, false); + } + + /** + * Highlights the value selected by touch gesture. Unlike + * highlightValues(...), this generates a callback to the + * OnChartValueSelectedListener. + * + * @param high - the highlight object + * @param callListener - call the listener + */ + public void highlightValue(Highlight high, boolean callListener) { + + Entry e = null; + + if (high == null) + mIndicesToHighlight = null; + else { + + if (mLogEnabled) + Log.i(LOG_TAG, "Highlighted: " + high.toString()); + + e = mData.getEntryForHighlight(high); + if (e == null) { + mIndicesToHighlight = null; + high = null; + } else { + + // set the indices to highlight + mIndicesToHighlight = new Highlight[]{ + high + }; + } + } + + setLastHighlighted(mIndicesToHighlight); + + if (callListener && mSelectionListener != null) { + + if (!valuesToHighlight()) + mSelectionListener.onNothingSelected(); + else { + // notify the listener + mSelectionListener.onValueSelected(e, high); + } + } + + // redraw the chart + invalidate(); + } + + /** + * Returns the Highlight object (contains x-index and DataSet index) of the + * selected value at the given touch point inside the Line-, Scatter-, or + * CandleStick-Chart. + * + * @param x + * @param y + * @return + */ + public Highlight getHighlightByTouchPoint(float x, float y) { + + if (mData == null) { + Log.e(LOG_TAG, "Can't select by touch. No data set."); + return null; + } else + return getHighlighter().getHighlight(x, y); + } + + /** + * Set a new (e.g. custom) ChartTouchListener NOTE: make sure to + * setTouchEnabled(true); if you need touch gestures on the chart + * + * @param l + */ + public void setOnTouchListener(ChartTouchListener l) { + this.mChartTouchListener = l; + } + + /** + * Returns an instance of the currently active touch listener. + * + * @return + */ + public ChartTouchListener getOnTouchListener() { + return mChartTouchListener; + } + + /** + * ################ ################ ################ ################ + */ + /** BELOW CODE IS FOR THE MARKER VIEW */ + + /** + * if set to true, the marker view is drawn when a value is clicked + */ + protected boolean mDrawMarkers = true; + + /** + * the view that represents the marker + */ + protected IMarker mMarker; + + /** + * draws all MarkerViews on the highlighted positions + */ + protected void drawMarkers(Canvas canvas) { + + // if there is no marker view or drawing marker is disabled + if (mMarker == null || !isDrawMarkersEnabled() || !valuesToHighlight()) + return; + + for (int i = 0; i < mIndicesToHighlight.length; i++) { + + Highlight highlight = mIndicesToHighlight[i]; + + IDataSet set = mData.getDataSetByIndex(highlight.getDataSetIndex()); + + Entry e = mData.getEntryForHighlight(mIndicesToHighlight[i]); + int entryIndex = set.getEntryIndex(e); + + // make sure entry not null + if (e == null || entryIndex > set.getEntryCount() * mAnimator.getPhaseX()) + continue; + + float[] pos = getMarkerPosition(highlight); + + // check bounds + if (!mViewPortHandler.isInBounds(pos[0], pos[1])) + continue; + + // callbacks to update the content + mMarker.refreshContent(e, highlight); + + // draw the marker + mMarker.draw(canvas, pos[0], pos[1]); + } + } + + /** + * Returns the actual position in pixels of the MarkerView for the given + * Highlight object. + * + * @param high + * @return + */ + protected float[] getMarkerPosition(Highlight high) { + return new float[]{high.getDrawX(), high.getDrawY()}; + } + + /** + * ################ ################ ################ ################ + * ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + */ + /** CODE BELOW THIS RELATED TO ANIMATION */ + + /** + * Returns the animator responsible for animating chart values. + * + * @return + */ + public ChartAnimator getAnimator() { + return mAnimator; + } + + /** + * If set to true, chart continues to scroll after touch up default: true + */ + public boolean isDragDecelerationEnabled() { + return mDragDecelerationEnabled; + } + + /** + * If set to true, chart continues to scroll after touch up. Default: true. + * + * @param enabled + */ + public void setDragDecelerationEnabled(boolean enabled) { + mDragDecelerationEnabled = enabled; + } + + /** + * Returns drag deceleration friction coefficient + * + * @return + */ + public float getDragDecelerationFrictionCoef() { + return mDragDecelerationFrictionCoef; + } + + /** + * Deceleration friction coefficient in [0 ; 1] interval, higher values + * indicate that speed will decrease slowly, for example if it set to 0, it + * will stop immediately. 1 is an invalid value, and will be converted to + * 0.999f automatically. + * + * @param newValue + */ + public void setDragDecelerationFrictionCoef(float newValue) { + + if (newValue < 0.f) + newValue = 0.f; + + if (newValue >= 1f) + newValue = 0.999f; + + mDragDecelerationFrictionCoef = newValue; + } + + /** + * ################ ################ ################ ################ + * ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + */ + /** CODE BELOW FOR PROVIDING EASING FUNCTIONS */ + + /** + * Animates the drawing / rendering of the chart on both x- and y-axis with + * the specified animation time. If animate(...) is called, no further + * calling of invalidate() is necessary to refresh the chart. ANIMATIONS + * ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillisX + * @param durationMillisY + * @param easingX a custom easing function to be used on the animation phase + * @param easingY a custom easing function to be used on the animation phase + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX, + EasingFunction easingY) { + mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY); + } + + /** + * Animates the drawing / rendering of the chart on both x- and y-axis with + * the specified animation time. If animate(...) is called, no further + * calling of invalidate() is necessary to refresh the chart. ANIMATIONS + * ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillisX + * @param durationMillisY + * @param easing a custom easing function to be used on the animation phase + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easing) { + mAnimator.animateXY(durationMillisX, durationMillisY, easing); + } + + /** + * Animates the rendering of the chart on the x-axis with the specified + * animation time. If animate(...) is called, no further calling of + * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR + * API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillis + * @param easing a custom easing function to be used on the animation phase + */ + @RequiresApi(11) + public void animateX(int durationMillis, EasingFunction easing) { + mAnimator.animateX(durationMillis, easing); + } + + /** + * Animates the rendering of the chart on the y-axis with the specified + * animation time. If animate(...) is called, no further calling of + * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR + * API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillis + * @param easing a custom easing function to be used on the animation phase + */ + @RequiresApi(11) + public void animateY(int durationMillis, EasingFunction easing) { + mAnimator.animateY(durationMillis, easing); + } + + /** + * ################ ################ ################ ################ + * ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + */ + /** CODE BELOW FOR PREDEFINED EASING OPTIONS */ + + /** + * ################ ################ ################ ################ + * ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + */ + /** CODE BELOW FOR ANIMATIONS WITHOUT EASING */ + + /** + * Animates the rendering of the chart on the x-axis with the specified + * animation time. If animate(...) is called, no further calling of + * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR + * API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillis + */ + @RequiresApi(11) + public void animateX(int durationMillis) { + mAnimator.animateX(durationMillis); + } + + /** + * Animates the rendering of the chart on the y-axis with the specified + * animation time. If animate(...) is called, no further calling of + * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR + * API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillis + */ + @RequiresApi(11) + public void animateY(int durationMillis) { + mAnimator.animateY(durationMillis); + } + + /** + * Animates the drawing / rendering of the chart on both x- and y-axis with + * the specified animation time. If animate(...) is called, no further + * calling of invalidate() is necessary to refresh the chart. ANIMATIONS + * ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillisX + * @param durationMillisY + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY) { + mAnimator.animateXY(durationMillisX, durationMillisY); + } + + /** + * ################ ################ ################ ################ + */ + /** BELOW THIS ONLY GETTERS AND SETTERS */ + + + /** + * Returns the object representing all x-labels, this method can be used to + * acquire the XAxis object and modify it (e.g. change the position of the + * labels, styling, etc.) + * + * @return + */ + public XAxis getXAxis() { + return mXAxis; + } + + /** + * Returns the default IValueFormatter that has been determined by the chart + * considering the provided minimum and maximum values. + * + * @return + */ + public IValueFormatter getDefaultValueFormatter() { + return mDefaultValueFormatter; + } + + /** + * set a selection listener for the chart + * + * @param l + */ + public void setOnChartValueSelectedListener(OnChartValueSelectedListener l) { + this.mSelectionListener = l; + } + + /** + * Sets a gesture-listener for the chart for custom callbacks when executing + * gestures on the chart surface. + * + * @param l + */ + public void setOnChartGestureListener(OnChartGestureListener l) { + this.mGestureListener = l; + } + + /** + * Returns the custom gesture listener. + * + * @return + */ + public OnChartGestureListener getOnChartGestureListener() { + return mGestureListener; + } + + /** + * returns the current y-max value across all DataSets + * + * @return + */ + public float getYMax() { + return mData.getYMax(); + } + + /** + * returns the current y-min value across all DataSets + * + * @return + */ + public float getYMin() { + return mData.getYMin(); + } + + @Override + public float getXChartMax() { + return mXAxis.mAxisMaximum; + } + + @Override + public float getXChartMin() { + return mXAxis.mAxisMinimum; + } + + @Override + public float getXRange() { + return mXAxis.mAxisRange; + } + + /** + * Returns a recyclable MPPointF instance. + * Returns the center point of the chart (the whole View) in pixels. + * + * @return + */ + public MPPointF getCenter() { + return MPPointF.getInstance(getWidth() / 2f, getHeight() / 2f); + } + + /** + * Returns a recyclable MPPointF instance. + * Returns the center of the chart taking offsets under consideration. + * (returns the center of the content rectangle) + * + * @return + */ + @Override + public MPPointF getCenterOffsets() { + return mViewPortHandler.getContentCenter(); + } + + /** + * Sets extra offsets (around the chart view) to be appended to the + * auto-calculated offsets. + * + * @param left + * @param top + * @param right + * @param bottom + */ + public void setExtraOffsets(float left, float top, float right, float bottom) { + setExtraLeftOffset(left); + setExtraTopOffset(top); + setExtraRightOffset(right); + setExtraBottomOffset(bottom); + } + + /** + * Set an extra offset to be appended to the viewport's top + */ + public void setExtraTopOffset(float offset) { + mExtraTopOffset = Utils.convertDpToPixel(offset); + } + + /** + * @return the extra offset to be appended to the viewport's top + */ + public float getExtraTopOffset() { + return mExtraTopOffset; + } + + /** + * Set an extra offset to be appended to the viewport's right + */ + public void setExtraRightOffset(float offset) { + mExtraRightOffset = Utils.convertDpToPixel(offset); + } + + /** + * @return the extra offset to be appended to the viewport's right + */ + public float getExtraRightOffset() { + return mExtraRightOffset; + } + + /** + * Set an extra offset to be appended to the viewport's bottom + */ + public void setExtraBottomOffset(float offset) { + mExtraBottomOffset = Utils.convertDpToPixel(offset); + } + + /** + * @return the extra offset to be appended to the viewport's bottom + */ + public float getExtraBottomOffset() { + return mExtraBottomOffset; + } + + /** + * Set an extra offset to be appended to the viewport's left + */ + public void setExtraLeftOffset(float offset) { + mExtraLeftOffset = Utils.convertDpToPixel(offset); + } + + /** + * @return the extra offset to be appended to the viewport's left + */ + public float getExtraLeftOffset() { + return mExtraLeftOffset; + } + + /** + * Set this to true to enable logcat outputs for the chart. Beware that + * logcat output decreases rendering performance. Default: disabled. + * + * @param enabled + */ + public void setLogEnabled(boolean enabled) { + mLogEnabled = enabled; + } + + /** + * Returns true if log-output is enabled for the chart, fals if not. + * + * @return + */ + public boolean isLogEnabled() { + return mLogEnabled; + } + + /** + * Sets the text that informs the user that there is no data available with + * which to draw the chart. + * + * @param text + */ + public void setNoDataText(String text) { + mNoDataText = text; + } + + /** + * Sets the color of the no data text. + * + * @param color + */ + public void setNoDataTextColor(int color) { + mInfoPaint.setColor(color); + } + + public void setNodataTextSize(float size) { + mInfoPaint.setTextSize(Utils.convertDpToPixel(size)); + } + + /** + * Sets the typeface to be used for the no data text. + * + * @param tf + */ + public void setNoDataTextTypeface(Typeface tf) { + mInfoPaint.setTypeface(tf); + } + + /** + * alignment of the no data text + * + * @param align + */ + public void setNoDataTextAlignment(Align align) { + mInfoPaint.setTextAlign(align); + } + + /** + * Set this to false to disable all gestures and touches on the chart, + * default: true + * + * @param enabled + */ + public void setTouchEnabled(boolean enabled) { + this.mTouchEnabled = enabled; + } + + /** + * sets the marker that is displayed when a value is clicked on the chart + * + * @param marker + */ + public void setMarker(IMarker marker) { + mMarker = marker; + } + + /** + * returns the marker that is set as a marker view for the chart + * + * @return + */ + public IMarker getMarker() { + return mMarker; + } + + @Deprecated + public void setMarkerView(IMarker v) { + setMarker(v); + } + + @Deprecated + public IMarker getMarkerView() { + return getMarker(); + } + + /** + * Sets a new Description object for the chart. + * + * @param desc + */ + public void setDescription(Description desc) { + this.mDescription = desc; + } + + /** + * Returns the Description object of the chart that is responsible for holding all information related + * to the description text that is displayed in the bottom right corner of the chart (by default). + * + * @return + */ + public Description getDescription() { + return mDescription; + } + + /** + * Returns the Legend object of the chart. This method can be used to get an + * instance of the legend in order to customize the automatically generated + * Legend. + * + * @return + */ + public Legend getLegend() { + return mLegend; + } + + /** + * Returns the renderer object responsible for rendering / drawing the + * Legend. + * + * @return + */ + public LegendRenderer getLegendRenderer() { + return mLegendRenderer; + } + + /** + * Returns the rectangle that defines the borders of the chart-value surface + * (into which the actual values are drawn). + * + * @return + */ + @Override + public RectF getContentRect() { + return mViewPortHandler.getContentRect(); + } + + /** + * disables intercept touchevents + */ + public void disableScroll() { + ViewParent parent = getParent(); + if (parent != null) + parent.requestDisallowInterceptTouchEvent(true); + } + + /** + * enables intercept touchevents + */ + public void enableScroll() { + ViewParent parent = getParent(); + if (parent != null) + parent.requestDisallowInterceptTouchEvent(false); + } + + /** + * paint for the grid background (only line and barchart) + */ + public static final int PAINT_GRID_BACKGROUND = 4; + + /** + * paint for the info text that is displayed when there are no values in the + * chart + */ + public static final int PAINT_INFO = 7; + + /** + * paint for the description text in the bottom right corner + */ + public static final int PAINT_DESCRIPTION = 11; + + /** + * paint for the hole in the middle of the pie chart + */ + public static final int PAINT_HOLE = 13; + + /** + * paint for the text in the middle of the pie chart + */ + public static final int PAINT_CENTER_TEXT = 14; + + /** + * paint used for the legend + */ + public static final int PAINT_LEGEND_LABEL = 18; + + /** + * set a new paint object for the specified parameter in the chart e.g. + * Chart.PAINT_VALUES + * + * @param p the new paint object + * @param which Chart.PAINT_VALUES, Chart.PAINT_GRID, Chart.PAINT_VALUES, + * ... + */ + public void setPaint(Paint p, int which) { + + switch (which) { + case PAINT_INFO: + mInfoPaint = p; + break; + case PAINT_DESCRIPTION: + mDescPaint = p; + break; + } + } + + /** + * Returns the paint object associated with the provided constant. + * + * @param which e.g. Chart.PAINT_LEGEND_LABEL + * @return + */ + public Paint getPaint(int which) { + switch (which) { + case PAINT_INFO: + return mInfoPaint; + case PAINT_DESCRIPTION: + return mDescPaint; + } + + return null; + } + + @Deprecated + public boolean isDrawMarkerViewsEnabled() { + return isDrawMarkersEnabled(); + } + + @Deprecated + public void setDrawMarkerViews(boolean enabled) { + setDrawMarkers(enabled); + } + + /** + * returns true if drawing the marker is enabled when tapping on values + * (use the setMarker(IMarker marker) method to specify a marker) + * + * @return + */ + public boolean isDrawMarkersEnabled() { + return mDrawMarkers; + } + + /** + * Set this to true to draw a user specified marker when tapping on + * chart values (use the setMarker(IMarker marker) method to specify a + * marker). Default: true + * + * @param enabled + */ + public void setDrawMarkers(boolean enabled) { + mDrawMarkers = enabled; + } + + /** + * Returns the ChartData object that has been set for the chart. + * + * @return + */ + public T getData() { + return mData; + } + + /** + * Returns the ViewPortHandler of the chart that is responsible for the + * content area of the chart and its offsets and dimensions. + * + * @return + */ + public ViewPortHandler getViewPortHandler() { + return mViewPortHandler; + } + + /** + * Returns the Renderer object the chart uses for drawing data. + * + * @return + */ + public DataRenderer getRenderer() { + return mRenderer; + } + + /** + * Sets a new DataRenderer object for the chart. + * + * @param renderer + */ + public void setRenderer(DataRenderer renderer) { + + if (renderer != null) + mRenderer = renderer; + } + + public IHighlighter getHighlighter() { + return mHighlighter; + } + + /** + * Sets a custom highligher object for the chart that handles / processes + * all highlight touch events performed on the chart-view. + * + * @param highlighter + */ + public void setHighlighter(ChartHighlighter highlighter) { + mHighlighter = highlighter; + } + + /** + * Returns a recyclable MPPointF instance. + * + * @return + */ + @Override + public MPPointF getCenterOfView() { + return getCenter(); + } + + /** + * Returns the bitmap that represents the chart. + * + * @return + */ + public Bitmap getChartBitmap() { + // Define a bitmap with the same size as the view + Bitmap returnedBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565); + // Bind a canvas to it + Canvas canvas = new Canvas(returnedBitmap); + // Get the view's background + Drawable bgDrawable = getBackground(); + if (bgDrawable != null) + // has background drawable, then draw it on the canvas + bgDrawable.draw(canvas); + else + // does not have background drawable, then draw white background on + // the canvas + canvas.drawColor(Color.WHITE); + // draw the view on the canvas + draw(canvas); + // return the bitmap + return returnedBitmap; + } + + /** + * Saves the current chart state with the given name to the given path on + * the sdcard leaving the path empty "" will put the saved file directly on + * the SD card chart is saved as a PNG image, example: + * saveToPath("myfilename", "foldername1/foldername2"); + * + * @param title + * @param pathOnSD e.g. "folder1/folder2/folder3" + * @return returns true on success, false on error + */ + public boolean saveToPath(String title, String pathOnSD) { + + + Bitmap b = getChartBitmap(); + + OutputStream stream = null; + try { + stream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + + pathOnSD + "/" + title + + ".png"); + + /* + * Write bitmap to file using JPEG or PNG and 40% quality hint for + * JPEG. + */ + b.compress(CompressFormat.PNG, 40, stream); + + stream.close(); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + return true; + } + + /** + * Saves the current state of the chart to the gallery as an image type. The + * compression must be set for JPEG only. 0 == maximum compression, 100 = low + * compression (high quality). NOTE: Needs permission WRITE_EXTERNAL_STORAGE + * + * @param fileName e.g. "my_image" + * @param subFolderPath e.g. "ChartPics" + * @param fileDescription e.g. "Chart details" + * @param format e.g. Bitmap.CompressFormat.PNG + * @param quality e.g. 50, min = 0, max = 100 + * @return returns true if saving was successful, false if not + */ + public boolean saveToGallery(String fileName, String subFolderPath, String fileDescription, Bitmap.CompressFormat + format, int quality) { + // restrain quality + if (quality < 0 || quality > 100) + quality = 50; + + long currentTime = System.currentTimeMillis(); + + File extBaseDir = Environment.getExternalStorageDirectory(); + File file = new File(extBaseDir.getAbsolutePath() + "/DCIM/" + subFolderPath); + if (!file.exists()) { + if (!file.mkdirs()) { + return false; + } + } + + String mimeType = ""; + switch (format) { + case PNG: + mimeType = "image/png"; + if (!fileName.endsWith(".png")) + fileName += ".png"; + break; + case WEBP: + mimeType = "image/webp"; + if (!fileName.endsWith(".webp")) + fileName += ".webp"; + break; + case JPEG: + default: + mimeType = "image/jpeg"; + if (!(fileName.endsWith(".jpg") || fileName.endsWith(".jpeg"))) + fileName += ".jpg"; + break; + } + + String filePath = file.getAbsolutePath() + "/" + fileName; + FileOutputStream out = null; + try { + out = new FileOutputStream(filePath); + + Bitmap b = getChartBitmap(); + b.compress(format, quality, out); + + out.flush(); + out.close(); + + } catch (IOException e) { + e.printStackTrace(); + + return false; + } + + long size = new File(filePath).length(); + + ContentValues values = new ContentValues(8); + + // store the details + values.put(Images.Media.TITLE, fileName); + values.put(Images.Media.DISPLAY_NAME, fileName); + values.put(Images.Media.DATE_ADDED, currentTime); + values.put(Images.Media.MIME_TYPE, mimeType); + values.put(Images.Media.DESCRIPTION, fileDescription); + values.put(Images.Media.ORIENTATION, 0); + values.put(Images.Media.DATA, filePath); + values.put(Images.Media.SIZE, size); + + return getContext().getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values) != null; + } + + /** + * Saves the current state of the chart to the gallery as a JPEG image. The + * filename and compression can be set. 0 == maximum compression, 100 = low + * compression (high quality). NOTE: Needs permission WRITE_EXTERNAL_STORAGE + * + * @param fileName e.g. "my_image" + * @param quality e.g. 50, min = 0, max = 100 + * @return returns true if saving was successful, false if not + */ + public boolean saveToGallery(String fileName, int quality) { + return saveToGallery(fileName, "", "MPAndroidChart-Library Save", Bitmap.CompressFormat.PNG, quality); + } + + /** + * Saves the current state of the chart to the gallery as a PNG image. + * NOTE: Needs permission WRITE_EXTERNAL_STORAGE + * + * @param fileName e.g. "my_image" + * @return returns true if saving was successful, false if not + */ + public boolean saveToGallery(String fileName) { + return saveToGallery(fileName, "", "MPAndroidChart-Library Save", Bitmap.CompressFormat.PNG, 40); + } + + /** + * tasks to be done after the view is setup + */ + protected ArrayList mJobs = new ArrayList(); + + public void removeViewportJob(Runnable job) { + mJobs.remove(job); + } + + public void clearAllViewportJobs() { + mJobs.clear(); + } + + /** + * Either posts a job immediately if the chart has already setup it's + * dimensions or adds the job to the execution queue. + * + * @param job + */ + public void addViewportJob(Runnable job) { + + if (mViewPortHandler.hasChartDimens()) { + post(job); + } else { + mJobs.add(job); + } + } + + /** + * Returns all jobs that are scheduled to be executed after + * onSizeChanged(...). + * + * @return + */ + public ArrayList getJobs() { + return mJobs; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).layout(left, top, right, bottom); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int size = (int) Utils.convertDpToPixel(50f); + setMeasuredDimension( + Math.max(getSuggestedMinimumWidth(), + resolveSize(size, + widthMeasureSpec)), + Math.max(getSuggestedMinimumHeight(), + resolveSize(size, + heightMeasureSpec))); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (mLogEnabled) + Log.i(LOG_TAG, "OnSizeChanged()"); + + if (w > 0 && h > 0 && w < 10000 && h < 10000) { + if (mLogEnabled) + Log.i(LOG_TAG, "Setting chart dimens, width: " + w + ", height: " + h); + mViewPortHandler.setChartDimens(w, h); + } else { + if (mLogEnabled) + Log.w(LOG_TAG, "*Avoiding* setting chart dimens! width: " + w + ", height: " + h); + } + + // This may cause the chart view to mutate properties affecting the view port -- + // lets do this before we try to run any pending jobs on the view port itself + notifyDataSetChanged(); + + for (Runnable r : mJobs) { + post(r); + } + + mJobs.clear(); + + super.onSizeChanged(w, h, oldw, oldh); + } + + /** + * Setting this to true will set the layer-type HARDWARE for the view, false + * will set layer-type SOFTWARE. + * + * @param enabled + */ + public void setHardwareAccelerationEnabled(boolean enabled) { + + if (enabled) + setLayerType(View.LAYER_TYPE_HARDWARE, null); + else + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + //Log.i(LOG_TAG, "Detaching..."); + + if (mUnbind) + unbindDrawables(this); + } + + /** + * unbind flag + */ + private boolean mUnbind = false; + + /** + * Unbind all drawables to avoid memory leaks. + * Link: http://stackoverflow.com/a/6779164/1590502 + * + * @param view + */ + private void unbindDrawables(View view) { + + if (view.getBackground() != null) { + view.getBackground().setCallback(null); + } + if (view instanceof ViewGroup) { + for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) { + unbindDrawables(((ViewGroup) view).getChildAt(i)); + } + ((ViewGroup) view).removeAllViews(); + } + } + + /** + * Set this to true to enable "unbinding" of drawables. When a View is detached + * from a window. This helps avoid memory leaks. + * Default: false + * Link: http://stackoverflow.com/a/6779164/1590502 + * + * @param enabled + */ + public void setUnbindEnabled(boolean enabled) { + this.mUnbind = enabled; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CombinedChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CombinedChart.java new file mode 100644 index 0000000..cd01f0e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CombinedChart.java @@ -0,0 +1,272 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.util.Log; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BubbleData; +import com.github.mikephil.charting.data.CandleData; +import com.github.mikephil.charting.data.CombinedData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.highlight.CombinedHighlighter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.CombinedDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.renderer.CombinedChartRenderer; + +/** + * This chart class allows the combination of lines, bars, scatter and candle + * data all displayed in one chart area. + * + * @author Philipp Jahoda + */ +public class CombinedChart extends BarLineChartBase implements CombinedDataProvider { + + /** + * if set to true, all values are drawn above their bars, instead of below + * their top + */ + private boolean mDrawValueAboveBar = true; + + + /** + * flag that indicates whether the highlight should be full-bar oriented, or single-value? + */ + protected boolean mHighlightFullBarEnabled = false; + + /** + * if set to true, a grey area is drawn behind each bar that indicates the + * maximum value + */ + private boolean mDrawBarShadow = false; + + protected DrawOrder[] mDrawOrder; + + /** + * enum that allows to specify the order in which the different data objects + * for the combined-chart are drawn + */ + public enum DrawOrder { + BAR, BUBBLE, LINE, CANDLE, SCATTER + } + + public CombinedChart(Context context) { + super(context); + } + + public CombinedChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CombinedChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + // Default values are not ready here yet + mDrawOrder = new DrawOrder[]{ + DrawOrder.BAR, DrawOrder.BUBBLE, DrawOrder.LINE, DrawOrder.CANDLE, DrawOrder.SCATTER + }; + + setHighlighter(new CombinedHighlighter(this, this)); + + // Old default behaviour + setHighlightFullBarEnabled(true); + + mRenderer = new CombinedChartRenderer(this, mAnimator, mViewPortHandler); + } + + @Override + public CombinedData getCombinedData() { + return mData; + } + + @Override + public void setData(CombinedData data) { + super.setData(data); + setHighlighter(new CombinedHighlighter(this, this)); + ((CombinedChartRenderer)mRenderer).createRenderers(); + mRenderer.initBuffers(); + } + + /** + * Returns the Highlight object (contains x-index and DataSet index) of the selected value at the given touch + * point + * inside the CombinedChart. + * + * @param x + * @param y + * @return + */ + @Override + public Highlight getHighlightByTouchPoint(float x, float y) { + + if (mData == null) { + Log.e(LOG_TAG, "Can't select by touch. No data set."); + return null; + } else { + Highlight h = getHighlighter().getHighlight(x, y); + if (h == null || !isHighlightFullBarEnabled()) return h; + + // For isHighlightFullBarEnabled, remove stackIndex + return new Highlight(h.getX(), h.getY(), + h.getXPx(), h.getYPx(), + h.getDataSetIndex(), -1, h.getAxis()); + } + } + + @Override + public LineData getLineData() { + if (mData == null) + return null; + return mData.getLineData(); + } + + @Override + public BarData getBarData() { + if (mData == null) + return null; + return mData.getBarData(); + } + + @Override + public ScatterData getScatterData() { + if (mData == null) + return null; + return mData.getScatterData(); + } + + @Override + public CandleData getCandleData() { + if (mData == null) + return null; + return mData.getCandleData(); + } + + @Override + public BubbleData getBubbleData() { + if (mData == null) + return null; + return mData.getBubbleData(); + } + + @Override + public boolean isDrawBarShadowEnabled() { + return mDrawBarShadow; + } + + @Override + public boolean isDrawValueAboveBarEnabled() { + return mDrawValueAboveBar; + } + + /** + * If set to true, all values are drawn above their bars, instead of below + * their top. + * + * @param enabled + */ + public void setDrawValueAboveBar(boolean enabled) { + mDrawValueAboveBar = enabled; + } + + + /** + * If set to true, a grey area is drawn behind each bar that indicates the + * maximum value. Enabling his will reduce performance by about 50%. + * + * @param enabled + */ + public void setDrawBarShadow(boolean enabled) { + mDrawBarShadow = enabled; + } + + /** + * Set this to true to make the highlight operation full-bar oriented, + * false to make it highlight single values (relevant only for stacked). + * + * @param enabled + */ + public void setHighlightFullBarEnabled(boolean enabled) { + mHighlightFullBarEnabled = enabled; + } + + /** + * @return true the highlight operation is be full-bar oriented, false if single-value + */ + @Override + public boolean isHighlightFullBarEnabled() { + return mHighlightFullBarEnabled; + } + + /** + * Returns the currently set draw order. + * + * @return + */ + public DrawOrder[] getDrawOrder() { + return mDrawOrder; + } + + /** + * Sets the order in which the provided data objects should be drawn. The + * earlier you place them in the provided array, the further they will be in + * the background. e.g. if you provide new DrawOrer[] { DrawOrder.BAR, + * DrawOrder.LINE }, the bars will be drawn behind the lines. + * + * @param order + */ + public void setDrawOrder(DrawOrder[] order) { + if (order == null || order.length <= 0) + return; + mDrawOrder = order; + } + + /** + * draws all MarkerViews on the highlighted positions + */ + protected void drawMarkers(Canvas canvas) { + + // if there is no marker view or drawing marker is disabled + if (mMarker == null || !isDrawMarkersEnabled() || !valuesToHighlight()) + return; + + for (int i = 0; i < mIndicesToHighlight.length; i++) { + + Highlight highlight = mIndicesToHighlight[i]; + + IDataSet set = mData.getDataSetByHighlight(highlight); + + Entry e = mData.getEntryForHighlight(highlight); + if (e == null) + continue; + + int entryIndex = set.getEntryIndex(e); + + // make sure entry not null + if (entryIndex > set.getEntryCount() * mAnimator.getPhaseX()) + continue; + + float[] pos = getMarkerPosition(highlight); + + // check bounds + if (!mViewPortHandler.isInBounds(pos[0], pos[1])) + continue; + + // callbacks to update the content + mMarker.refreshContent(e, highlight); + + // draw the marker + mMarker.draw(canvas, pos[0], pos[1]); + } + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/HorizontalBarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/HorizontalBarChart.java new file mode 100644 index 0000000..9aac1ce --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/HorizontalBarChart.java @@ -0,0 +1,346 @@ +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; + +import com.github.mikephil.charting.components.XAxis.XAxisPosition; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.highlight.HorizontalBarHighlighter; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.renderer.HorizontalBarChartRenderer; +import com.github.mikephil.charting.renderer.XAxisRendererHorizontalBarChart; +import com.github.mikephil.charting.renderer.YAxisRendererHorizontalBarChart; +import com.github.mikephil.charting.utils.HorizontalViewPortHandler; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.TransformerHorizontalBarChart; +import com.github.mikephil.charting.utils.Utils; + +/** + * BarChart with horizontal bar orientation. In this implementation, x- and y-axis are switched, meaning the YAxis class + * represents the horizontal values and the XAxis class represents the vertical values. + * + * @author Philipp Jahoda + */ +public class HorizontalBarChart extends BarChart { + + public HorizontalBarChart(Context context) { + super(context); + } + + public HorizontalBarChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public HorizontalBarChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + + mViewPortHandler = new HorizontalViewPortHandler(); + + super.init(); + + mLeftAxisTransformer = new TransformerHorizontalBarChart(mViewPortHandler); + mRightAxisTransformer = new TransformerHorizontalBarChart(mViewPortHandler); + + mRenderer = new HorizontalBarChartRenderer(this, mAnimator, mViewPortHandler); + setHighlighter(new HorizontalBarHighlighter(this)); + + mAxisRendererLeft = new YAxisRendererHorizontalBarChart(mViewPortHandler, mAxisLeft, mLeftAxisTransformer); + mAxisRendererRight = new YAxisRendererHorizontalBarChart(mViewPortHandler, mAxisRight, mRightAxisTransformer); + mXAxisRenderer = new XAxisRendererHorizontalBarChart(mViewPortHandler, mXAxis, mLeftAxisTransformer, this); + } + + private RectF mOffsetsBuffer = new RectF(); + + protected void calculateLegendOffsets(RectF offsets) { + + offsets.left = 0.f; + offsets.right = 0.f; + offsets.top = 0.f; + offsets.bottom = 0.f; + + if (mLegend == null || !mLegend.isEnabled() || mLegend.isDrawInsideEnabled()) + return; + + switch (mLegend.getOrientation()) { + case VERTICAL: + + switch (mLegend.getHorizontalAlignment()) { + case LEFT: + offsets.left += Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()) + + mLegend.getXOffset(); + break; + + case RIGHT: + offsets.right += Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()) + + mLegend.getXOffset(); + break; + + case CENTER: + + switch (mLegend.getVerticalAlignment()) { + case TOP: + offsets.top += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + break; + + case BOTTOM: + offsets.bottom += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + break; + + default: + break; + } + } + + break; + + case HORIZONTAL: + + switch (mLegend.getVerticalAlignment()) { + case TOP: + offsets.top += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + + if (mAxisLeft.isEnabled() && mAxisLeft.isDrawLabelsEnabled()) + offsets.top += mAxisLeft.getRequiredHeightSpace( + mAxisRendererLeft.getPaintAxisLabels()); + break; + + case BOTTOM: + offsets.bottom += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + + if (mAxisRight.isEnabled() && mAxisRight.isDrawLabelsEnabled()) + offsets.bottom += mAxisRight.getRequiredHeightSpace( + mAxisRendererRight.getPaintAxisLabels()); + break; + + default: + break; + } + break; + } + } + + @Override + public void calculateOffsets() { + + float offsetLeft = 0f, offsetRight = 0f, offsetTop = 0f, offsetBottom = 0f; + + calculateLegendOffsets(mOffsetsBuffer); + + offsetLeft += mOffsetsBuffer.left; + offsetTop += mOffsetsBuffer.top; + offsetRight += mOffsetsBuffer.right; + offsetBottom += mOffsetsBuffer.bottom; + + // offsets for y-labels + if (mAxisLeft.needsOffset()) { + offsetTop += mAxisLeft.getRequiredHeightSpace(mAxisRendererLeft.getPaintAxisLabels()); + } + + if (mAxisRight.needsOffset()) { + offsetBottom += mAxisRight.getRequiredHeightSpace(mAxisRendererRight.getPaintAxisLabels()); + } + + float xlabelwidth = mXAxis.mLabelRotatedWidth; + + if (mXAxis.isEnabled()) { + + // offsets for x-labels + if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { + + offsetLeft += xlabelwidth; + + } else if (mXAxis.getPosition() == XAxisPosition.TOP) { + + offsetRight += xlabelwidth; + + } else if (mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + + offsetLeft += xlabelwidth; + offsetRight += xlabelwidth; + } + } + + offsetTop += getExtraTopOffset(); + offsetRight += getExtraRightOffset(); + offsetBottom += getExtraBottomOffset(); + offsetLeft += getExtraLeftOffset(); + + float minOffset = Utils.convertDpToPixel(mMinOffset); + + mViewPortHandler.restrainViewPort( + Math.max(minOffset, offsetLeft), + Math.max(minOffset, offsetTop), + Math.max(minOffset, offsetRight), + Math.max(minOffset, offsetBottom)); + + if (mLogEnabled) { + Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop + ", offsetRight: " + + offsetRight + ", offsetBottom: " + + offsetBottom); + Log.i(LOG_TAG, "Content: " + mViewPortHandler.getContentRect().toString()); + } + + prepareOffsetMatrix(); + prepareValuePxMatrix(); + } + + @Override + protected void prepareValuePxMatrix() { + mRightAxisTransformer.prepareMatrixValuePx(mAxisRight.mAxisMinimum, mAxisRight.mAxisRange, mXAxis.mAxisRange, + mXAxis.mAxisMinimum); + mLeftAxisTransformer.prepareMatrixValuePx(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisRange, mXAxis.mAxisRange, + mXAxis.mAxisMinimum); + } + + @Override + protected float[] getMarkerPosition(Highlight high) { + return new float[]{high.getDrawY(), high.getDrawX()}; + } + + @Override + public void getBarBounds(BarEntry e, RectF outputRect) { + + RectF bounds = outputRect; + IBarDataSet set = mData.getDataSetForEntry(e); + + if (set == null) { + outputRect.set(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); + return; + } + + float y = e.getY(); + float x = e.getX(); + + float barWidth = mData.getBarWidth(); + + float top = x - barWidth / 2f; + float bottom = x + barWidth / 2f; + float left = y >= 0 ? y : 0; + float right = y <= 0 ? y : 0; + + bounds.set(left, top, right, bottom); + + getTransformer(set.getAxisDependency()).rectValueToPixel(bounds); + + } + + protected float[] mGetPositionBuffer = new float[2]; + + /** + * Returns a recyclable MPPointF instance. + * + * @param e + * @param axis + * @return + */ + @Override + public MPPointF getPosition(Entry e, AxisDependency axis) { + + if (e == null) + return null; + + float[] vals = mGetPositionBuffer; + vals[0] = e.getY(); + vals[1] = e.getX(); + + getTransformer(axis).pointValuesToPixel(vals); + + return MPPointF.getInstance(vals[0], vals[1]); + } + + /** + * Returns the Highlight object (contains x-index and DataSet index) of the selected value at the given touch point + * inside the BarChart. + * + * @param x + * @param y + * @return + */ + @Override + public Highlight getHighlightByTouchPoint(float x, float y) { + + if (mData == null) { + if (mLogEnabled) + Log.e(LOG_TAG, "Can't select by touch. No data set."); + return null; + } else + return getHighlighter().getHighlight(y, x); // switch x and y + } + + @Override + public float getLowestVisibleX() { + getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), posForGetLowestVisibleX); + float result = (float) Math.max(mXAxis.mAxisMinimum, posForGetLowestVisibleX.y); + return result; + } + + @Override + public float getHighestVisibleX() { + getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), posForGetHighestVisibleX); + float result = (float) Math.min(mXAxis.mAxisMaximum, posForGetHighestVisibleX.y); + return result; + } + + /** + * ###### VIEWPORT METHODS BELOW THIS ###### + */ + + @Override + public void setVisibleXRangeMaximum(float maxXRange) { + float xScale = mXAxis.mAxisRange / (maxXRange); + mViewPortHandler.setMinimumScaleY(xScale); + } + + @Override + public void setVisibleXRangeMinimum(float minXRange) { + float xScale = mXAxis.mAxisRange / (minXRange); + mViewPortHandler.setMaximumScaleY(xScale); + } + + @Override + public void setVisibleXRange(float minXRange, float maxXRange) { + float minScale = mXAxis.mAxisRange / minXRange; + float maxScale = mXAxis.mAxisRange / maxXRange; + mViewPortHandler.setMinMaxScaleY(minScale, maxScale); + } + + @Override + public void setVisibleYRangeMaximum(float maxYRange, AxisDependency axis) { + float yScale = getAxisRange(axis) / maxYRange; + mViewPortHandler.setMinimumScaleX(yScale); + } + + @Override + public void setVisibleYRangeMinimum(float minYRange, AxisDependency axis) { + float yScale = getAxisRange(axis) / minYRange; + mViewPortHandler.setMaximumScaleX(yScale); + } + + @Override + public void setVisibleYRange(float minYRange, float maxYRange, AxisDependency axis) { + float minScale = getAxisRange(axis) / minYRange; + float maxScale = getAxisRange(axis) / maxYRange; + mViewPortHandler.setMinMaxScaleX(minScale, maxScale); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/LineChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/LineChart.java new file mode 100644 index 0000000..aa7afc4 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/LineChart.java @@ -0,0 +1,50 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.util.AttributeSet; + +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; +import com.github.mikephil.charting.renderer.LineChartRenderer; + +/** + * Chart that draws lines, surfaces, circles, ... + * + * @author Philipp Jahoda + */ +public class LineChart extends BarLineChartBase implements LineDataProvider { + + public LineChart(Context context) { + super(context); + } + + public LineChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LineChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new LineChartRenderer(this, mAnimator, mViewPortHandler); + } + + @Override + public LineData getLineData() { + return mData; + } + + @Override + protected void onDetachedFromWindow() { + // releases the bitmap in the renderer to avoid oom error + if (mRenderer != null && mRenderer instanceof LineChartRenderer) { + ((LineChartRenderer) mRenderer).releaseBitmap(); + } + super.onDetachedFromWindow(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieChart.java new file mode 100644 index 0000000..de11b3a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieChart.java @@ -0,0 +1,804 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.util.AttributeSet; + +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.highlight.PieHighlighter; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; +import com.github.mikephil.charting.renderer.PieChartRenderer; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +import java.util.List; + +/** + * View that represents a pie chart. Draws cake like slices. + * + * @author Philipp Jahoda + */ +public class PieChart extends PieRadarChartBase { + + /** + * rect object that represents the bounds of the piechart, needed for + * drawing the circle + */ + private RectF mCircleBox = new RectF(); + + /** + * flag indicating if entry labels should be drawn or not + */ + private boolean mDrawEntryLabels = true; + + /** + * array that holds the width of each pie-slice in degrees + */ + private float[] mDrawAngles = new float[1]; + + /** + * array that holds the absolute angle in degrees of each slice + */ + private float[] mAbsoluteAngles = new float[1]; + + /** + * if true, the white hole inside the chart will be drawn + */ + private boolean mDrawHole = true; + + /** + * if true, the hole will see-through to the inner tips of the slices + */ + private boolean mDrawSlicesUnderHole = false; + + /** + * if true, the values inside the piechart are drawn as percent values + */ + private boolean mUsePercentValues = false; + + /** + * if true, the slices of the piechart are rounded + */ + private boolean mDrawRoundedSlices = false; + + /** + * variable for the text that is drawn in the center of the pie-chart + */ + private CharSequence mCenterText = ""; + + private MPPointF mCenterTextOffset = MPPointF.getInstance(0, 0); + + /** + * indicates the size of the hole in the center of the piechart, default: + * radius / 2 + */ + private float mHoleRadiusPercent = 50f; + + /** + * the radius of the transparent circle next to the chart-hole in the center + */ + protected float mTransparentCircleRadiusPercent = 55f; + + /** + * if enabled, centertext is drawn + */ + private boolean mDrawCenterText = true; + + private float mCenterTextRadiusPercent = 100.f; + + protected float mMaxAngle = 360f; + + /** + * Minimum angle to draw slices, this only works if there is enough room for all slices to have + * the minimum angle, default 0f. + */ + private float mMinAngleForSlices = 0f; + + public PieChart(Context context) { + super(context); + } + + public PieChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PieChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new PieChartRenderer(this, mAnimator, mViewPortHandler); + mXAxis = null; + + mHighlighter = new PieHighlighter(this); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mData == null) + return; + + mRenderer.drawData(canvas); + + if (valuesToHighlight()) + mRenderer.drawHighlighted(canvas, mIndicesToHighlight); + + mRenderer.drawExtras(canvas); + + mRenderer.drawValues(canvas); + + mLegendRenderer.renderLegend(canvas); + + drawDescription(canvas); + + drawMarkers(canvas); + } + + @Override + public void calculateOffsets() { + super.calculateOffsets(); + + // prevent nullpointer when no data set + if (mData == null) + return; + + float diameter = getDiameter(); + float radius = diameter / 2f; + + MPPointF c = getCenterOffsets(); + + float shift = mData.getDataSet().getSelectionShift(); + + // create the circle box that will contain the pie-chart (the bounds of + // the pie-chart) + mCircleBox.set(c.x - radius + shift, + c.y - radius + shift, + c.x + radius - shift, + c.y + radius - shift); + + MPPointF.recycleInstance(c); + } + + @Override + protected void calcMinMax() { + calcAngles(); + } + + @Override + protected float[] getMarkerPosition(Highlight highlight) { + + MPPointF center = getCenterCircleBox(); + float r = getRadius(); + + float off = r / 10f * 3.6f; + + if (isDrawHoleEnabled()) { + off = (r - (r / 100f * getHoleRadius())) / 2f; + } + + r -= off; // offset to keep things inside the chart + + float rotationAngle = getRotationAngle(); + + int entryIndex = (int) highlight.getX(); + + // offset needed to center the drawn text in the slice + float offset = mDrawAngles[entryIndex] / 2; + + // calculate the text position + float x = (float) (r + * Math.cos(Math.toRadians((rotationAngle + mAbsoluteAngles[entryIndex] - offset) + * mAnimator.getPhaseY())) + center.x); + float y = (float) (r + * Math.sin(Math.toRadians((rotationAngle + mAbsoluteAngles[entryIndex] - offset) + * mAnimator.getPhaseY())) + center.y); + + MPPointF.recycleInstance(center); + return new float[]{x, y}; + } + + /** + * calculates the needed angles for the chart slices + */ + private void calcAngles() { + + int entryCount = mData.getEntryCount(); + + if (mDrawAngles.length != entryCount) { + mDrawAngles = new float[entryCount]; + } else { + for (int i = 0; i < entryCount; i++) { + mDrawAngles[i] = 0; + } + } + if (mAbsoluteAngles.length != entryCount) { + mAbsoluteAngles = new float[entryCount]; + } else { + for (int i = 0; i < entryCount; i++) { + mAbsoluteAngles[i] = 0; + } + } + + float yValueSum = mData.getYValueSum(); + + List dataSets = mData.getDataSets(); + + boolean hasMinAngle = mMinAngleForSlices != 0f && entryCount * mMinAngleForSlices <= mMaxAngle; + float[] minAngles = new float[entryCount]; + + int cnt = 0; + float offset = 0f; + float diff = 0f; + + for (int i = 0; i < mData.getDataSetCount(); i++) { + + IPieDataSet set = dataSets.get(i); + + for (int j = 0; j < set.getEntryCount(); j++) { + + float drawAngle = calcAngle(Math.abs(set.getEntryForIndex(j).getY()), yValueSum); + + if (hasMinAngle) { + float temp = drawAngle - mMinAngleForSlices; + if (temp <= 0) { + minAngles[cnt] = mMinAngleForSlices; + offset += -temp; + } else { + minAngles[cnt] = drawAngle; + diff += temp; + } + } + + mDrawAngles[cnt] = drawAngle; + + if (cnt == 0) { + mAbsoluteAngles[cnt] = mDrawAngles[cnt]; + } else { + mAbsoluteAngles[cnt] = mAbsoluteAngles[cnt - 1] + mDrawAngles[cnt]; + } + + cnt++; + } + } + + if (hasMinAngle) { + // Correct bigger slices by relatively reducing their angles based on the total angle needed to subtract + // This requires that `entryCount * mMinAngleForSlices <= mMaxAngle` be true to properly work! + for (int i = 0; i < entryCount; i++) { + minAngles[i] -= (minAngles[i] - mMinAngleForSlices) / diff * offset; + if (i == 0) { + mAbsoluteAngles[0] = minAngles[0]; + } else { + mAbsoluteAngles[i] = mAbsoluteAngles[i - 1] + minAngles[i]; + } + } + + mDrawAngles = minAngles; + } + } + + /** + * Checks if the given index is set to be highlighted. + * + * @param index + * @return + */ + public boolean needsHighlight(int index) { + + // no highlight + if (!valuesToHighlight()) + return false; + + for (int i = 0; i < mIndicesToHighlight.length; i++) + + // check if the xvalue for the given dataset needs highlight + if ((int) mIndicesToHighlight[i].getX() == index) + return true; + + return false; + } + + /** + * calculates the needed angle for a given value + * + * @param value + * @return + */ + private float calcAngle(float value) { + return calcAngle(value, mData.getYValueSum()); + } + + /** + * calculates the needed angle for a given value + * + * @param value + * @param yValueSum + * @return + */ + private float calcAngle(float value, float yValueSum) { + return value / yValueSum * mMaxAngle; + } + + /** + * This will throw an exception, PieChart has no XAxis object. + * + * @return + */ + @Deprecated + @Override + public XAxis getXAxis() { + throw new RuntimeException("PieChart has no XAxis"); + } + + @Override + public int getIndexForAngle(float angle) { + + // take the current angle of the chart into consideration + float a = Utils.getNormalizedAngle(angle - getRotationAngle()); + + for (int i = 0; i < mAbsoluteAngles.length; i++) { + if (mAbsoluteAngles[i] > a) + return i; + } + + return -1; // return -1 if no index found + } + + /** + * Returns the index of the DataSet this x-index belongs to. + * + * @param xIndex + * @return + */ + public int getDataSetIndexForIndex(int xIndex) { + + List dataSets = mData.getDataSets(); + + for (int i = 0; i < dataSets.size(); i++) { + if (dataSets.get(i).getEntryForXValue(xIndex, Float.NaN) != null) + return i; + } + + return -1; + } + + /** + * returns an integer array of all the different angles the chart slices + * have the angles in the returned array determine how much space (of 360°) + * each slice takes + * + * @return + */ + public float[] getDrawAngles() { + return mDrawAngles; + } + + /** + * returns the absolute angles of the different chart slices (where the + * slices end) + * + * @return + */ + public float[] getAbsoluteAngles() { + return mAbsoluteAngles; + } + + /** + * Sets the color for the hole that is drawn in the center of the PieChart + * (if enabled). + * + * @param color + */ + public void setHoleColor(int color) { + ((PieChartRenderer) mRenderer).getPaintHole().setColor(color); + } + + /** + * Enable or disable the visibility of the inner tips of the slices behind the hole + */ + public void setDrawSlicesUnderHole(boolean enable) { + mDrawSlicesUnderHole = enable; + } + + /** + * Returns true if the inner tips of the slices are visible behind the hole, + * false if not. + * + * @return true if slices are visible behind the hole. + */ + public boolean isDrawSlicesUnderHoleEnabled() { + return mDrawSlicesUnderHole; + } + + /** + * set this to true to draw the pie center empty + * + * @param enabled + */ + public void setDrawHoleEnabled(boolean enabled) { + this.mDrawHole = enabled; + } + + /** + * returns true if the hole in the center of the pie-chart is set to be + * visible, false if not + * + * @return + */ + public boolean isDrawHoleEnabled() { + return mDrawHole; + } + + /** + * Sets the text String that is displayed in the center of the PieChart. + * + * @param text + */ + public void setCenterText(CharSequence text) { + if (text == null) + mCenterText = ""; + else + mCenterText = text; + } + + /** + * returns the text that is drawn in the center of the pie-chart + * + * @return + */ + public CharSequence getCenterText() { + return mCenterText; + } + + /** + * set this to true to draw the text that is displayed in the center of the + * pie chart + * + * @param enabled + */ + public void setDrawCenterText(boolean enabled) { + this.mDrawCenterText = enabled; + } + + /** + * returns true if drawing the center text is enabled + * + * @return + */ + public boolean isDrawCenterTextEnabled() { + return mDrawCenterText; + } + + @Override + protected float getRequiredLegendOffset() { + return mLegendRenderer.getLabelPaint().getTextSize() * 2.f; + } + + @Override + protected float getRequiredBaseOffset() { + return 0; + } + + @Override + public float getRadius() { + if (mCircleBox == null) + return 0; + else + return Math.min(mCircleBox.width() / 2f, mCircleBox.height() / 2f); + } + + /** + * returns the circlebox, the boundingbox of the pie-chart slices + * + * @return + */ + public RectF getCircleBox() { + return mCircleBox; + } + + /** + * returns the center of the circlebox + * + * @return + */ + public MPPointF getCenterCircleBox() { + return MPPointF.getInstance(mCircleBox.centerX(), mCircleBox.centerY()); + } + + /** + * sets the typeface for the center-text paint + * + * @param t + */ + public void setCenterTextTypeface(Typeface t) { + ((PieChartRenderer) mRenderer).getPaintCenterText().setTypeface(t); + } + + /** + * Sets the size of the center text of the PieChart in dp. + * + * @param sizeDp + */ + public void setCenterTextSize(float sizeDp) { + ((PieChartRenderer) mRenderer).getPaintCenterText().setTextSize( + Utils.convertDpToPixel(sizeDp)); + } + + /** + * Sets the size of the center text of the PieChart in pixels. + * + * @param sizePixels + */ + public void setCenterTextSizePixels(float sizePixels) { + ((PieChartRenderer) mRenderer).getPaintCenterText().setTextSize(sizePixels); + } + + /** + * Sets the offset the center text should have from it's original position in dp. Default x = 0, y = 0 + * + * @param x + * @param y + */ + public void setCenterTextOffset(float x, float y) { + mCenterTextOffset.x = Utils.convertDpToPixel(x); + mCenterTextOffset.y = Utils.convertDpToPixel(y); + } + + /** + * Returns the offset on the x- and y-axis the center text has in dp. + * + * @return + */ + public MPPointF getCenterTextOffset() { + return MPPointF.getInstance(mCenterTextOffset.x, mCenterTextOffset.y); + } + + /** + * Sets the color of the center text of the PieChart. + * + * @param color + */ + public void setCenterTextColor(int color) { + ((PieChartRenderer) mRenderer).getPaintCenterText().setColor(color); + } + + /** + * sets the radius of the hole in the center of the piechart in percent of + * the maximum radius (max = the radius of the whole chart), default 50% + * + * @param percent + */ + public void setHoleRadius(final float percent) { + mHoleRadiusPercent = percent; + } + + /** + * Returns the size of the hole radius in percent of the total radius. + * + * @return + */ + public float getHoleRadius() { + return mHoleRadiusPercent; + } + + /** + * Sets the color the transparent-circle should have. + * + * @param color + */ + public void setTransparentCircleColor(int color) { + + Paint p = ((PieChartRenderer) mRenderer).getPaintTransparentCircle(); + int alpha = p.getAlpha(); + p.setColor(color); + p.setAlpha(alpha); + } + + /** + * sets the radius of the transparent circle that is drawn next to the hole + * in the piechart in percent of the maximum radius (max = the radius of the + * whole chart), default 55% -> means 5% larger than the center-hole by + * default + * + * @param percent + */ + public void setTransparentCircleRadius(final float percent) { + mTransparentCircleRadiusPercent = percent; + } + + public float getTransparentCircleRadius() { + return mTransparentCircleRadiusPercent; + } + + /** + * Sets the amount of transparency the transparent circle should have 0 = fully transparent, + * 255 = fully opaque. + * Default value is 100. + * + * @param alpha 0-255 + */ + public void setTransparentCircleAlpha(int alpha) { + ((PieChartRenderer) mRenderer).getPaintTransparentCircle().setAlpha(alpha); + } + + /** + * Set this to true to draw the entry labels into the pie slices (Provided by the getLabel() method of the PieEntry class). + * Deprecated -> use setDrawEntryLabels(...) instead. + * + * @param enabled + */ + @Deprecated + public void setDrawSliceText(boolean enabled) { + mDrawEntryLabels = enabled; + } + + /** + * Set this to true to draw the entry labels into the pie slices (Provided by the getLabel() method of the PieEntry class). + * + * @param enabled + */ + public void setDrawEntryLabels(boolean enabled) { + mDrawEntryLabels = enabled; + } + + /** + * Returns true if drawing the entry labels is enabled, false if not. + * + * @return + */ + public boolean isDrawEntryLabelsEnabled() { + return mDrawEntryLabels; + } + + /** + * Sets the color the entry labels are drawn with. + * + * @param color + */ + public void setEntryLabelColor(int color) { + ((PieChartRenderer) mRenderer).getPaintEntryLabels().setColor(color); + } + + /** + * Sets a custom Typeface for the drawing of the entry labels. + * + * @param tf + */ + public void setEntryLabelTypeface(Typeface tf) { + ((PieChartRenderer) mRenderer).getPaintEntryLabels().setTypeface(tf); + } + + /** + * Sets the size of the entry labels in dp. Default: 13dp + * + * @param size + */ + public void setEntryLabelTextSize(float size) { + ((PieChartRenderer) mRenderer).getPaintEntryLabels().setTextSize(Utils.convertDpToPixel(size)); + } + + /** + * Sets whether to draw slices in a curved fashion, only works if drawing the hole is enabled + * and if the slices are not drawn under the hole. + * + * @param enabled draw curved ends of slices + */ + public void setDrawRoundedSlices(boolean enabled) { + mDrawRoundedSlices = enabled; + } + + /** + * Returns true if the chart is set to draw each end of a pie-slice + * "rounded". + * + * @return + */ + public boolean isDrawRoundedSlicesEnabled() { + return mDrawRoundedSlices; + } + + /** + * If this is enabled, values inside the PieChart are drawn in percent and + * not with their original value. Values provided for the IValueFormatter to + * format are then provided in percent. + * + * @param enabled + */ + public void setUsePercentValues(boolean enabled) { + mUsePercentValues = enabled; + } + + /** + * Returns true if using percentage values is enabled for the chart. + * + * @return + */ + public boolean isUsePercentValuesEnabled() { + return mUsePercentValues; + } + + /** + * the rectangular radius of the bounding box for the center text, as a percentage of the pie + * hole + * default 1.f (100%) + */ + public void setCenterTextRadiusPercent(float percent) { + mCenterTextRadiusPercent = percent; + } + + /** + * the rectangular radius of the bounding box for the center text, as a percentage of the pie + * hole + * default 1.f (100%) + */ + public float getCenterTextRadiusPercent() { + return mCenterTextRadiusPercent; + } + + public float getMaxAngle() { + return mMaxAngle; + } + + /** + * Sets the max angle that is used for calculating the pie-circle. 360f means + * it's a full PieChart, 180f results in a half-pie-chart. Default: 360f + * + * @param maxangle min 90, max 360 + */ + public void setMaxAngle(float maxangle) { + + if (maxangle > 360) + maxangle = 360f; + + if (maxangle < 90) + maxangle = 90f; + + this.mMaxAngle = maxangle; + } + + /** + * The minimum angle slices on the chart are rendered with, default is 0f. + * + * @return minimum angle for slices + */ + public float getMinAngleForSlices() { + return mMinAngleForSlices; + } + + /** + * Set the angle to set minimum size for slices, you must call {@link #notifyDataSetChanged()} + * and {@link #invalidate()} when changing this, only works if there is enough room for all + * slices to have the minimum angle. + * + * @param minAngle minimum 0, maximum is half of {@link #setMaxAngle(float)} + */ + public void setMinAngleForSlices(float minAngle) { + + if (minAngle > (mMaxAngle / 2f)) + minAngle = mMaxAngle / 2f; + else if (minAngle < 0) + minAngle = 0f; + + this.mMinAngleForSlices = minAngle; + } + + @Override + protected void onDetachedFromWindow() { + // releases the bitmap in the renderer to avoid oom error + if (mRenderer != null && mRenderer instanceof PieChartRenderer) { + ((PieChartRenderer) mRenderer).releaseBitmap(); + } + super.onDetachedFromWindow(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieRadarChartBase.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieRadarChartBase.java new file mode 100644 index 0000000..e6d38d3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieRadarChartBase.java @@ -0,0 +1,499 @@ + +package com.github.mikephil.charting.charts; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.animation.Easing.EasingFunction; +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.listener.PieRadarChartTouchListener; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +/** + * Baseclass of PieChart and RadarChart. + * + * @author Philipp Jahoda + */ +public abstract class PieRadarChartBase>> + extends Chart { + + /** + * holds the normalized version of the current rotation angle of the chart + */ + private float mRotationAngle = 270f; + + /** + * holds the raw version of the current rotation angle of the chart + */ + private float mRawRotationAngle = 270f; + + /** + * flag that indicates if rotation is enabled or not + */ + protected boolean mRotateEnabled = true; + + /** + * Sets the minimum offset (padding) around the chart, defaults to 0.f + */ + protected float mMinOffset = 0.f; + + public PieRadarChartBase(Context context) { + super(context); + } + + public PieRadarChartBase(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PieRadarChartBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mChartTouchListener = new PieRadarChartTouchListener(this); + } + + @Override + protected void calcMinMax() { + //mXAxis.mAxisRange = mData.getXVals().size() - 1; + } + + @Override + public int getMaxVisibleCount() { + return mData.getEntryCount(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // use the pie- and radarchart listener own listener + if (mTouchEnabled && mChartTouchListener != null) + return mChartTouchListener.onTouch(this, event); + else + return super.onTouchEvent(event); + } + + @Override + public void computeScroll() { + + if (mChartTouchListener instanceof PieRadarChartTouchListener) + ((PieRadarChartTouchListener) mChartTouchListener).computeScroll(); + } + + @Override + public void notifyDataSetChanged() { + if (mData == null) + return; + + calcMinMax(); + + if (mLegend != null) + mLegendRenderer.computeLegend(mData); + + calculateOffsets(); + } + + @Override + public void calculateOffsets() { + + float legendLeft = 0f, legendRight = 0f, legendBottom = 0f, legendTop = 0f; + + if (mLegend != null && mLegend.isEnabled() && !mLegend.isDrawInsideEnabled()) { + + float fullLegendWidth = Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()); + + switch (mLegend.getOrientation()) { + case VERTICAL: { + float xLegendOffset = 0.f; + + if (mLegend.getHorizontalAlignment() == Legend.LegendHorizontalAlignment.LEFT + || mLegend.getHorizontalAlignment() == Legend.LegendHorizontalAlignment.RIGHT) { + if (mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.CENTER) { + // this is the space between the legend and the chart + final float spacing = Utils.convertDpToPixel(13f); + + xLegendOffset = fullLegendWidth + spacing; + + } else { + // this is the space between the legend and the chart + float spacing = Utils.convertDpToPixel(8f); + + float legendWidth = fullLegendWidth + spacing; + float legendHeight = mLegend.mNeededHeight + mLegend.mTextHeightMax; + + MPPointF center = getCenter(); + + float bottomX = mLegend.getHorizontalAlignment() == + Legend.LegendHorizontalAlignment.RIGHT + ? getWidth() - legendWidth + 15.f + : legendWidth - 15.f; + float bottomY = legendHeight + 15.f; + float distLegend = distanceToCenter(bottomX, bottomY); + + MPPointF reference = getPosition(center, getRadius(), + getAngleForPoint(bottomX, bottomY)); + + float distReference = distanceToCenter(reference.x, reference.y); + float minOffset = Utils.convertDpToPixel(5f); + + if (bottomY >= center.y && getHeight() - legendWidth > getWidth()) { + xLegendOffset = legendWidth; + } else if (distLegend < distReference) { + + float diff = distReference - distLegend; + xLegendOffset = minOffset + diff; + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(reference); + } + } + + switch (mLegend.getHorizontalAlignment()) { + case LEFT: + legendLeft = xLegendOffset; + break; + + case RIGHT: + legendRight = xLegendOffset; + break; + + case CENTER: + switch (mLegend.getVerticalAlignment()) { + case TOP: + legendTop = Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); + break; + case BOTTOM: + legendBottom = Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); + break; + } + break; + } + } + break; + + case HORIZONTAL: + float yLegendOffset = 0.f; + + if (mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.TOP || + mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.BOTTOM) { + + // It's possible that we do not need this offset anymore as it + // is available through the extraOffsets, but changing it can mean + // changing default visibility for existing apps. + float yOffset = getRequiredLegendOffset(); + + yLegendOffset = Math.min(mLegend.mNeededHeight + yOffset, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); + + switch (mLegend.getVerticalAlignment()) { + case TOP: + legendTop = yLegendOffset; + break; + case BOTTOM: + legendBottom = yLegendOffset; + break; + } + } + break; + } + + legendLeft += getRequiredBaseOffset(); + legendRight += getRequiredBaseOffset(); + legendTop += getRequiredBaseOffset(); + legendBottom += getRequiredBaseOffset(); + } + + float minOffset = Utils.convertDpToPixel(mMinOffset); + + if (this instanceof RadarChart) { + XAxis x = this.getXAxis(); + + if (x.isEnabled() && x.isDrawLabelsEnabled()) { + minOffset = Math.max(minOffset, x.mLabelRotatedWidth); + } + } + + legendTop += getExtraTopOffset(); + legendRight += getExtraRightOffset(); + legendBottom += getExtraBottomOffset(); + legendLeft += getExtraLeftOffset(); + + float offsetLeft = Math.max(minOffset, legendLeft); + float offsetTop = Math.max(minOffset, legendTop); + float offsetRight = Math.max(minOffset, legendRight); + float offsetBottom = Math.max(minOffset, Math.max(getRequiredBaseOffset(), legendBottom)); + + mViewPortHandler.restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom); + + if (mLogEnabled) + Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop + + ", offsetRight: " + offsetRight + ", offsetBottom: " + offsetBottom); + } + + /** + * returns the angle relative to the chart center for the given point on the + * chart in degrees. The angle is always between 0 and 360°, 0° is NORTH, + * 90° is EAST, ... + * + * @param x + * @param y + * @return + */ + public float getAngleForPoint(float x, float y) { + + MPPointF c = getCenterOffsets(); + + double tx = x - c.x, ty = y - c.y; + double length = Math.sqrt(tx * tx + ty * ty); + double r = Math.acos(ty / length); + + float angle = (float) Math.toDegrees(r); + + if (x > c.x) + angle = 360f - angle; + + // add 90° because chart starts EAST + angle = angle + 90f; + + // neutralize overflow + if (angle > 360f) + angle = angle - 360f; + + MPPointF.recycleInstance(c); + + return angle; + } + + /** + * Returns a recyclable MPPointF instance. + * Calculates the position around a center point, depending on the distance + * from the center, and the angle of the position around the center. + * + * @param center + * @param dist + * @param angle in degrees, converted to radians internally + * @return + */ + public MPPointF getPosition(MPPointF center, float dist, float angle) { + + MPPointF p = MPPointF.getInstance(0, 0); + getPosition(center, dist, angle, p); + return p; + } + + public void getPosition(MPPointF center, float dist, float angle, MPPointF outputPoint) { + outputPoint.x = (float) (center.x + dist * Math.cos(Math.toRadians(angle))); + outputPoint.y = (float) (center.y + dist * Math.sin(Math.toRadians(angle))); + } + + /** + * Returns the distance of a certain point on the chart to the center of the + * chart. + * + * @param x + * @param y + * @return + */ + public float distanceToCenter(float x, float y) { + + MPPointF c = getCenterOffsets(); + + float dist = 0f; + + float xDist = 0f; + float yDist = 0f; + + if (x > c.x) { + xDist = x - c.x; + } else { + xDist = c.x - x; + } + + if (y > c.y) { + yDist = y - c.y; + } else { + yDist = c.y - y; + } + + // pythagoras + dist = (float) Math.sqrt(Math.pow(xDist, 2.0) + Math.pow(yDist, 2.0)); + + MPPointF.recycleInstance(c); + + return dist; + } + + /** + * Returns the xIndex for the given angle around the center of the chart. + * Returns -1 if not found / outofbounds. + * + * @param angle + * @return + */ + public abstract int getIndexForAngle(float angle); + + /** + * Set an offset for the rotation of the RadarChart in degrees. Default 270f + * --> top (NORTH) + * + * @param angle + */ + public void setRotationAngle(float angle) { + mRawRotationAngle = angle; + mRotationAngle = Utils.getNormalizedAngle(mRawRotationAngle); + } + + /** + * gets the raw version of the current rotation angle of the pie chart the + * returned value could be any value, negative or positive, outside of the + * 360 degrees. this is used when working with rotation direction, mainly by + * gestures and animations. + * + * @return + */ + public float getRawRotationAngle() { + return mRawRotationAngle; + } + + /** + * gets a normalized version of the current rotation angle of the pie chart, + * which will always be between 0.0 < 360.0 + * + * @return + */ + public float getRotationAngle() { + return mRotationAngle; + } + + /** + * Set this to true to enable the rotation / spinning of the chart by touch. + * Set it to false to disable it. Default: true + * + * @param enabled + */ + public void setRotationEnabled(boolean enabled) { + mRotateEnabled = enabled; + } + + /** + * Returns true if rotation of the chart by touch is enabled, false if not. + * + * @return + */ + public boolean isRotationEnabled() { + return mRotateEnabled; + } + + /** + * Gets the minimum offset (padding) around the chart, defaults to 0.f + */ + public float getMinOffset() { + return mMinOffset; + } + + /** + * Sets the minimum offset (padding) around the chart, defaults to 0.f + */ + public void setMinOffset(float minOffset) { + mMinOffset = minOffset; + } + + /** + * returns the diameter of the pie- or radar-chart + * + * @return + */ + public float getDiameter() { + RectF content = mViewPortHandler.getContentRect(); + content.left += getExtraLeftOffset(); + content.top += getExtraTopOffset(); + content.right -= getExtraRightOffset(); + content.bottom -= getExtraBottomOffset(); + return Math.min(content.width(), content.height()); + } + + /** + * Returns the radius of the chart in pixels. + * + * @return + */ + public abstract float getRadius(); + + /** + * Returns the required offset for the chart legend. + * + * @return + */ + protected abstract float getRequiredLegendOffset(); + + /** + * Returns the base offset needed for the chart without calculating the + * legend size. + * + * @return + */ + protected abstract float getRequiredBaseOffset(); + + @Override + public float getYChartMax() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public float getYChartMin() { + // TODO Auto-generated method stub + return 0; + } + + /** + * ################ ################ ################ ################ + */ + /** CODE BELOW THIS RELATED TO ANIMATION */ + + /** + * Applys a spin animation to the Chart. + * + * @param durationmillis + * @param fromangle + * @param toangle + */ + @SuppressLint("NewApi") + public void spin(int durationmillis, float fromangle, float toangle, EasingFunction easing) { + + setRotationAngle(fromangle); + + ObjectAnimator spinAnimator = ObjectAnimator.ofFloat(this, "rotationAngle", fromangle, + toangle); + spinAnimator.setDuration(durationmillis); + spinAnimator.setInterpolator(easing); + + spinAnimator.addUpdateListener(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + postInvalidate(); + } + }); + spinAnimator.start(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/RadarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/RadarChart.java new file mode 100644 index 0000000..8c08853 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/RadarChart.java @@ -0,0 +1,362 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.RectF; +import android.util.AttributeSet; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.data.RadarData; +import com.github.mikephil.charting.highlight.RadarHighlighter; +import com.github.mikephil.charting.renderer.RadarChartRenderer; +import com.github.mikephil.charting.renderer.XAxisRendererRadarChart; +import com.github.mikephil.charting.renderer.YAxisRendererRadarChart; +import com.github.mikephil.charting.utils.Utils; + +/** + * Implementation of the RadarChart, a "spidernet"-like chart. It works best + * when displaying 5-10 entries per DataSet. + * + * @author Philipp Jahoda + */ +public class RadarChart extends PieRadarChartBase { + + /** + * width of the main web lines + */ + private float mWebLineWidth = 2.5f; + + /** + * width of the inner web lines + */ + private float mInnerWebLineWidth = 1.5f; + + /** + * color for the main web lines + */ + private int mWebColor = Color.rgb(122, 122, 122); + + /** + * color for the inner web + */ + private int mWebColorInner = Color.rgb(122, 122, 122); + + /** + * transparency the grid is drawn with (0-255) + */ + private int mWebAlpha = 150; + + /** + * flag indicating if the web lines should be drawn or not + */ + private boolean mDrawWeb = true; + + /** + * modulus that determines how many labels and web-lines are skipped before the next is drawn + */ + private int mSkipWebLineCount = 0; + + /** + * the object reprsenting the y-axis labels + */ + private YAxis mYAxis; + + protected YAxisRendererRadarChart mYAxisRenderer; + protected XAxisRendererRadarChart mXAxisRenderer; + + public RadarChart(Context context) { + super(context); + } + + public RadarChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RadarChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mYAxis = new YAxis(AxisDependency.LEFT); + mYAxis.setLabelXOffset(10f); + + mWebLineWidth = Utils.convertDpToPixel(1.5f); + mInnerWebLineWidth = Utils.convertDpToPixel(0.75f); + + mRenderer = new RadarChartRenderer(this, mAnimator, mViewPortHandler); + mYAxisRenderer = new YAxisRendererRadarChart(mViewPortHandler, mYAxis, this); + mXAxisRenderer = new XAxisRendererRadarChart(mViewPortHandler, mXAxis, this); + + mHighlighter = new RadarHighlighter(this); + } + + @Override + protected void calcMinMax() { + super.calcMinMax(); + + mYAxis.calculate(mData.getYMin(AxisDependency.LEFT), mData.getYMax(AxisDependency.LEFT)); + mXAxis.calculate(0, mData.getMaxEntryCountSet().getEntryCount()); + } + + @Override + public void notifyDataSetChanged() { + if (mData == null) + return; + + calcMinMax(); + + mYAxisRenderer.computeAxis(mYAxis.mAxisMinimum, mYAxis.mAxisMaximum, mYAxis.isInverted()); + mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false); + + if (mLegend != null && !mLegend.isLegendCustom()) + mLegendRenderer.computeLegend(mData); + + calculateOffsets(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mData == null) + return; + +// if (mYAxis.isEnabled()) +// mYAxisRenderer.computeAxis(mYAxis.mAxisMinimum, mYAxis.mAxisMaximum, mYAxis.isInverted()); + + if (mXAxis.isEnabled()) + mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false); + + mXAxisRenderer.renderAxisLabels(canvas); + + if (mDrawWeb) + mRenderer.drawExtras(canvas); + + if (mYAxis.isEnabled() && mYAxis.isDrawLimitLinesBehindDataEnabled()) + mYAxisRenderer.renderLimitLines(canvas); + + mRenderer.drawData(canvas); + + if (valuesToHighlight()) + mRenderer.drawHighlighted(canvas, mIndicesToHighlight); + + if (mYAxis.isEnabled() && !mYAxis.isDrawLimitLinesBehindDataEnabled()) + mYAxisRenderer.renderLimitLines(canvas); + + mYAxisRenderer.renderAxisLabels(canvas); + + mRenderer.drawValues(canvas); + + mLegendRenderer.renderLegend(canvas); + + drawDescription(canvas); + + drawMarkers(canvas); + } + + /** + * Returns the factor that is needed to transform values into pixels. + * + * @return + */ + public float getFactor() { + RectF content = mViewPortHandler.getContentRect(); + return Math.min(content.width() / 2f, content.height() / 2f) / mYAxis.mAxisRange; + } + + /** + * Returns the angle that each slice in the radar chart occupies. + * + * @return + */ + public float getSliceAngle() { + return 360f / (float) mData.getMaxEntryCountSet().getEntryCount(); + } + + @Override + public int getIndexForAngle(float angle) { + + // take the current angle of the chart into consideration + float a = Utils.getNormalizedAngle(angle - getRotationAngle()); + + float sliceangle = getSliceAngle(); + + int max = mData.getMaxEntryCountSet().getEntryCount(); + + int index = 0; + + for (int i = 0; i < max; i++) { + + float referenceAngle = sliceangle * (i + 1) - sliceangle / 2f; + + if (referenceAngle > a) { + index = i; + break; + } + } + + return index; + } + + /** + * Returns the object that represents all y-labels of the RadarChart. + * + * @return + */ + public YAxis getYAxis() { + return mYAxis; + } + + /** + * Sets the width of the web lines that come from the center. + * + * @param width + */ + public void setWebLineWidth(float width) { + mWebLineWidth = Utils.convertDpToPixel(width); + } + + public float getWebLineWidth() { + return mWebLineWidth; + } + + /** + * Sets the width of the web lines that are in between the lines coming from + * the center. + * + * @param width + */ + public void setWebLineWidthInner(float width) { + mInnerWebLineWidth = Utils.convertDpToPixel(width); + } + + public float getWebLineWidthInner() { + return mInnerWebLineWidth; + } + + /** + * Sets the transparency (alpha) value for all web lines, default: 150, 255 + * = 100% opaque, 0 = 100% transparent + * + * @param alpha + */ + public void setWebAlpha(int alpha) { + mWebAlpha = alpha; + } + + /** + * Returns the alpha value for all web lines. + * + * @return + */ + public int getWebAlpha() { + return mWebAlpha; + } + + /** + * Sets the color for the web lines that come from the center. Don't forget + * to use getResources().getColor(...) when loading a color from the + * resources. Default: Color.rgb(122, 122, 122) + * + * @param color + */ + public void setWebColor(int color) { + mWebColor = color; + } + + public int getWebColor() { + return mWebColor; + } + + /** + * Sets the color for the web lines in between the lines that come from the + * center. Don't forget to use getResources().getColor(...) when loading a + * color from the resources. Default: Color.rgb(122, 122, 122) + * + * @param color + */ + public void setWebColorInner(int color) { + mWebColorInner = color; + } + + public int getWebColorInner() { + return mWebColorInner; + } + + /** + * If set to true, drawing the web is enabled, if set to false, drawing the + * whole web is disabled. Default: true + * + * @param enabled + */ + public void setDrawWeb(boolean enabled) { + mDrawWeb = enabled; + } + + /** + * Sets the number of web-lines that should be skipped on chart web before the + * next one is drawn. This targets the lines that come from the center of the RadarChart. + * + * @param count if count = 1 -> 1 line is skipped in between + */ + public void setSkipWebLineCount(int count) { + + mSkipWebLineCount = Math.max(0, count); + } + + /** + * Returns the modulus that is used for skipping web-lines. + * + * @return + */ + public int getSkipWebLineCount() { + return mSkipWebLineCount; + } + + @Override + protected float getRequiredLegendOffset() { + return mLegendRenderer.getLabelPaint().getTextSize() * 4.f; + } + + @Override + protected float getRequiredBaseOffset() { + return mXAxis.isEnabled() && mXAxis.isDrawLabelsEnabled() ? + mXAxis.mLabelRotatedWidth : + Utils.convertDpToPixel(10f); + } + + @Override + public float getRadius() { + RectF content = mViewPortHandler.getContentRect(); + return Math.min(content.width() / 2f, content.height() / 2f); + } + + /** + * Returns the maximum value this chart can display on it's y-axis. + */ + public float getYChartMax() { + return mYAxis.mAxisMaximum; + } + + /** + * Returns the minimum value this chart can display on it's y-axis. + */ + public float getYChartMin() { + return mYAxis.mAxisMinimum; + } + + /** + * Returns the range of y-values this chart can display. + * + * @return + */ + public float getYRange() { + return mYAxis.mAxisRange; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/ScatterChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/ScatterChart.java new file mode 100644 index 0000000..37e8395 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/ScatterChart.java @@ -0,0 +1,77 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.util.AttributeSet; + +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.interfaces.dataprovider.ScatterDataProvider; +import com.github.mikephil.charting.renderer.ScatterChartRenderer; + +/** + * The ScatterChart. Draws dots, triangles, squares and custom shapes into the + * Chart-View. CIRCLE and SCQUARE offer the best performance, TRIANGLE has the + * worst performance. + * + * @author Philipp Jahoda + */ +public class ScatterChart extends BarLineChartBase implements ScatterDataProvider { + + public ScatterChart(Context context) { + super(context); + } + + public ScatterChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ScatterChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + + @Override + protected void init() { + super.init(); + + mRenderer = new ScatterChartRenderer(this, mAnimator, mViewPortHandler); + + getXAxis().setSpaceMin(0.5f); + getXAxis().setSpaceMax(0.5f); + } + + @Override + public ScatterData getScatterData() { + return mData; + } + + /** + * Predefined ScatterShapes that allow the specification of a shape a ScatterDataSet should be drawn with. + * If a ScatterShape is specified for a ScatterDataSet, the required renderer is set. + */ + public enum ScatterShape { + + SQUARE("SQUARE"), + CIRCLE("CIRCLE"), + TRIANGLE("TRIANGLE"), + CROSS("CROSS"), + X("X"), + CHEVRON_UP("CHEVRON_UP"), + CHEVRON_DOWN("CHEVRON_DOWN"); + + private final String shapeIdentifier; + + ScatterShape(final String shapeIdentifier) { + this.shapeIdentifier = shapeIdentifier; + } + + @Override + public String toString() { + return shapeIdentifier; + } + + public static ScatterShape[] getAllDefaultShapes() { + return new ScatterShape[]{SQUARE, CIRCLE, TRIANGLE, CROSS, X, CHEVRON_UP, CHEVRON_DOWN}; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/AxisBase.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/AxisBase.java new file mode 100644 index 0000000..c90b4fc --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/AxisBase.java @@ -0,0 +1,816 @@ + +package com.github.mikephil.charting.components; + +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.util.Log; + +import com.github.mikephil.charting.formatter.DefaultAxisValueFormatter; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base-class of all axes (previously called labels). + * + * @author Philipp Jahoda + */ +public abstract class AxisBase extends ComponentBase { + + /** + * custom formatter that is used instead of the auto-formatter if set + */ + protected IAxisValueFormatter mAxisValueFormatter; + + private int mGridColor = Color.GRAY; + + private float mGridLineWidth = 1f; + + private int mAxisLineColor = Color.GRAY; + + private float mAxisLineWidth = 1f; + + /** + * the actual array of entries + */ + public float[] mEntries = new float[]{}; + + /** + * axis label entries only used for centered labels + */ + public float[] mCenteredEntries = new float[]{}; + + /** + * the number of entries the legend contains + */ + public int mEntryCount; + + /** + * the number of decimal digits to use + */ + public int mDecimals; + + /** + * the number of label entries the axis should have, default 6 + */ + private int mLabelCount = 6; + + /** + * the minimum interval between axis values + */ + protected float mGranularity = 1.0f; + + /** + * When true, axis labels are controlled by the `granularity` property. + * When false, axis values could possibly be repeated. + * This could happen if two adjacent axis values are rounded to same value. + * If using granularity this could be avoided by having fewer axis values visible. + */ + protected boolean mGranularityEnabled = false; + + /** + * if true, the set number of y-labels will be forced + */ + protected boolean mForceLabels = false; + + /** + * flag indicating if the grid lines for this axis should be drawn + */ + protected boolean mDrawGridLines = true; + + /** + * flag that indicates if the line alongside the axis is drawn or not + */ + protected boolean mDrawAxisLine = true; + + /** + * flag that indicates of the labels of this axis should be drawn or not + */ + protected boolean mDrawLabels = true; + + protected boolean mCenterAxisLabels = false; + + /** + * the path effect of the axis line that makes dashed lines possible + */ + private DashPathEffect mAxisLineDashPathEffect = null; + + /** + * the path effect of the grid lines that makes dashed lines possible + */ + private DashPathEffect mGridDashPathEffect = null; + + /** + * array of limit lines that can be set for the axis + */ + protected List mLimitLines; + + /** + * flag indicating the limit lines layer depth + */ + protected boolean mDrawLimitLineBehindData = false; + + /** + * flag indicating the grid lines layer depth + */ + protected boolean mDrawGridLinesBehindData = true; + + /** + * Extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + protected float mSpaceMin = 0.f; + + /** + * Extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + protected float mSpaceMax = 0.f; + + /** + * flag indicating that the axis-min value has been customized + */ + protected boolean mCustomAxisMin = false; + + /** + * flag indicating that the axis-max value has been customized + */ + protected boolean mCustomAxisMax = false; + + /** + * don't touch this direclty, use setter + */ + public float mAxisMaximum = 0f; + + /** + * don't touch this directly, use setter + */ + public float mAxisMinimum = 0f; + + /** + * the total range of values this axis covers + */ + public float mAxisRange = 0f; + + private int mAxisMinLabels = 2; + private int mAxisMaxLabels = 25; + + /** + * The minumum number of labels on the axis + */ + public int getAxisMinLabels() { + return mAxisMinLabels; + } + + /** + * The minumum number of labels on the axis + */ + public void setAxisMinLabels(int labels) { + if (labels > 0) + mAxisMinLabels = labels; + } + + /** + * The maximum number of labels on the axis + */ + public int getAxisMaxLabels() { + return mAxisMaxLabels; + } + + /** + * The maximum number of labels on the axis + */ + public void setAxisMaxLabels(int labels) { + if (labels > 0) + mAxisMaxLabels = labels; + } + + /** + * default constructor + */ + public AxisBase() { + this.mTextSize = Utils.convertDpToPixel(10f); + this.mXOffset = Utils.convertDpToPixel(5f); + this.mYOffset = Utils.convertDpToPixel(5f); + this.mLimitLines = new ArrayList(); + } + + /** + * Set this to true to enable drawing the grid lines for this axis. + * + * @param enabled + */ + public void setDrawGridLines(boolean enabled) { + mDrawGridLines = enabled; + } + + /** + * Returns true if drawing grid lines is enabled for this axis. + * + * @return + */ + public boolean isDrawGridLinesEnabled() { + return mDrawGridLines; + } + + /** + * Set this to true if the line alongside the axis should be drawn or not. + * + * @param enabled + */ + public void setDrawAxisLine(boolean enabled) { + mDrawAxisLine = enabled; + } + + /** + * Returns true if the line alongside the axis should be drawn. + * + * @return + */ + public boolean isDrawAxisLineEnabled() { + return mDrawAxisLine; + } + + /** + * Centers the axis labels instead of drawing them at their original position. + * This is useful especially for grouped BarChart. + * + * @param enabled + */ + public void setCenterAxisLabels(boolean enabled) { + mCenterAxisLabels = enabled; + } + + public boolean isCenterAxisLabelsEnabled() { + return mCenterAxisLabels && mEntryCount > 0; + } + + /** + * Sets the color of the grid lines for this axis (the horizontal lines + * coming from each label). + * + * @param color + */ + public void setGridColor(int color) { + mGridColor = color; + } + + /** + * Returns the color of the grid lines for this axis (the horizontal lines + * coming from each label). + * + * @return + */ + public int getGridColor() { + return mGridColor; + } + + /** + * Sets the width of the border surrounding the chart in dp. + * + * @param width + */ + public void setAxisLineWidth(float width) { + mAxisLineWidth = Utils.convertDpToPixel(width); + } + + /** + * Returns the width of the axis line (line alongside the axis). + * + * @return + */ + public float getAxisLineWidth() { + return mAxisLineWidth; + } + + /** + * Sets the width of the grid lines that are drawn away from each axis + * label. + * + * @param width + */ + public void setGridLineWidth(float width) { + mGridLineWidth = Utils.convertDpToPixel(width); + } + + /** + * Returns the width of the grid lines that are drawn away from each axis + * label. + * + * @return + */ + public float getGridLineWidth() { + return mGridLineWidth; + } + + /** + * Sets the color of the border surrounding the chart. + * + * @param color + */ + public void setAxisLineColor(int color) { + mAxisLineColor = color; + } + + /** + * Returns the color of the axis line (line alongside the axis). + * + * @return + */ + public int getAxisLineColor() { + return mAxisLineColor; + } + + /** + * Set this to true to enable drawing the labels of this axis (this will not + * affect drawing the grid lines or axis lines). + * + * @param enabled + */ + public void setDrawLabels(boolean enabled) { + mDrawLabels = enabled; + } + + /** + * Returns true if drawing the labels is enabled for this axis. + * + * @return + */ + public boolean isDrawLabelsEnabled() { + return mDrawLabels; + } + + /** + * Sets the number of label entries for the y-axis max = 25, min = 2, default: 6, be aware + * that this number is not fixed. + * + * @param count the number of y-axis labels that should be displayed + */ + public void setLabelCount(int count) { + + if (count > getAxisMaxLabels()) + count = getAxisMaxLabels(); + if (count < getAxisMinLabels()) + count = getAxisMinLabels(); + + mLabelCount = count; + mForceLabels = false; + } + + /** + * sets the number of label entries for the y-axis max = 25, min = 2, default: 6, be aware + * that this number is not + * fixed (if force == false) and can only be approximated. + * + * @param count the number of y-axis labels that should be displayed + * @param force if enabled, the set label count will be forced, meaning that the exact + * specified count of labels will + * be drawn and evenly distributed alongside the axis - this might cause labels + * to have uneven values + */ + public void setLabelCount(int count, boolean force) { + + setLabelCount(count); + mForceLabels = force; + } + + /** + * Returns true if focing the y-label count is enabled. Default: false + * + * @return + */ + public boolean isForceLabelsEnabled() { + return mForceLabels; + } + + /** + * Returns the number of label entries the y-axis should have + * + * @return + */ + public int getLabelCount() { + return mLabelCount; + } + + /** + * @return true if granularity is enabled + */ + public boolean isGranularityEnabled() { + return mGranularityEnabled; + } + + /** + * Enabled/disable granularity control on axis value intervals. If enabled, the axis + * interval is not allowed to go below a certain granularity. Default: false + * + * @param enabled + */ + public void setGranularityEnabled(boolean enabled) { + mGranularityEnabled = enabled; + } + + /** + * @return the minimum interval between axis values + */ + public float getGranularity() { + return mGranularity; + } + + /** + * Set a minimum interval for the axis when zooming in. The axis is not allowed to go below + * that limit. This can be used to avoid label duplicating when zooming in. + * + * @param granularity + */ + public void setGranularity(float granularity) { + mGranularity = granularity; + // set this to true if it was disabled, as it makes no sense to call this method with granularity disabled + mGranularityEnabled = true; + } + + /** + * Adds a new LimitLine to this axis. + * + * @param l + */ + public void addLimitLine(LimitLine l) { + mLimitLines.add(l); + + if (mLimitLines.size() > 6) { + Log.e("MPAndroiChart", + "Warning! You have more than 6 LimitLines on your axis, do you really want " + + "that?"); + } + } + + /** + * Removes the specified LimitLine from the axis. + * + * @param l + */ + public void removeLimitLine(LimitLine l) { + mLimitLines.remove(l); + } + + /** + * Removes all LimitLines from the axis. + */ + public void removeAllLimitLines() { + mLimitLines.clear(); + } + + /** + * Returns the LimitLines of this axis. + * + * @return + */ + public List getLimitLines() { + return mLimitLines; + } + + /** + * If this is set to true, the LimitLines are drawn behind the actual data, + * otherwise on top. Default: false + * + * @param enabled + */ + public void setDrawLimitLinesBehindData(boolean enabled) { + mDrawLimitLineBehindData = enabled; + } + + public boolean isDrawLimitLinesBehindDataEnabled() { + return mDrawLimitLineBehindData; + } + + /** + * If this is set to false, the grid lines are draw on top of the actual data, + * otherwise behind. Default: true + * + * @param enabled + */ + public void setDrawGridLinesBehindData(boolean enabled) { mDrawGridLinesBehindData = enabled; } + + public boolean isDrawGridLinesBehindDataEnabled() { + return mDrawGridLinesBehindData; + } + + /** + * Returns the longest formatted label (in terms of characters), this axis + * contains. + * + * @return + */ + public String getLongestLabel() { + + String longest = ""; + + for (int i = 0; i < mEntries.length; i++) { + String text = getFormattedLabel(i); + + if (text != null && longest.length() < text.length()) + longest = text; + } + + return longest; + } + + public String getFormattedLabel(int index) { + + if (index < 0 || index >= mEntries.length) + return ""; + else + return getValueFormatter().getFormattedValue(mEntries[index], this); + } + + /** + * Sets the formatter to be used for formatting the axis labels. If no formatter is set, the + * chart will + * automatically determine a reasonable formatting (concerning decimals) for all the values + * that are drawn inside + * the chart. Use chart.getDefaultValueFormatter() to use the formatter calculated by the chart. + * + * @param f + */ + public void setValueFormatter(IAxisValueFormatter f) { + + if (f == null) + mAxisValueFormatter = new DefaultAxisValueFormatter(mDecimals); + else + mAxisValueFormatter = f; + } + + /** + * Returns the formatter used for formatting the axis labels. + * + * @return + */ + public IAxisValueFormatter getValueFormatter() { + + if (mAxisValueFormatter == null || + (mAxisValueFormatter instanceof DefaultAxisValueFormatter && + ((DefaultAxisValueFormatter)mAxisValueFormatter).getDecimalDigits() != mDecimals)) + mAxisValueFormatter = new DefaultAxisValueFormatter(mDecimals); + + return mAxisValueFormatter; + } + + /** + * Enables the grid line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space in between the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableGridDashedLine(float lineLength, float spaceLength, float phase) { + mGridDashPathEffect = new DashPathEffect(new float[]{ + lineLength, spaceLength + }, phase); + } + + /** + * Enables the grid line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param effect the DashPathEffect + */ + public void setGridDashedLine(DashPathEffect effect) { + mGridDashPathEffect = effect; + } + + /** + * Disables the grid line to be drawn in dashed mode. + */ + public void disableGridDashedLine() { + mGridDashPathEffect = null; + } + + /** + * Returns true if the grid dashed-line effect is enabled, false if not. + * + * @return + */ + public boolean isGridDashedLineEnabled() { + return mGridDashPathEffect == null ? false : true; + } + + /** + * returns the DashPathEffect that is set for grid line + * + * @return + */ + public DashPathEffect getGridDashPathEffect() { + return mGridDashPathEffect; + } + + + /** + * Enables the axis line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space in between the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableAxisLineDashedLine(float lineLength, float spaceLength, float phase) { + mAxisLineDashPathEffect = new DashPathEffect(new float[]{ + lineLength, spaceLength + }, phase); + } + + /** + * Enables the axis line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param effect the DashPathEffect + */ + public void setAxisLineDashedLine(DashPathEffect effect) { + mAxisLineDashPathEffect = effect; + } + + /** + * Disables the axis line to be drawn in dashed mode. + */ + public void disableAxisLineDashedLine() { + mAxisLineDashPathEffect = null; + } + + /** + * Returns true if the axis dashed-line effect is enabled, false if not. + * + * @return + */ + public boolean isAxisLineDashedLineEnabled() { + return mAxisLineDashPathEffect == null ? false : true; + } + + /** + * returns the DashPathEffect that is set for axis line + * + * @return + */ + public DashPathEffect getAxisLineDashPathEffect() { + return mAxisLineDashPathEffect; + } + + /** + * ###### BELOW CODE RELATED TO CUSTOM AXIS VALUES ###### + */ + + public float getAxisMaximum() { + return mAxisMaximum; + } + + public float getAxisMinimum() { + return mAxisMinimum; + } + + /** + * By calling this method, any custom maximum value that has been previously set is reseted, + * and the calculation is + * done automatically. + */ + public void resetAxisMaximum() { + mCustomAxisMax = false; + } + + /** + * Returns true if the axis max value has been customized (and is not calculated automatically) + * + * @return + */ + public boolean isAxisMaxCustom() { + return mCustomAxisMax; + } + + /** + * By calling this method, any custom minimum value that has been previously set is reseted, + * and the calculation is + * done automatically. + */ + public void resetAxisMinimum() { + mCustomAxisMin = false; + } + + /** + * Returns true if the axis min value has been customized (and is not calculated automatically) + * + * @return + */ + public boolean isAxisMinCustom() { + return mCustomAxisMin; + } + + /** + * Set a custom minimum value for this axis. If set, this value will not be calculated + * automatically depending on + * the provided data. Use resetAxisMinValue() to undo this. Do not forget to call + * setStartAtZero(false) if you use + * this method. Otherwise, the axis-minimum value will still be forced to 0. + * + * @param min + */ + public void setAxisMinimum(float min) { + mCustomAxisMin = true; + mAxisMinimum = min; + this.mAxisRange = Math.abs(mAxisMaximum - min); + } + + /** + * Use setAxisMinimum(...) instead. + * + * @param min + */ + @Deprecated + public void setAxisMinValue(float min) { + setAxisMinimum(min); + } + + /** + * Set a custom maximum value for this axis. If set, this value will not be calculated + * automatically depending on + * the provided data. Use resetAxisMaxValue() to undo this. + * + * @param max + */ + public void setAxisMaximum(float max) { + mCustomAxisMax = true; + mAxisMaximum = max; + this.mAxisRange = Math.abs(max - mAxisMinimum); + } + + /** + * Use setAxisMaximum(...) instead. + * + * @param max + */ + @Deprecated + public void setAxisMaxValue(float max) { + setAxisMaximum(max); + } + + /** + * Calculates the minimum / maximum and range values of the axis with the given + * minimum and maximum values from the chart data. + * + * @param dataMin the min value according to chart data + * @param dataMax the max value according to chart data + */ + public void calculate(float dataMin, float dataMax) { + + // if custom, use value as is, else use data value + float min = mCustomAxisMin ? mAxisMinimum : (dataMin - mSpaceMin); + float max = mCustomAxisMax ? mAxisMaximum : (dataMax + mSpaceMax); + + // temporary range (before calculations) + float range = Math.abs(max - min); + + // in case all values are equal + if (range == 0f) { + max = max + 1f; + min = min - 1f; + } + + this.mAxisMinimum = min; + this.mAxisMaximum = max; + + // actual range + this.mAxisRange = Math.abs(max - min); + } + + /** + * Gets extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + public float getSpaceMin() + { + return mSpaceMin; + } + + /** + * Sets extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + public void setSpaceMin(float mSpaceMin) + { + this.mSpaceMin = mSpaceMin; + } + + /** + * Gets extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + public float getSpaceMax() + { + return mSpaceMax; + } + + /** + * Sets extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + public void setSpaceMax(float mSpaceMax) + { + this.mSpaceMax = mSpaceMax; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/ComponentBase.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/ComponentBase.java new file mode 100644 index 0000000..d3a1d4d --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/ComponentBase.java @@ -0,0 +1,173 @@ + +package com.github.mikephil.charting.components; + +import android.graphics.Color; +import android.graphics.Typeface; + +import com.github.mikephil.charting.utils.Utils; + +/** + * This class encapsulates everything both Axis, Legend and LimitLines have in common. + * + * @author Philipp Jahoda + */ +public abstract class ComponentBase { + + /** + * flag that indicates if this axis / legend is enabled or not + */ + protected boolean mEnabled = true; + + /** + * the offset in pixels this component has on the x-axis + */ + protected float mXOffset = 5f; + + /** + * the offset in pixels this component has on the Y-axis + */ + protected float mYOffset = 5f; + + /** + * the typeface used for the labels + */ + protected Typeface mTypeface = null; + + /** + * the text size of the labels + */ + protected float mTextSize = Utils.convertDpToPixel(10f); + + /** + * the text color to use for the labels + */ + protected int mTextColor = Color.BLACK; + + + public ComponentBase() { + + } + + /** + * Returns the used offset on the x-axis for drawing the axis or legend + * labels. This offset is applied before and after the label. + * + * @return + */ + public float getXOffset() { + return mXOffset; + } + + /** + * Sets the used x-axis offset for the labels on this axis. + * + * @param xOffset + */ + public void setXOffset(float xOffset) { + mXOffset = Utils.convertDpToPixel(xOffset); + } + + /** + * Returns the used offset on the x-axis for drawing the axis labels. This + * offset is applied before and after the label. + * + * @return + */ + public float getYOffset() { + return mYOffset; + } + + /** + * Sets the used y-axis offset for the labels on this axis. For the legend, + * higher offset means the legend as a whole will be placed further away + * from the top. + * + * @param yOffset + */ + public void setYOffset(float yOffset) { + mYOffset = Utils.convertDpToPixel(yOffset); + } + + /** + * returns the Typeface used for the labels, returns null if none is set + * + * @return + */ + public Typeface getTypeface() { + return mTypeface; + } + + /** + * sets a specific Typeface for the labels + * + * @param tf + */ + public void setTypeface(Typeface tf) { + mTypeface = tf; + } + + /** + * sets the size of the label text in density pixels min = 6f, max = 24f, default + * 10f + * + * @param size the text size, in DP + */ + public void setTextSize(float size) { + + if (size > 24f) + size = 24f; + if (size < 6f) + size = 6f; + + mTextSize = Utils.convertDpToPixel(size); + } + + /** + * returns the text size that is currently set for the labels, in pixels + * + * @return + */ + public float getTextSize() { + return mTextSize; + } + + + /** + * Sets the text color to use for the labels. Make sure to use + * getResources().getColor(...) when using a color from the resources. + * + * @param color + */ + public void setTextColor(int color) { + mTextColor = color; + } + + /** + * Returns the text color that is set for the labels. + * + * @return + */ + public int getTextColor() { + return mTextColor; + } + + /** + * Set this to true if this component should be enabled (should be drawn), + * false if not. If disabled, nothing of this component will be drawn. + * Default: true + * + * @param enabled + */ + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + /** + * Returns true if this comonent is enabled (should be drawn), false if not. + * + * @return + */ + public boolean isEnabled() { + return mEnabled; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/Description.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/Description.java new file mode 100644 index 0000000..18294a3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/Description.java @@ -0,0 +1,95 @@ +package com.github.mikephil.charting.components; + +import android.graphics.Paint; + +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +/** + * Created by Philipp Jahoda on 17/09/16. + */ +public class Description extends ComponentBase { + + /** + * the text used in the description + */ + private String text = "Description Label"; + + /** + * the custom position of the description text + */ + private MPPointF mPosition; + + /** + * the alignment of the description text + */ + private Paint.Align mTextAlign = Paint.Align.RIGHT; + + public Description() { + super(); + + // default size + mTextSize = Utils.convertDpToPixel(8f); + } + + /** + * Sets the text to be shown as the description. + * Never set this to null as this will cause nullpointer exception when drawing with Android Canvas. + * + * @param text + */ + public void setText(String text) { + this.text = text; + } + + /** + * Returns the description text. + * + * @return + */ + public String getText() { + return text; + } + + /** + * Sets a custom position for the description text in pixels on the screen. + * + * @param x - xcoordinate + * @param y - ycoordinate + */ + public void setPosition(float x, float y) { + if (mPosition == null) { + mPosition = MPPointF.getInstance(x, y); + } else { + mPosition.x = x; + mPosition.y = y; + } + } + + /** + * Returns the customized position of the description, or null if none set. + * + * @return + */ + public MPPointF getPosition() { + return mPosition; + } + + /** + * Sets the text alignment of the description text. Default RIGHT. + * + * @param align + */ + public void setTextAlign(Paint.Align align) { + this.mTextAlign = align; + } + + /** + * Returns the text alignment of the description. + * + * @return + */ + public Paint.Align getTextAlign() { + return mTextAlign; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/IMarker.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/IMarker.java new file mode 100644 index 0000000..3b8ca43 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/IMarker.java @@ -0,0 +1,47 @@ +package com.github.mikephil.charting.components; + +import android.graphics.Canvas; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.MPPointF; + +public interface IMarker { + + /** + * @return The desired (general) offset you wish the IMarker to have on the x- and y-axis. + * By returning x: -(width / 2) you will center the IMarker horizontally. + * By returning y: -(height / 2) you will center the IMarker vertically. + */ + MPPointF getOffset(); + + /** + * @return The offset for drawing at the specific `point`. This allows conditional adjusting of the Marker position. + * If you have no adjustments to make, return getOffset(). + * + * @param posX This is the X position at which the marker wants to be drawn. + * You can adjust the offset conditionally based on this argument. + * @param posY This is the X position at which the marker wants to be drawn. + * You can adjust the offset conditionally based on this argument. + */ + MPPointF getOffsetForDrawingAtPoint(float posX, float posY); + + /** + * This method enables a specified custom IMarker to update it's content every time the IMarker is redrawn. + * + * @param e The Entry the IMarker belongs to. This can also be any subclass of Entry, like BarEntry or + * CandleEntry, simply cast it at runtime. + * @param highlight The highlight object contains information about the highlighted value such as it's dataset-index, the + * selected range or stack-index (only stacked bar entries). + */ + void refreshContent(Entry e, Highlight highlight); + + /** + * Draws the IMarker on the given position on the screen with the given Canvas object. + * + * @param canvas + * @param posX + * @param posY + */ + void draw(Canvas canvas, float posX, float posY); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/Legend.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/Legend.java new file mode 100644 index 0000000..7081292 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/Legend.java @@ -0,0 +1,825 @@ +package com.github.mikephil.charting.components; + +import android.graphics.DashPathEffect; +import android.graphics.Paint; + +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class representing the legend of the chart. The legend will contain one entry + * per color and DataSet. Multiple colors in one DataSet are grouped together. + * The legend object is NOT available before setting data to the chart. + * + * @author Philipp Jahoda + */ +public class Legend extends ComponentBase { + + public enum LegendForm { + /** + * Avoid drawing a form + */ + NONE, + + /** + * Do not draw the a form, but leave space for it + */ + EMPTY, + + /** + * Use default (default dataset's form to the legend's form) + */ + DEFAULT, + + /** + * Draw a square + */ + SQUARE, + + /** + * Draw a circle + */ + CIRCLE, + + /** + * Draw a horizontal line + */ + LINE + } + + public enum LegendHorizontalAlignment { + LEFT, CENTER, RIGHT + } + + public enum LegendVerticalAlignment { + TOP, CENTER, BOTTOM + } + + public enum LegendOrientation { + HORIZONTAL, VERTICAL + } + + public enum LegendDirection { + LEFT_TO_RIGHT, RIGHT_TO_LEFT + } + + /** + * The legend entries array + */ + private LegendEntry[] mEntries = new LegendEntry[]{}; + + /** + * Entries that will be appended to the end of the auto calculated entries after calculating the legend. + * (if the legend has already been calculated, you will need to call notifyDataSetChanged() to let the changes take effect) + */ + private LegendEntry[] mExtraEntries; + + /** + * Are the legend labels/colors a custom value or auto calculated? If false, + * then it's auto, if true, then custom. default false (automatic legend) + */ + private boolean mIsLegendCustom = false; + + private LegendHorizontalAlignment mHorizontalAlignment = LegendHorizontalAlignment.LEFT; + private LegendVerticalAlignment mVerticalAlignment = LegendVerticalAlignment.BOTTOM; + private LegendOrientation mOrientation = LegendOrientation.HORIZONTAL; + private boolean mDrawInside = false; + + /** + * the text direction for the legend + */ + private LegendDirection mDirection = LegendDirection.LEFT_TO_RIGHT; + + /** + * the shape/form the legend colors are drawn in + */ + private LegendForm mShape = LegendForm.SQUARE; + + /** + * the size of the legend forms/shapes + */ + private float mFormSize = 8f; + + /** + * the size of the legend forms/shapes + */ + private float mFormLineWidth = 3f; + + /** + * Line dash path effect used for shapes that consist of lines. + */ + private DashPathEffect mFormLineDashEffect = null; + + /** + * the space between the legend entries on a horizontal axis, default 6f + */ + private float mXEntrySpace = 6f; + + /** + * the space between the legend entries on a vertical axis, default 5f + */ + private float mYEntrySpace = 0f; + + /** + * the space between the legend entries on a vertical axis, default 2f + * private float mYEntrySpace = 2f; /** the space between the form and the + * actual label/text + */ + private float mFormToTextSpace = 5f; + + /** + * the space that should be left between stacked forms + */ + private float mStackSpace = 3f; + + /** + * the maximum relative size out of the whole chart view in percent + */ + private float mMaxSizePercent = 0.95f; + + /** + * default constructor + */ + public Legend() { + + this.mTextSize = Utils.convertDpToPixel(10f); + this.mXOffset = Utils.convertDpToPixel(5f); + this.mYOffset = Utils.convertDpToPixel(3f); // 2 + } + + /** + * Constructor. Provide entries for the legend. + * + * @param entries + */ + public Legend(LegendEntry[] entries) { + this(); + + if (entries == null) { + throw new IllegalArgumentException("entries array is NULL"); + } + + this.mEntries = entries; + } + + /** + * This method sets the automatically computed colors for the legend. Use setCustom(...) to set custom colors. + * + * @param entries + */ + public void setEntries(List entries) { + mEntries = entries.toArray(new LegendEntry[entries.size()]); + } + + public LegendEntry[] getEntries() { + return mEntries; + } + + /** + * returns the maximum length in pixels across all legend labels + formsize + * + formtotextspace + * + * @param p the paint object used for rendering the text + * @return + */ + public float getMaximumEntryWidth(Paint p) { + + float max = 0f; + float maxFormSize = 0f; + float formToTextSpace = Utils.convertDpToPixel(mFormToTextSpace); + + for (LegendEntry entry : mEntries) { + final float formSize = Utils.convertDpToPixel( + Float.isNaN(entry.formSize) + ? mFormSize : entry.formSize); + if (formSize > maxFormSize) + maxFormSize = formSize; + + String label = entry.label; + if (label == null) continue; + + float length = (float) Utils.calcTextWidth(p, label); + + if (length > max) + max = length; + } + + return max + maxFormSize + formToTextSpace; + } + + /** + * returns the maximum height in pixels across all legend labels + * + * @param p the paint object used for rendering the text + * @return + */ + public float getMaximumEntryHeight(Paint p) { + + float max = 0f; + + for (LegendEntry entry : mEntries) { + String label = entry.label; + if (label == null) continue; + + float length = (float) Utils.calcTextHeight(p, label); + + if (length > max) + max = length; + } + + return max; + } + + public LegendEntry[] getExtraEntries() { + + return mExtraEntries; + } + + public void setExtra(List entries) { + mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); + } + + public void setExtra(LegendEntry[] entries) { + if (entries == null) + entries = new LegendEntry[]{}; + mExtraEntries = entries; + } + + /** + * Entries that will be appended to the end of the auto calculated + * entries after calculating the legend. + * (if the legend has already been calculated, you will need to call notifyDataSetChanged() + * to let the changes take effect) + */ + public void setExtra(int[] colors, String[] labels) { + + List entries = new ArrayList<>(); + + for (int i = 0; i < Math.min(colors.length, labels.length); i++) { + final LegendEntry entry = new LegendEntry(); + entry.formColor = colors[i]; + entry.label = labels[i]; + + if (entry.formColor == ColorTemplate.COLOR_SKIP || + entry.formColor == 0) + entry.form = LegendForm.NONE; + else if (entry.formColor == ColorTemplate.COLOR_NONE) + entry.form = LegendForm.EMPTY; + + entries.add(entry); + } + + mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); + } + + /** + * Sets a custom legend's entries array. + * * A null label will start a group. + * This will disable the feature that automatically calculates the legend + * entries from the datasets. + * Call resetCustom() to re-enable automatic calculation (and then + * notifyDataSetChanged() is needed to auto-calculate the legend again) + */ + public void setCustom(LegendEntry[] entries) { + + mEntries = entries; + mIsLegendCustom = true; + } + + /** + * Sets a custom legend's entries array. + * * A null label will start a group. + * This will disable the feature that automatically calculates the legend + * entries from the datasets. + * Call resetCustom() to re-enable automatic calculation (and then + * notifyDataSetChanged() is needed to auto-calculate the legend again) + */ + public void setCustom(List entries) { + + mEntries = entries.toArray(new LegendEntry[entries.size()]); + mIsLegendCustom = true; + } + + /** + * Calling this will disable the custom legend entries (set by + * setCustom(...)). Instead, the entries will again be calculated + * automatically (after notifyDataSetChanged() is called). + */ + public void resetCustom() { + mIsLegendCustom = false; + } + + /** + * @return true if a custom legend entries has been set default + * false (automatic legend) + */ + public boolean isLegendCustom() { + return mIsLegendCustom; + } + + /** + * returns the horizontal alignment of the legend + * + * @return + */ + public LegendHorizontalAlignment getHorizontalAlignment() { + return mHorizontalAlignment; + } + + /** + * sets the horizontal alignment of the legend + * + * @param value + */ + public void setHorizontalAlignment(LegendHorizontalAlignment value) { + mHorizontalAlignment = value; + } + + /** + * returns the vertical alignment of the legend + * + * @return + */ + public LegendVerticalAlignment getVerticalAlignment() { + return mVerticalAlignment; + } + + /** + * sets the vertical alignment of the legend + * + * @param value + */ + public void setVerticalAlignment(LegendVerticalAlignment value) { + mVerticalAlignment = value; + } + + /** + * returns the orientation of the legend + * + * @return + */ + public LegendOrientation getOrientation() { + return mOrientation; + } + + /** + * sets the orientation of the legend + * + * @param value + */ + public void setOrientation(LegendOrientation value) { + mOrientation = value; + } + + /** + * returns whether the legend will draw inside the chart or outside + * + * @return + */ + public boolean isDrawInsideEnabled() { + return mDrawInside; + } + + /** + * sets whether the legend will draw inside the chart or outside + * + * @param value + */ + public void setDrawInside(boolean value) { + mDrawInside = value; + } + + /** + * returns the text direction of the legend + * + * @return + */ + public LegendDirection getDirection() { + return mDirection; + } + + /** + * sets the text direction of the legend + * + * @param pos + */ + public void setDirection(LegendDirection pos) { + mDirection = pos; + } + + /** + * returns the current form/shape that is set for the legend + * + * @return + */ + public LegendForm getForm() { + return mShape; + } + + /** + * sets the form/shape of the legend forms + * + * @param shape + */ + public void setForm(LegendForm shape) { + mShape = shape; + } + + /** + * sets the size in dp of the legend forms, default 8f + * + * @param size + */ + public void setFormSize(float size) { + mFormSize = size; + } + + /** + * returns the size in dp of the legend forms + * + * @return + */ + public float getFormSize() { + return mFormSize; + } + + /** + * sets the line width in dp for forms that consist of lines, default 3f + * + * @param size + */ + public void setFormLineWidth(float size) { + mFormLineWidth = size; + } + + /** + * returns the line width in dp for drawing forms that consist of lines + * + * @return + */ + public float getFormLineWidth() { + return mFormLineWidth; + } + + /** + * Sets the line dash path effect used for shapes that consist of lines. + * + * @param dashPathEffect + */ + public void setFormLineDashEffect(DashPathEffect dashPathEffect) { + mFormLineDashEffect = dashPathEffect; + } + + /** + * @return The line dash path effect used for shapes that consist of lines. + */ + public DashPathEffect getFormLineDashEffect() { + return mFormLineDashEffect; + } + + /** + * returns the space between the legend entries on a horizontal axis in + * pixels + * + * @return + */ + public float getXEntrySpace() { + return mXEntrySpace; + } + + /** + * sets the space between the legend entries on a horizontal axis in pixels, + * converts to dp internally + * + * @param space + */ + public void setXEntrySpace(float space) { + mXEntrySpace = space; + } + + /** + * returns the space between the legend entries on a vertical axis in pixels + * + * @return + */ + public float getYEntrySpace() { + return mYEntrySpace; + } + + /** + * sets the space between the legend entries on a vertical axis in pixels, + * converts to dp internally + * + * @param space + */ + public void setYEntrySpace(float space) { + mYEntrySpace = space; + } + + /** + * returns the space between the form and the actual label/text + * + * @return + */ + public float getFormToTextSpace() { + return mFormToTextSpace; + } + + /** + * sets the space between the form and the actual label/text, converts to dp + * internally + * + * @param space + */ + public void setFormToTextSpace(float space) { + this.mFormToTextSpace = space; + } + + /** + * returns the space that is left out between stacked forms (with no label) + * + * @return + */ + public float getStackSpace() { + return mStackSpace; + } + + /** + * sets the space that is left out between stacked forms (with no label) + * + * @param space + */ + public void setStackSpace(float space) { + mStackSpace = space; + } + + /** + * the total width of the legend (needed width space) + */ + public float mNeededWidth = 0f; + + /** + * the total height of the legend (needed height space) + */ + public float mNeededHeight = 0f; + + public float mTextHeightMax = 0f; + + public float mTextWidthMax = 0f; + + /** + * flag that indicates if word wrapping is enabled + */ + private boolean mWordWrapEnabled = false; + + /** + * Should the legend word wrap? / this is currently supported only for: + * BelowChartLeft, BelowChartRight, BelowChartCenter. / note that word + * wrapping a legend takes a toll on performance. / you may want to set + * maxSizePercent when word wrapping, to set the point where the text wraps. + * / default: false + * + * @param enabled + */ + public void setWordWrapEnabled(boolean enabled) { + mWordWrapEnabled = enabled; + } + + /** + * If this is set, then word wrapping the legend is enabled. This means the + * legend will not be cut off if too long. + * + * @return + */ + public boolean isWordWrapEnabled() { + return mWordWrapEnabled; + } + + /** + * The maximum relative size out of the whole chart view. / If the legend is + * to the right/left of the chart, then this affects the width of the + * legend. / If the legend is to the top/bottom of the chart, then this + * affects the height of the legend. / If the legend is the center of the + * piechart, then this defines the size of the rectangular bounds out of the + * size of the "hole". / default: 0.95f (95%) + * + * @return + */ + public float getMaxSizePercent() { + return mMaxSizePercent; + } + + /** + * The maximum relative size out of the whole chart view. / If + * the legend is to the right/left of the chart, then this affects the width + * of the legend. / If the legend is to the top/bottom of the chart, then + * this affects the height of the legend. / default: 0.95f (95%) + * + * @param maxSize + */ + public void setMaxSizePercent(float maxSize) { + mMaxSizePercent = maxSize; + } + + private List mCalculatedLabelSizes = new ArrayList<>(16); + private List mCalculatedLabelBreakPoints = new ArrayList<>(16); + private List mCalculatedLineSizes = new ArrayList<>(16); + + public List getCalculatedLabelSizes() { + return mCalculatedLabelSizes; + } + + public List getCalculatedLabelBreakPoints() { + return mCalculatedLabelBreakPoints; + } + + public List getCalculatedLineSizes() { + return mCalculatedLineSizes; + } + + /** + * Calculates the dimensions of the Legend. This includes the maximum width + * and height of a single entry, as well as the total width and height of + * the Legend. + * + * @param labelpaint + */ + public void calculateDimensions(Paint labelpaint, ViewPortHandler viewPortHandler) { + + float defaultFormSize = Utils.convertDpToPixel(mFormSize); + float stackSpace = Utils.convertDpToPixel(mStackSpace); + float formToTextSpace = Utils.convertDpToPixel(mFormToTextSpace); + float xEntrySpace = Utils.convertDpToPixel(mXEntrySpace); + float yEntrySpace = Utils.convertDpToPixel(mYEntrySpace); + boolean wordWrapEnabled = mWordWrapEnabled; + LegendEntry[] entries = mEntries; + int entryCount = entries.length; + + mTextWidthMax = getMaximumEntryWidth(labelpaint); + mTextHeightMax = getMaximumEntryHeight(labelpaint); + + switch (mOrientation) { + case VERTICAL: { + + float maxWidth = 0f, maxHeight = 0f, width = 0f; + float labelLineHeight = Utils.getLineHeight(labelpaint); + boolean wasStacked = false; + + for (int i = 0; i < entryCount; i++) { + + LegendEntry e = entries[i]; + boolean drawingForm = e.form != LegendForm.NONE; + float formSize = Float.isNaN(e.formSize) + ? defaultFormSize + : Utils.convertDpToPixel(e.formSize); + String label = e.label; + + if (!wasStacked) + width = 0.f; + + if (drawingForm) { + if (wasStacked) + width += stackSpace; + width += formSize; + } + + // grouped forms have null labels + if (label != null) { + + // make a step to the left + if (drawingForm && !wasStacked) + width += formToTextSpace; + else if (wasStacked) { + maxWidth = Math.max(maxWidth, width); + maxHeight += labelLineHeight + yEntrySpace; + width = 0.f; + wasStacked = false; + } + + width += Utils.calcTextWidth(labelpaint, label); + + maxHeight += labelLineHeight + yEntrySpace; + } else { + wasStacked = true; + width += formSize; + if (i < entryCount - 1) + width += stackSpace; + } + + maxWidth = Math.max(maxWidth, width); + } + + mNeededWidth = maxWidth; + mNeededHeight = maxHeight; + + break; + } + case HORIZONTAL: { + + float labelLineHeight = Utils.getLineHeight(labelpaint); + float labelLineSpacing = Utils.getLineSpacing(labelpaint) + yEntrySpace; + float contentWidth = viewPortHandler.contentWidth() * mMaxSizePercent; + + // Start calculating layout + float maxLineWidth = 0.f; + float currentLineWidth = 0.f; + float requiredWidth = 0.f; + int stackedStartIndex = -1; + + mCalculatedLabelBreakPoints.clear(); + mCalculatedLabelSizes.clear(); + mCalculatedLineSizes.clear(); + + for (int i = 0; i < entryCount; i++) { + + LegendEntry e = entries[i]; + boolean drawingForm = e.form != LegendForm.NONE; + float formSize = Float.isNaN(e.formSize) + ? defaultFormSize + : Utils.convertDpToPixel(e.formSize); + String label = e.label; + + mCalculatedLabelBreakPoints.add(false); + + if (stackedStartIndex == -1) { + // we are not stacking, so required width is for this label + // only + requiredWidth = 0.f; + } else { + // add the spacing appropriate for stacked labels/forms + requiredWidth += stackSpace; + } + + // grouped forms have null labels + if (label != null) { + + mCalculatedLabelSizes.add(Utils.calcTextSize(labelpaint, label)); + requiredWidth += drawingForm ? formToTextSpace + formSize : 0.f; + requiredWidth += mCalculatedLabelSizes.get(i).width; + } else { + + mCalculatedLabelSizes.add(FSize.getInstance(0.f, 0.f)); + requiredWidth += drawingForm ? formSize : 0.f; + + if (stackedStartIndex == -1) { + // mark this index as we might want to break here later + stackedStartIndex = i; + } + } + + if (label != null || i == entryCount - 1) { + + float requiredSpacing = currentLineWidth == 0.f ? 0.f : xEntrySpace; + + if (!wordWrapEnabled // No word wrapping, it must fit. + // The line is empty, it must fit + || currentLineWidth == 0.f + // It simply fits + || (contentWidth - currentLineWidth >= + requiredSpacing + requiredWidth)) { + // Expand current line + currentLineWidth += requiredSpacing + requiredWidth; + } else { // It doesn't fit, we need to wrap a line + + // Add current line size to array + mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); + maxLineWidth = Math.max(maxLineWidth, currentLineWidth); + + // Start a new line + mCalculatedLabelBreakPoints.set( + stackedStartIndex > -1 ? stackedStartIndex + : i, true); + currentLineWidth = requiredWidth; + } + + if (i == entryCount - 1) { + // Add last line size to array + mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); + maxLineWidth = Math.max(maxLineWidth, currentLineWidth); + } + } + + stackedStartIndex = label != null ? -1 : stackedStartIndex; + } + + mNeededWidth = maxLineWidth; + mNeededHeight = labelLineHeight + * (float) (mCalculatedLineSizes.size()) + + labelLineSpacing * + (float) (mCalculatedLineSizes.size() == 0 + ? 0 + : (mCalculatedLineSizes.size() - 1)); + + break; + } + } + + mNeededHeight += mYOffset; + mNeededWidth += mXOffset; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/LegendEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/LegendEntry.java new file mode 100644 index 0000000..3acec0f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/LegendEntry.java @@ -0,0 +1,78 @@ +package com.github.mikephil.charting.components; + + +import android.graphics.DashPathEffect; + +import com.github.mikephil.charting.utils.ColorTemplate; + +public class LegendEntry { + public LegendEntry() { + + } + + /** + * + * @param label The legend entry text. A `null` label will start a group. + * @param form The form to draw for this entry. + * @param formSize Set to NaN to use the legend's default. + * @param formLineWidth Set to NaN to use the legend's default. + * @param formLineDashEffect Set to nil to use the legend's default. + * @param formColor The color for drawing the form. + */ + public LegendEntry(String label, + Legend.LegendForm form, + float formSize, + float formLineWidth, + DashPathEffect formLineDashEffect, + int formColor) + { + this.label = label; + this.form = form; + this.formSize = formSize; + this.formLineWidth = formLineWidth; + this.formLineDashEffect = formLineDashEffect; + this.formColor = formColor; + } + + /** + * The legend entry text. + * A `null` label will start a group. + */ + public String label; + + /** + * The form to draw for this entry. + * + * `NONE` will avoid drawing a form, and any related space. + * `EMPTY` will avoid drawing a form, but keep its space. + * `DEFAULT` will use the Legend's default. + */ + public Legend.LegendForm form = Legend.LegendForm.DEFAULT; + + /** + * Form size will be considered except for when .None is used + * + * Set as NaN to use the legend's default + */ + public float formSize = Float.NaN; + + /** + * Line width used for shapes that consist of lines. + * + * Set as NaN to use the legend's default + */ + public float formLineWidth = Float.NaN; + + /** + * Line dash path effect used for shapes that consist of lines. + * + * Set to null to use the legend's default + */ + public DashPathEffect formLineDashEffect = null; + + /** + * The color for drawing the form + */ + public int formColor = ColorTemplate.COLOR_NONE; + +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/LimitLine.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/LimitLine.java new file mode 100644 index 0000000..8fcdee8 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/LimitLine.java @@ -0,0 +1,215 @@ + +package com.github.mikephil.charting.components; + +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Typeface; + +import com.github.mikephil.charting.utils.Utils; + +/** + * The limit line is an additional feature for all Line-, Bar- and + * ScatterCharts. It allows the displaying of an additional line in the chart + * that marks a certain maximum / limit on the specified axis (x- or y-axis). + * + * @author Philipp Jahoda + */ +public class LimitLine extends ComponentBase { + + /** limit / maximum (the y-value or xIndex) */ + private float mLimit = 0f; + + /** the width of the limit line */ + private float mLineWidth = 2f; + + /** the color of the limit line */ + private int mLineColor = Color.rgb(237, 91, 91); + + /** the style of the label text */ + private Paint.Style mTextStyle = Paint.Style.FILL_AND_STROKE; + + /** label string that is drawn next to the limit line */ + private String mLabel = ""; + + /** the path effect of this LimitLine that makes dashed lines possible */ + private DashPathEffect mDashPathEffect = null; + + /** indicates the position of the LimitLine label */ + private LimitLabelPosition mLabelPosition = LimitLabelPosition.RIGHT_TOP; + + /** enum that indicates the position of the LimitLine label */ + public enum LimitLabelPosition { + LEFT_TOP, LEFT_BOTTOM, RIGHT_TOP, RIGHT_BOTTOM + } + + /** + * Constructor with limit. + * + * @param limit - the position (the value) on the y-axis (y-value) or x-axis + * (xIndex) where this line should appear + */ + public LimitLine(float limit) { + mLimit = limit; + } + + /** + * Constructor with limit and label. + * + * @param limit - the position (the value) on the y-axis (y-value) or x-axis + * (xIndex) where this line should appear + * @param label - provide "" if no label is required + */ + public LimitLine(float limit, String label) { + mLimit = limit; + mLabel = label; + } + + /** + * Returns the limit that is set for this line. + * + * @return + */ + public float getLimit() { + return mLimit; + } + + /** + * set the line width of the chart (min = 0.2f, max = 12f); default 2f NOTE: + * thinner line == better performance, thicker line == worse performance + * + * @param width + */ + public void setLineWidth(float width) { + + if (width < 0.2f) + width = 0.2f; + if (width > 12.0f) + width = 12.0f; + mLineWidth = Utils.convertDpToPixel(width); + } + + /** + * returns the width of limit line + * + * @return + */ + public float getLineWidth() { + return mLineWidth; + } + + /** + * Sets the linecolor for this LimitLine. Make sure to use + * getResources().getColor(...) + * + * @param color + */ + public void setLineColor(int color) { + mLineColor = color; + } + + /** + * Returns the color that is used for this LimitLine + * + * @return + */ + public int getLineColor() { + return mLineColor; + } + + /** + * Enables the line to be drawn in dashed mode, e.g. like this "- - - - - -" + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space inbetween the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableDashedLine(float lineLength, float spaceLength, float phase) { + mDashPathEffect = new DashPathEffect(new float[] { + lineLength, spaceLength + }, phase); + } + + /** + * Disables the line to be drawn in dashed mode. + */ + public void disableDashedLine() { + mDashPathEffect = null; + } + + /** + * Returns true if the dashed-line effect is enabled, false if not. Default: + * disabled + * + * @return + */ + public boolean isDashedLineEnabled() { + return mDashPathEffect == null ? false : true; + } + + /** + * returns the DashPathEffect that is set for this LimitLine + * + * @return + */ + public DashPathEffect getDashPathEffect() { + return mDashPathEffect; + } + + /** + * Sets the color of the value-text that is drawn next to the LimitLine. + * Default: Paint.Style.FILL_AND_STROKE + * + * @param style + */ + public void setTextStyle(Paint.Style style) { + this.mTextStyle = style; + } + + /** + * Returns the color of the value-text that is drawn next to the LimitLine. + * + * @return + */ + public Paint.Style getTextStyle() { + return mTextStyle; + } + + /** + * Sets the position of the LimitLine value label (either on the right or on + * the left edge of the chart). Not supported for RadarChart. + * + * @param pos + */ + public void setLabelPosition(LimitLabelPosition pos) { + mLabelPosition = pos; + } + + /** + * Returns the position of the LimitLine label (value). + * + * @return + */ + public LimitLabelPosition getLabelPosition() { + return mLabelPosition; + } + + /** + * Sets the label that is drawn next to the limit line. Provide "" if no + * label is required. + * + * @param label + */ + public void setLabel(String label) { + mLabel = label; + } + + /** + * Returns the label that is drawn next to the limit line. + * + * @return + */ + public String getLabel() { + return mLabel; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerImage.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerImage.java new file mode 100644 index 0000000..7bd7b8e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerImage.java @@ -0,0 +1,167 @@ +package com.github.mikephil.charting.components; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RelativeLayout; + +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.MPPointF; + +import java.lang.ref.WeakReference; + +/** + * View that can be displayed when selecting values in the chart. Extend this class to provide custom layouts for your + * markers. + * + * @author Philipp Jahoda + */ +public class MarkerImage implements IMarker { + + private Context mContext; + private Drawable mDrawable; + + private MPPointF mOffset = new MPPointF(); + private MPPointF mOffset2 = new MPPointF(); + private WeakReference mWeakChart; + + private FSize mSize = new FSize(); + private Rect mDrawableBoundsCache = new Rect(); + + /** + * Constructor. Sets up the MarkerView with a custom layout resource. + * + * @param context + * @param drawableResourceId the drawable resource to render + */ + public MarkerImage(Context context, int drawableResourceId) { + mContext = context; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + mDrawable = mContext.getResources().getDrawable(drawableResourceId, null); + } + else + { + mDrawable = mContext.getResources().getDrawable(drawableResourceId); + } + } + + public void setOffset(MPPointF offset) { + mOffset = offset; + + if (mOffset == null) { + mOffset = new MPPointF(); + } + } + + public void setOffset(float offsetX, float offsetY) { + mOffset.x = offsetX; + mOffset.y = offsetY; + } + + @Override + public MPPointF getOffset() { + return mOffset; + } + + public void setSize(FSize size) { + mSize = size; + + if (mSize == null) { + mSize = new FSize(); + } + } + + public FSize getSize() { + return mSize; + } + + public void setChartView(Chart chart) { + mWeakChart = new WeakReference<>(chart); + } + + public Chart getChartView() { + return mWeakChart == null ? null : mWeakChart.get(); + } + + @Override + public MPPointF getOffsetForDrawingAtPoint(float posX, float posY) { + + MPPointF offset = getOffset(); + mOffset2.x = offset.x; + mOffset2.y = offset.y; + + Chart chart = getChartView(); + + float width = mSize.width; + float height = mSize.height; + + if (width == 0.f && mDrawable != null) { + width = mDrawable.getIntrinsicWidth(); + } + if (height == 0.f && mDrawable != null) { + height = mDrawable.getIntrinsicHeight(); + } + + if (posX + mOffset2.x < 0) { + mOffset2.x = - posX; + } else if (chart != null && posX + width + mOffset2.x > chart.getWidth()) { + mOffset2.x = chart.getWidth() - posX - width; + } + + if (posY + mOffset2.y < 0) { + mOffset2.y = - posY; + } else if (chart != null && posY + height + mOffset2.y > chart.getHeight()) { + mOffset2.y = chart.getHeight() - posY - height; + } + + return mOffset2; + } + + @Override + public void refreshContent(Entry e, Highlight highlight) { + + } + + @Override + public void draw(Canvas canvas, float posX, float posY) { + + if (mDrawable == null) return; + + MPPointF offset = getOffsetForDrawingAtPoint(posX, posY); + + float width = mSize.width; + float height = mSize.height; + + if (width == 0.f) { + width = mDrawable.getIntrinsicWidth(); + } + if (height == 0.f) { + height = mDrawable.getIntrinsicHeight(); + } + + mDrawable.copyBounds(mDrawableBoundsCache); + mDrawable.setBounds( + mDrawableBoundsCache.left, + mDrawableBoundsCache.top, + mDrawableBoundsCache.left + (int)width, + mDrawableBoundsCache.top + (int)height); + + int saveId = canvas.save(); + // translate to the correct position and draw + canvas.translate(posX + offset.x, posY + offset.y); + mDrawable.draw(canvas); + canvas.restoreToCount(saveId); + + mDrawable.setBounds(mDrawableBoundsCache); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerView.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerView.java new file mode 100644 index 0000000..162e88e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerView.java @@ -0,0 +1,129 @@ +package com.github.mikephil.charting.components; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RelativeLayout; + +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.MPPointF; + +import java.lang.ref.WeakReference; + +/** + * View that can be displayed when selecting values in the chart. Extend this class to provide custom layouts for your + * markers. + * + * @author Philipp Jahoda + */ +public class MarkerView extends RelativeLayout implements IMarker { + + private MPPointF mOffset = new MPPointF(); + private MPPointF mOffset2 = new MPPointF(); + private WeakReference mWeakChart; + + /** + * Constructor. Sets up the MarkerView with a custom layout resource. + * + * @param context + * @param layoutResource the layout resource to use for the MarkerView + */ + public MarkerView(Context context, int layoutResource) { + super(context); + setupLayoutResource(layoutResource); + } + + /** + * Sets the layout resource for a custom MarkerView. + * + * @param layoutResource + */ + private void setupLayoutResource(int layoutResource) { + + View inflated = LayoutInflater.from(getContext()).inflate(layoutResource, this); + + inflated.setLayoutParams(new LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)); + inflated.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + + // measure(getWidth(), getHeight()); + inflated.layout(0, 0, inflated.getMeasuredWidth(), inflated.getMeasuredHeight()); + } + + public void setOffset(MPPointF offset) { + mOffset = offset; + + if (mOffset == null) { + mOffset = new MPPointF(); + } + } + + public void setOffset(float offsetX, float offsetY) { + mOffset.x = offsetX; + mOffset.y = offsetY; + } + + @Override + public MPPointF getOffset() { + return mOffset; + } + + public void setChartView(Chart chart) { + mWeakChart = new WeakReference<>(chart); + } + + public Chart getChartView() { + return mWeakChart == null ? null : mWeakChart.get(); + } + + @Override + public MPPointF getOffsetForDrawingAtPoint(float posX, float posY) { + + MPPointF offset = getOffset(); + mOffset2.x = offset.x; + mOffset2.y = offset.y; + + Chart chart = getChartView(); + + float width = getWidth(); + float height = getHeight(); + + if (posX + mOffset2.x < 0) { + mOffset2.x = - posX; + } else if (chart != null && posX + width + mOffset2.x > chart.getWidth()) { + mOffset2.x = chart.getWidth() - posX - width; + } + + if (posY + mOffset2.y < 0) { + mOffset2.y = - posY; + } else if (chart != null && posY + height + mOffset2.y > chart.getHeight()) { + mOffset2.y = chart.getHeight() - posY - height; + } + + return mOffset2; + } + + @Override + public void refreshContent(Entry e, Highlight highlight) { + + measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + layout(0, 0, getMeasuredWidth(), getMeasuredHeight()); + + } + + @Override + public void draw(Canvas canvas, float posX, float posY) { + + MPPointF offset = getOffsetForDrawingAtPoint(posX, posY); + + int saveId = canvas.save(); + // translate to the correct position and draw + canvas.translate(posX + offset.x, posY + offset.y); + draw(canvas); + canvas.restoreToCount(saveId); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/XAxis.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/XAxis.java new file mode 100644 index 0000000..77d4aaf --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/XAxis.java @@ -0,0 +1,118 @@ + +package com.github.mikephil.charting.components; + +import com.github.mikephil.charting.utils.Utils; + +/** + * Class representing the x-axis labels settings. Only use the setter methods to + * modify it. Do not access public variables directly. Be aware that not all + * features the XLabels class provides are suitable for the RadarChart. + * + * @author Philipp Jahoda + */ +public class XAxis extends AxisBase { + + /** + * width of the x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public int mLabelWidth = 1; + + /** + * height of the x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public int mLabelHeight = 1; + + /** + * width of the (rotated) x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public int mLabelRotatedWidth = 1; + + /** + * height of the (rotated) x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public int mLabelRotatedHeight = 1; + + /** + * This is the angle for drawing the X axis labels (in degrees) + */ + protected float mLabelRotationAngle = 0f; + + /** + * if set to true, the chart will avoid that the first and last label entry + * in the chart "clip" off the edge of the chart + */ + private boolean mAvoidFirstLastClipping = false; + + /** + * the position of the x-labels relative to the chart + */ + private XAxisPosition mPosition = XAxisPosition.TOP; + + /** + * enum for the position of the x-labels relative to the chart + */ + public enum XAxisPosition { + TOP, BOTTOM, BOTH_SIDED, TOP_INSIDE, BOTTOM_INSIDE + } + + public XAxis() { + super(); + + mYOffset = Utils.convertDpToPixel(4.f); // -3 + } + + /** + * returns the position of the x-labels + */ + public XAxisPosition getPosition() { + return mPosition; + } + + /** + * sets the position of the x-labels + * + * @param pos + */ + public void setPosition(XAxisPosition pos) { + mPosition = pos; + } + + /** + * returns the angle for drawing the X axis labels (in degrees) + */ + public float getLabelRotationAngle() { + return mLabelRotationAngle; + } + + /** + * sets the angle for drawing the X axis labels (in degrees) + * + * @param angle the angle in degrees + */ + public void setLabelRotationAngle(float angle) { + mLabelRotationAngle = angle; + } + + /** + * if set to true, the chart will avoid that the first and last label entry + * in the chart "clip" off the edge of the chart or the screen + * + * @param enabled + */ + public void setAvoidFirstLastClipping(boolean enabled) { + mAvoidFirstLastClipping = enabled; + } + + /** + * returns true if avoid-first-lastclipping is enabled, false if not + * + * @return + */ + public boolean isAvoidFirstLastClippingEnabled() { + return mAvoidFirstLastClipping; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/YAxis.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/YAxis.java new file mode 100644 index 0000000..d2071ec --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/YAxis.java @@ -0,0 +1,467 @@ +package com.github.mikephil.charting.components; + +import android.graphics.Color; +import android.graphics.Paint; + +import com.github.mikephil.charting.utils.Utils; + +/** + * Class representing the y-axis labels settings and its entries. Only use the setter methods to + * modify it. Do not + * access public variables directly. Be aware that not all features the YLabels class provides + * are suitable for the + * RadarChart. Customizations that affect the value range of the axis need to be applied before + * setting data for the + * chart. + * + * @author Philipp Jahoda + */ +public class YAxis extends AxisBase { + + /** + * indicates if the bottom y-label entry is drawn or not + */ + private boolean mDrawBottomYLabelEntry = true; + + /** + * indicates if the top y-label entry is drawn or not + */ + private boolean mDrawTopYLabelEntry = true; + + /** + * flag that indicates if the axis is inverted or not + */ + protected boolean mInverted = false; + + /** + * flag that indicates if the zero-line should be drawn regardless of other grid lines + */ + protected boolean mDrawZeroLine = false; + + /** + * flag indicating that auto scale min restriction should be used + */ + private boolean mUseAutoScaleRestrictionMin = false; + + /** + * flag indicating that auto scale max restriction should be used + */ + private boolean mUseAutoScaleRestrictionMax = false; + + /** + * Color of the zero line + */ + protected int mZeroLineColor = Color.GRAY; + + /** + * Width of the zero line in pixels + */ + protected float mZeroLineWidth = 1f; + + /** + * axis space from the largest value to the top in percent of the total axis range + */ + protected float mSpacePercentTop = 10f; + + /** + * axis space from the smallest value to the bottom in percent of the total axis range + */ + protected float mSpacePercentBottom = 10f; + + /** + * the position of the y-labels relative to the chart + */ + private YAxisLabelPosition mPosition = YAxisLabelPosition.OUTSIDE_CHART; + + /** + * the horizontal offset of the y-label + */ + private float mXLabelOffset = 0.0f; + + /** + * enum for the position of the y-labels relative to the chart + */ + public enum YAxisLabelPosition { + OUTSIDE_CHART, INSIDE_CHART + } + + /** + * the side this axis object represents + */ + private AxisDependency mAxisDependency; + + /** + * the minimum width that the axis should take (in dp). + *

+ * default: 0.0 + */ + protected float mMinWidth = 0.f; + + /** + * the maximum width that the axis can take (in dp). + * use Inifinity for disabling the maximum + * default: Float.POSITIVE_INFINITY (no maximum specified) + */ + protected float mMaxWidth = Float.POSITIVE_INFINITY; + + /** + * Enum that specifies the axis a DataSet should be plotted against, either LEFT or RIGHT. + * + * @author Philipp Jahoda + */ + public enum AxisDependency { + LEFT, RIGHT + } + + public YAxis() { + super(); + + // default left + this.mAxisDependency = AxisDependency.LEFT; + this.mYOffset = 0f; + } + + public YAxis(AxisDependency position) { + super(); + this.mAxisDependency = position; + this.mYOffset = 0f; + } + + public AxisDependency getAxisDependency() { + return mAxisDependency; + } + + /** + * @return the minimum width that the axis should take (in dp). + */ + public float getMinWidth() { + return mMinWidth; + } + + /** + * Sets the minimum width that the axis should take (in dp). + * + * @param minWidth + */ + public void setMinWidth(float minWidth) { + mMinWidth = minWidth; + } + + /** + * @return the maximum width that the axis can take (in dp). + */ + public float getMaxWidth() { + return mMaxWidth; + } + + /** + * Sets the maximum width that the axis can take (in dp). + * + * @param maxWidth + */ + public void setMaxWidth(float maxWidth) { + mMaxWidth = maxWidth; + } + + /** + * returns the position of the y-labels + */ + public YAxisLabelPosition getLabelPosition() { + return mPosition; + } + + /** + * sets the position of the y-labels + * + * @param pos + */ + public void setPosition(YAxisLabelPosition pos) { + mPosition = pos; + } + + /** + * returns the horizontal offset of the y-label + */ + public float getLabelXOffset() { + return mXLabelOffset; + } + + /** + * sets the horizontal offset of the y-label + * + * @param xOffset + */ + public void setLabelXOffset(float xOffset) { + mXLabelOffset = xOffset; + } + + /** + * returns true if drawing the top y-axis label entry is enabled + * + * @return + */ + public boolean isDrawTopYLabelEntryEnabled() { + return mDrawTopYLabelEntry; + } + + /** + * returns true if drawing the bottom y-axis label entry is enabled + * + * @return + */ + public boolean isDrawBottomYLabelEntryEnabled() { + return mDrawBottomYLabelEntry; + } + + /** + * set this to true to enable drawing the top y-label entry. Disabling this can be helpful + * when the top y-label and + * left x-label interfere with each other. default: true + * + * @param enabled + */ + public void setDrawTopYLabelEntry(boolean enabled) { + mDrawTopYLabelEntry = enabled; + } + + /** + * If this is set to true, the y-axis is inverted which means that low values are on top of + * the chart, high values + * on bottom. + * + * @param enabled + */ + public void setInverted(boolean enabled) { + mInverted = enabled; + } + + /** + * If this returns true, the y-axis is inverted. + * + * @return + */ + public boolean isInverted() { + return mInverted; + } + + /** + * This method is deprecated. + * Use setAxisMinimum(...) / setAxisMaximum(...) instead. + * + * @param startAtZero + */ + @Deprecated + public void setStartAtZero(boolean startAtZero) { + if (startAtZero) + setAxisMinimum(0f); + else + resetAxisMinimum(); + } + + /** + * Sets the top axis space in percent of the full range. Default 10f + * + * @param percent + */ + public void setSpaceTop(float percent) { + mSpacePercentTop = percent; + } + + /** + * Returns the top axis space in percent of the full range. Default 10f + * + * @return + */ + public float getSpaceTop() { + return mSpacePercentTop; + } + + /** + * Sets the bottom axis space in percent of the full range. Default 10f + * + * @param percent + */ + public void setSpaceBottom(float percent) { + mSpacePercentBottom = percent; + } + + /** + * Returns the bottom axis space in percent of the full range. Default 10f + * + * @return + */ + public float getSpaceBottom() { + return mSpacePercentBottom; + } + + public boolean isDrawZeroLineEnabled() { + return mDrawZeroLine; + } + + /** + * Set this to true to draw the zero-line regardless of weather other + * grid-lines are enabled or not. Default: false + * + * @param mDrawZeroLine + */ + public void setDrawZeroLine(boolean mDrawZeroLine) { + this.mDrawZeroLine = mDrawZeroLine; + } + + public int getZeroLineColor() { + return mZeroLineColor; + } + + /** + * Sets the color of the zero line + * + * @param color + */ + public void setZeroLineColor(int color) { + mZeroLineColor = color; + } + + public float getZeroLineWidth() { + return mZeroLineWidth; + } + + /** + * Sets the width of the zero line in dp + * + * @param width + */ + public void setZeroLineWidth(float width) { + this.mZeroLineWidth = Utils.convertDpToPixel(width); + } + + /** + * This is for normal (not horizontal) charts horizontal spacing. + * + * @param p + * @return + */ + public float getRequiredWidthSpace(Paint p) { + + p.setTextSize(mTextSize); + + String label = getLongestLabel(); + float width = (float) Utils.calcTextWidth(p, label) + getXOffset() * 2f; + + float minWidth = getMinWidth(); + float maxWidth = getMaxWidth(); + + if (minWidth > 0.f) + minWidth = Utils.convertDpToPixel(minWidth); + + if (maxWidth > 0.f && maxWidth != Float.POSITIVE_INFINITY) + maxWidth = Utils.convertDpToPixel(maxWidth); + + width = Math.max(minWidth, Math.min(width, maxWidth > 0.0 ? maxWidth : width)); + + return width; + } + + /** + * This is for HorizontalBarChart vertical spacing. + * + * @param p + * @return + */ + public float getRequiredHeightSpace(Paint p) { + + p.setTextSize(mTextSize); + + String label = getLongestLabel(); + return (float) Utils.calcTextHeight(p, label) + getYOffset() * 2f; + } + + /** + * Returns true if this axis needs horizontal offset, false if no offset is needed. + * + * @return + */ + public boolean needsOffset() { + if (isEnabled() && isDrawLabelsEnabled() && getLabelPosition() == YAxisLabelPosition + .OUTSIDE_CHART) + return true; + else + return false; + } + + /** + * Returns true if autoscale restriction for axis min value is enabled + */ + @Deprecated + public boolean isUseAutoScaleMinRestriction( ) { + return mUseAutoScaleRestrictionMin; + } + + /** + * Sets autoscale restriction for axis min value as enabled/disabled + */ + @Deprecated + public void setUseAutoScaleMinRestriction( boolean isEnabled ) { + mUseAutoScaleRestrictionMin = isEnabled; + } + + /** + * Returns true if autoscale restriction for axis max value is enabled + */ + @Deprecated + public boolean isUseAutoScaleMaxRestriction() { + return mUseAutoScaleRestrictionMax; + } + + /** + * Sets autoscale restriction for axis max value as enabled/disabled + */ + @Deprecated + public void setUseAutoScaleMaxRestriction( boolean isEnabled ) { + mUseAutoScaleRestrictionMax = isEnabled; + } + + + @Override + public void calculate(float dataMin, float dataMax) { + + float min = dataMin; + float max = dataMax; + + // Make sure max is greater than min + // Discussion: https://github.com/danielgindi/Charts/pull/3650#discussion_r221409991 + if (min > max) + { + if (mCustomAxisMax && mCustomAxisMin) + { + float t = min; + min = max; + max = t; + } + else if (mCustomAxisMax) + { + min = max < 0f ? max * 1.5f : max * 0.5f; + } + else if (mCustomAxisMin) + { + max = min < 0f ? min * 0.5f : min * 1.5f; + } + } + + float range = Math.abs(max - min); + + // in case all values are equal + if (range == 0f) { + max = max + 1f; + min = min - 1f; + } + + // recalculate + range = Math.abs(max - min); + + // calc extra spacing + this.mAxisMinimum = mCustomAxisMin ? this.mAxisMinimum : min - (range / 100f) * getSpaceBottom(); + this.mAxisMaximum = mCustomAxisMax ? this.mAxisMaximum : max + (range / 100f) * getSpaceTop(); + + this.mAxisRange = Math.abs(this.mAxisMinimum - this.mAxisMaximum); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarData.java new file mode 100644 index 0000000..16d60f6 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarData.java @@ -0,0 +1,119 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; + +import java.util.List; + +/** + * Data object that represents all data for the BarChart. + * + * @author Philipp Jahoda + */ +public class BarData extends BarLineScatterCandleBubbleData { + + /** + * the width of the bars on the x-axis, in values (not pixels) + */ + private float mBarWidth = 0.85f; + + public BarData() { + super(); + } + + public BarData(IBarDataSet... dataSets) { + super(dataSets); + } + + public BarData(List dataSets) { + super(dataSets); + } + + /** + * Sets the width each bar should have on the x-axis (in values, not pixels). + * Default 0.85f + * + * @param mBarWidth + */ + public void setBarWidth(float mBarWidth) { + this.mBarWidth = mBarWidth; + } + + public float getBarWidth() { + return mBarWidth; + } + + /** + * Groups all BarDataSet objects this data object holds together by modifying the x-value of their entries. + * Previously set x-values of entries will be overwritten. Leaves space between bars and groups as specified + * by the parameters. + * Do not forget to call notifyDataSetChanged() on your BarChart object after calling this method. + * + * @param fromX the starting point on the x-axis where the grouping should begin + * @param groupSpace the space between groups of bars in values (not pixels) e.g. 0.8f for bar width 1f + * @param barSpace the space between individual bars in values (not pixels) e.g. 0.1f for bar width 1f + */ + public void groupBars(float fromX, float groupSpace, float barSpace) { + + int setCount = mDataSets.size(); + if (setCount <= 1) { + throw new RuntimeException("BarData needs to hold at least 2 BarDataSets to allow grouping."); + } + + IBarDataSet max = getMaxEntryCountSet(); + int maxEntryCount = max.getEntryCount(); + + float groupSpaceWidthHalf = groupSpace / 2f; + float barSpaceHalf = barSpace / 2f; + float barWidthHalf = mBarWidth / 2f; + + float interval = getGroupWidth(groupSpace, barSpace); + + for (int i = 0; i < maxEntryCount; i++) { + + float start = fromX; + fromX += groupSpaceWidthHalf; + + for (IBarDataSet set : mDataSets) { + + fromX += barSpaceHalf; + fromX += barWidthHalf; + + if (i < set.getEntryCount()) { + + BarEntry entry = set.getEntryForIndex(i); + + if (entry != null) { + entry.setX(fromX); + } + } + + fromX += barWidthHalf; + fromX += barSpaceHalf; + } + + fromX += groupSpaceWidthHalf; + float end = fromX; + float innerInterval = end - start; + float diff = interval - innerInterval; + + // correct rounding errors + if (diff > 0 || diff < 0) { + fromX += diff; + } + } + + notifyDataChanged(); + } + + /** + * In case of grouped bars, this method returns the space an individual group of bar needs on the x-axis. + * + * @param groupSpace + * @param barSpace + * @return + */ + public float getGroupWidth(float groupSpace, float barSpace) { + return mDataSets.size() * (mBarWidth + barSpace) + groupSpace; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarDataSet.java new file mode 100644 index 0000000..e656388 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarDataSet.java @@ -0,0 +1,299 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Color; + +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.Fill; + +import java.util.ArrayList; +import java.util.List; + +public class BarDataSet extends BarLineScatterCandleBubbleDataSet implements IBarDataSet { + + /** + * the maximum number of bars that are stacked upon each other, this value + * is calculated from the Entries that are added to the DataSet + */ + private int mStackSize = 1; + + /** + * the color used for drawing the bar shadows + */ + private int mBarShadowColor = Color.rgb(215, 215, 215); + + private float mBarBorderWidth = 0.0f; + + private int mBarBorderColor = Color.BLACK; + + /** + * the alpha value used to draw the highlight indicator bar + */ + private int mHighLightAlpha = 120; + + /** + * the overall entry count, including counting each stack-value individually + */ + private int mEntryCountStacks = 0; + + /** + * array of labels used to describe the different values of the stacked bars + */ + private String[] mStackLabels = new String[]{}; + + protected List mFills = null; + + public BarDataSet(List yVals, String label) { + super(yVals, label); + + mHighLightColor = Color.rgb(0, 0, 0); + + calcStackSize(yVals); + calcEntryCountIncludingStacks(yVals); + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + BarDataSet copied = new BarDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(BarDataSet barDataSet) { + super.copy(barDataSet); + barDataSet.mStackSize = mStackSize; + barDataSet.mBarShadowColor = mBarShadowColor; + barDataSet.mBarBorderWidth = mBarBorderWidth; + barDataSet.mStackLabels = mStackLabels; + barDataSet.mHighLightAlpha = mHighLightAlpha; + } + + @Override + public List getFills() { + return mFills; + } + + @Override + public Fill getFill(int index) { + return mFills.get(index % mFills.size()); + } + + /** + * This method is deprecated. + * Use getFills() instead. + */ + @Deprecated + public List getGradients() { + return mFills; + } + + /** + * This method is deprecated. + * Use getFill(...) instead. + * + * @param index + */ + @Deprecated + public Fill getGradient(int index) { + return getFill(index); + } + + /** + * Sets the start and end color for gradient color, ONLY color that should be used for this DataSet. + * + * @param startColor + * @param endColor + */ + public void setGradientColor(int startColor, int endColor) { + mFills.clear(); + mFills.add(new Fill(startColor, endColor)); + } + + /** + * This method is deprecated. + * Use setFills(...) instead. + * + * @param gradientColors + */ + @Deprecated + public void setGradientColors(List gradientColors) { + this.mFills = gradientColors; + } + + /** + * Sets the fills for the bars in this dataset. + * + * @param fills + */ + public void setFills(List fills) { + this.mFills = fills; + } + + /** + * Calculates the total number of entries this DataSet represents, including + * stacks. All values belonging to a stack are calculated separately. + */ + private void calcEntryCountIncludingStacks(List yVals) { + + mEntryCountStacks = 0; + + for (int i = 0; i < yVals.size(); i++) { + + float[] vals = yVals.get(i).getYVals(); + + if (vals == null) + mEntryCountStacks++; + else + mEntryCountStacks += vals.length; + } + } + + /** + * calculates the maximum stacksize that occurs in the Entries array of this + * DataSet + */ + private void calcStackSize(List yVals) { + + for (int i = 0; i < yVals.size(); i++) { + + float[] vals = yVals.get(i).getYVals(); + + if (vals != null && vals.length > mStackSize) + mStackSize = vals.length; + } + } + + @Override + protected void calcMinMax(BarEntry e) { + + if (e != null && !Float.isNaN(e.getY())) { + + if (e.getYVals() == null) { + + if (e.getY() < mYMin) + mYMin = e.getY(); + + if (e.getY() > mYMax) + mYMax = e.getY(); + } else { + + if (-e.getNegativeSum() < mYMin) + mYMin = -e.getNegativeSum(); + + if (e.getPositiveSum() > mYMax) + mYMax = e.getPositiveSum(); + } + + calcMinMaxX(e); + } + } + + @Override + public int getStackSize() { + return mStackSize; + } + + @Override + public boolean isStacked() { + return mStackSize > 1 ? true : false; + } + + /** + * returns the overall entry count, including counting each stack-value + * individually + * + * @return + */ + public int getEntryCountStacks() { + return mEntryCountStacks; + } + + /** + * Sets the color used for drawing the bar-shadows. The bar shadows is a + * surface behind the bar that indicates the maximum value. Don't for get to + * use getResources().getColor(...) to set this. Or Color.rgb(...). + * + * @param color + */ + public void setBarShadowColor(int color) { + mBarShadowColor = color; + } + + @Override + public int getBarShadowColor() { + return mBarShadowColor; + } + + /** + * Sets the width used for drawing borders around the bars. + * If borderWidth == 0, no border will be drawn. + * + * @return + */ + public void setBarBorderWidth(float width) { + mBarBorderWidth = width; + } + + /** + * Returns the width used for drawing borders around the bars. + * If borderWidth == 0, no border will be drawn. + * + * @return + */ + @Override + public float getBarBorderWidth() { + return mBarBorderWidth; + } + + /** + * Sets the color drawing borders around the bars. + * + * @return + */ + public void setBarBorderColor(int color) { + mBarBorderColor = color; + } + + /** + * Returns the color drawing borders around the bars. + * + * @return + */ + @Override + public int getBarBorderColor() { + return mBarBorderColor; + } + + /** + * Set the alpha value (transparency) that is used for drawing the highlight + * indicator bar. min = 0 (fully transparent), max = 255 (fully opaque) + * + * @param alpha + */ + public void setHighLightAlpha(int alpha) { + mHighLightAlpha = alpha; + } + + @Override + public int getHighLightAlpha() { + return mHighLightAlpha; + } + + /** + * Sets labels for different values of bar-stacks, in case there are one. + * + * @param labels + */ + public void setStackLabels(String[] labels) { + mStackLabels = labels; + } + + @Override + public String[] getStackLabels() { + return mStackLabels; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarEntry.java new file mode 100644 index 0000000..365ef51 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarEntry.java @@ -0,0 +1,310 @@ +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.highlight.Range; + +/** + * Entry class for the BarChart. (especially stacked bars) + * + * @author Philipp Jahoda + */ +@SuppressLint("ParcelCreator") +public class BarEntry extends Entry { + + /** + * the values the stacked barchart holds + */ + private float[] mYVals; + + /** + * the ranges for the individual stack values - automatically calculated + */ + private Range[] mRanges; + + /** + * the sum of all negative values this entry (if stacked) contains + */ + private float mNegativeSum; + + /** + * the sum of all positive values this entry (if stacked) contains + */ + private float mPositiveSum; + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + */ + public BarEntry(float x, float y) { + super(x, y); + } + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param data - Spot for additional data this Entry represents. + */ + public BarEntry(float x, float y, Object data) { + super(x, y, data); + } + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param icon - icon image + */ + public BarEntry(float x, float y, Drawable icon) { + super(x, y, icon); + } + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param icon - icon image + * @param data - Spot for additional data this Entry represents. + */ + public BarEntry(float x, float y, Drawable icon, Object data) { + super(x, y, icon, data); + } + + /** + * Constructor for stacked bar entries. One data object for whole stack + * + * @param x + * @param vals - the stack values, use at least 2 + */ + public BarEntry(float x, float[] vals) { + super(x, calcSum(vals)); + + this.mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Constructor for stacked bar entries. One data object for whole stack + * + * @param x + * @param vals - the stack values, use at least 2 + * @param data - Spot for additional data this Entry represents. + */ + public BarEntry(float x, float[] vals, Object data) { + super(x, calcSum(vals), data); + + this.mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Constructor for stacked bar entries. One data object for whole stack + * + * @param x + * @param vals - the stack values, use at least 2 + * @param icon - icon image + */ + public BarEntry(float x, float[] vals, Drawable icon) { + super(x, calcSum(vals), icon); + + this.mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Constructor for stacked bar entries. One data object for whole stack + * + * @param x + * @param vals - the stack values, use at least 2 + * @param icon - icon image + * @param data - Spot for additional data this Entry represents. + */ + public BarEntry(float x, float[] vals, Drawable icon, Object data) { + super(x, calcSum(vals), icon, data); + + this.mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Returns an exact copy of the BarEntry. + */ + public BarEntry copy() { + + BarEntry copied = new BarEntry(getX(), getY(), getData()); + copied.setVals(mYVals); + return copied; + } + + /** + * Returns the stacked values this BarEntry represents, or null, if only a single value is represented (then, use + * getY()). + * + * @return + */ + public float[] getYVals() { + return mYVals; + } + + /** + * Set the array of values this BarEntry should represent. + * + * @param vals + */ + public void setVals(float[] vals) { + setY(calcSum(vals)); + mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Returns the value of this BarEntry. If the entry is stacked, it returns the positive sum of all values. + * + * @return + */ + @Override + public float getY() { + return super.getY(); + } + + /** + * Returns the ranges of the individual stack-entries. Will return null if this entry is not stacked. + * + * @return + */ + public Range[] getRanges() { + return mRanges; + } + + /** + * Returns true if this BarEntry is stacked (has a values array), false if not. + * + * @return + */ + public boolean isStacked() { + return mYVals != null; + } + + /** + * Use `getSumBelow(stackIndex)` instead. + */ + @Deprecated + public float getBelowSum(int stackIndex) { + return getSumBelow(stackIndex); + } + + public float getSumBelow(int stackIndex) { + + if (mYVals == null) + return 0; + + float remainder = 0f; + int index = mYVals.length - 1; + + while (index > stackIndex && index >= 0) { + remainder += mYVals[index]; + index--; + } + + return remainder; + } + + /** + * Reuturns the sum of all positive values this entry (if stacked) contains. + * + * @return + */ + public float getPositiveSum() { + return mPositiveSum; + } + + /** + * Returns the sum of all negative values this entry (if stacked) contains. (this is a positive number) + * + * @return + */ + public float getNegativeSum() { + return mNegativeSum; + } + + private void calcPosNegSum() { + + if (mYVals == null) { + mNegativeSum = 0; + mPositiveSum = 0; + return; + } + + float sumNeg = 0f; + float sumPos = 0f; + + for (float f : mYVals) { + if (f <= 0f) + sumNeg += Math.abs(f); + else + sumPos += f; + } + + mNegativeSum = sumNeg; + mPositiveSum = sumPos; + } + + /** + * Calculates the sum across all values of the given stack. + * + * @param vals + * @return + */ + private static float calcSum(float[] vals) { + + if (vals == null) + return 0f; + + float sum = 0f; + + for (float f : vals) + sum += f; + + return sum; + } + + protected void calcRanges() { + + float[] values = getYVals(); + + if (values == null || values.length == 0) + return; + + mRanges = new Range[values.length]; + + float negRemain = -getNegativeSum(); + float posRemain = 0f; + + for (int i = 0; i < mRanges.length; i++) { + + float value = values[i]; + + if (value < 0) { + mRanges[i] = new Range(negRemain, negRemain - value); + negRemain -= value; + } else { + mRanges[i] = new Range(posRemain, posRemain + value); + posRemain += value; + } + } + } +} + + diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleData.java new file mode 100644 index 0000000..c09eade --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleData.java @@ -0,0 +1,27 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; + +import java.util.List; + +/** + * Baseclass for all Line, Bar, Scatter, Candle and Bubble data. + * + * @author Philipp Jahoda + */ +public abstract class BarLineScatterCandleBubbleData> + extends ChartData { + + public BarLineScatterCandleBubbleData() { + super(); + } + + public BarLineScatterCandleBubbleData(T... sets) { + super(sets); + } + + public BarLineScatterCandleBubbleData(List sets) { + super(sets); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleDataSet.java new file mode 100644 index 0000000..eab6dcc --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleDataSet.java @@ -0,0 +1,48 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Color; + +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; + +import java.util.List; + +/** + * Baseclass of all DataSets for Bar-, Line-, Scatter- and CandleStickChart. + * + * @author Philipp Jahoda + */ +public abstract class BarLineScatterCandleBubbleDataSet + extends DataSet + implements IBarLineScatterCandleBubbleDataSet { + + /** + * default highlight color + */ + protected int mHighLightColor = Color.rgb(255, 187, 115); + + public BarLineScatterCandleBubbleDataSet(List yVals, String label) { + super(yVals, label); + } + + /** + * Sets the color that is used for drawing the highlight indicators. Dont + * forget to resolve the color using getResources().getColor(...) or + * Color.rgb(...). + * + * @param color + */ + public void setHighLightColor(int color) { + mHighLightColor = color; + } + + @Override + public int getHighLightColor() { + return mHighLightColor; + } + + protected void copy(BarLineScatterCandleBubbleDataSet barLineScatterCandleBubbleDataSet) { + super.copy(barLineScatterCandleBubbleDataSet); + barLineScatterCandleBubbleDataSet.mHighLightColor = mHighLightColor; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseDataSet.java new file mode 100644 index 0000000..7e7445c --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseDataSet.java @@ -0,0 +1,506 @@ +package com.github.mikephil.charting.data; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Typeface; + +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Philipp Jahoda on 21/10/15. + * This is the base dataset of all DataSets. It's purpose is to implement critical methods + * provided by the IDataSet interface. + */ +public abstract class BaseDataSet implements IDataSet { + + /** + * List representing all colors that are used for this DataSet + */ + protected List mColors = null; + + /** + * List representing all colors that are used for drawing the actual values for this DataSet + */ + protected List mValueColors = null; + + /** + * label that describes the DataSet or the data the DataSet represents + */ + private String mLabel = "DataSet"; + + /** + * this specifies which axis this DataSet should be plotted against + */ + protected YAxis.AxisDependency mAxisDependency = YAxis.AxisDependency.LEFT; + + /** + * if true, value highlightning is enabled + */ + protected boolean mHighlightEnabled = true; + + /** + * custom formatter that is used instead of the auto-formatter if set + */ + protected transient IValueFormatter mValueFormatter; + + /** + * the typeface used for the value text + */ + protected Typeface mValueTypeface; + + private Legend.LegendForm mForm = Legend.LegendForm.DEFAULT; + private float mFormSize = Float.NaN; + private float mFormLineWidth = Float.NaN; + private DashPathEffect mFormLineDashEffect = null; + + /** + * if true, y-values are drawn on the chart + */ + protected boolean mDrawValues = true; + + /** + * if true, y-icons are drawn on the chart + */ + protected boolean mDrawIcons = true; + + /** + * the offset for drawing icons (in dp) + */ + protected MPPointF mIconsOffset = new MPPointF(); + + /** + * the size of the value-text labels + */ + protected float mValueTextSize = 17f; + + /** + * flag that indicates if the DataSet is visible or not + */ + protected boolean mVisible = true; + + /** + * Default constructor. + */ + public BaseDataSet() { + mColors = new ArrayList(); + mValueColors = new ArrayList(); + + // default color + mColors.add(Color.rgb(140, 234, 255)); + mValueColors.add(Color.BLACK); + } + + /** + * Constructor with label. + * + * @param label + */ + public BaseDataSet(String label) { + this(); + this.mLabel = label; + } + + /** + * Use this method to tell the data set that the underlying data has changed. + */ + public void notifyDataSetChanged() { + calcMinMax(); + } + + + /** + * ###### ###### COLOR GETTING RELATED METHODS ##### ###### + */ + + @Override + public List getColors() { + return mColors; + } + + public List getValueColors() { + return mValueColors; + } + + @Override + public int getColor() { + return mColors.get(0); + } + + @Override + public int getColor(int index) { + return mColors.get(index % mColors.size()); + } + + /** + * ###### ###### COLOR SETTING RELATED METHODS ##### ###### + */ + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. If you are using colors from the resources, + * make sure that the colors are already prepared (by calling + * getResources().getColor(...)) before adding them to the DataSet. + * + * @param colors + */ + public void setColors(List colors) { + this.mColors = colors; + } + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. If you are using colors from the resources, + * make sure that the colors are already prepared (by calling + * getResources().getColor(...)) before adding them to the DataSet. + * + * @param colors + */ + public void setColors(int... colors) { + this.mColors = ColorTemplate.createColors(colors); + } + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. You can use + * "new int[] { R.color.red, R.color.green, ... }" to provide colors for + * this method. Internally, the colors are resolved using + * getResources().getColor(...) + * + * @param colors + */ + public void setColors(int[] colors, Context c) { + + if (mColors == null) { + mColors = new ArrayList<>(); + } + + mColors.clear(); + + for (int color : colors) { + mColors.add(c.getResources().getColor(color)); + } + } + + /** + * Adds a new color to the colors array of the DataSet. + * + * @param color + */ + public void addColor(int color) { + if (mColors == null) + mColors = new ArrayList(); + mColors.add(color); + } + + /** + * Sets the one and ONLY color that should be used for this DataSet. + * Internally, this recreates the colors array and adds the specified color. + * + * @param color + */ + public void setColor(int color) { + resetColors(); + mColors.add(color); + } + + /** + * Sets a color with a specific alpha value. + * + * @param color + * @param alpha from 0-255 + */ + public void setColor(int color, int alpha) { + setColor(Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))); + } + + /** + * Sets colors with a specific alpha value. + * + * @param colors + * @param alpha + */ + public void setColors(int[] colors, int alpha) { + resetColors(); + for (int color : colors) { + addColor(Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))); + } + } + + /** + * Resets all colors of this DataSet and recreates the colors array. + */ + public void resetColors() { + if (mColors == null) { + mColors = new ArrayList(); + } + mColors.clear(); + } + + /** + * ###### ###### OTHER STYLING RELATED METHODS ##### ###### + */ + + @Override + public void setLabel(String label) { + mLabel = label; + } + + @Override + public String getLabel() { + return mLabel; + } + + @Override + public void setHighlightEnabled(boolean enabled) { + mHighlightEnabled = enabled; + } + + @Override + public boolean isHighlightEnabled() { + return mHighlightEnabled; + } + + @Override + public void setValueFormatter(IValueFormatter f) { + + if (f == null) + return; + else + mValueFormatter = f; + } + + @Override + public IValueFormatter getValueFormatter() { + if (needsFormatter()) + return Utils.getDefaultValueFormatter(); + return mValueFormatter; + } + + @Override + public boolean needsFormatter() { + return mValueFormatter == null; + } + + @Override + public void setValueTextColor(int color) { + mValueColors.clear(); + mValueColors.add(color); + } + + @Override + public void setValueTextColors(List colors) { + mValueColors = colors; + } + + @Override + public void setValueTypeface(Typeface tf) { + mValueTypeface = tf; + } + + @Override + public void setValueTextSize(float size) { + mValueTextSize = Utils.convertDpToPixel(size); + } + + @Override + public int getValueTextColor() { + return mValueColors.get(0); + } + + @Override + public int getValueTextColor(int index) { + return mValueColors.get(index % mValueColors.size()); + } + + @Override + public Typeface getValueTypeface() { + return mValueTypeface; + } + + @Override + public float getValueTextSize() { + return mValueTextSize; + } + + public void setForm(Legend.LegendForm form) { + mForm = form; + } + + @Override + public Legend.LegendForm getForm() { + return mForm; + } + + public void setFormSize(float formSize) { + mFormSize = formSize; + } + + @Override + public float getFormSize() { + return mFormSize; + } + + public void setFormLineWidth(float formLineWidth) { + mFormLineWidth = formLineWidth; + } + + @Override + public float getFormLineWidth() { + return mFormLineWidth; + } + + public void setFormLineDashEffect(DashPathEffect dashPathEffect) { + mFormLineDashEffect = dashPathEffect; + } + + @Override + public DashPathEffect getFormLineDashEffect() { + return mFormLineDashEffect; + } + + @Override + public void setDrawValues(boolean enabled) { + this.mDrawValues = enabled; + } + + @Override + public boolean isDrawValuesEnabled() { + return mDrawValues; + } + + @Override + public void setDrawIcons(boolean enabled) { + mDrawIcons = enabled; + } + + @Override + public boolean isDrawIconsEnabled() { + return mDrawIcons; + } + + @Override + public void setIconsOffset(MPPointF offsetDp) { + + mIconsOffset.x = offsetDp.x; + mIconsOffset.y = offsetDp.y; + } + + @Override + public MPPointF getIconsOffset() { + return mIconsOffset; + } + + @Override + public void setVisible(boolean visible) { + mVisible = visible; + } + + @Override + public boolean isVisible() { + return mVisible; + } + + @Override + public YAxis.AxisDependency getAxisDependency() { + return mAxisDependency; + } + + @Override + public void setAxisDependency(YAxis.AxisDependency dependency) { + mAxisDependency = dependency; + } + + + /** + * ###### ###### DATA RELATED METHODS ###### ###### + */ + + @Override + public int getIndexInEntries(int xIndex) { + + for (int i = 0; i < getEntryCount(); i++) { + if (xIndex == getEntryForIndex(i).getX()) + return i; + } + + return -1; + } + + @Override + public boolean removeFirst() { + + if (getEntryCount() > 0) { + + T entry = getEntryForIndex(0); + return removeEntry(entry); + } else + return false; + } + + @Override + public boolean removeLast() { + + if (getEntryCount() > 0) { + + T e = getEntryForIndex(getEntryCount() - 1); + return removeEntry(e); + } else + return false; + } + + @Override + public boolean removeEntryByXValue(float xValue) { + + T e = getEntryForXValue(xValue, Float.NaN); + return removeEntry(e); + } + + @Override + public boolean removeEntry(int index) { + + T e = getEntryForIndex(index); + return removeEntry(e); + } + + @Override + public boolean contains(T e) { + + for (int i = 0; i < getEntryCount(); i++) { + if (getEntryForIndex(i).equals(e)) + return true; + } + + return false; + } + + protected void copy(BaseDataSet baseDataSet) { + baseDataSet.mAxisDependency = mAxisDependency; + baseDataSet.mColors = mColors; + baseDataSet.mDrawIcons = mDrawIcons; + baseDataSet.mDrawValues = mDrawValues; + baseDataSet.mForm = mForm; + baseDataSet.mFormLineDashEffect = mFormLineDashEffect; + baseDataSet.mFormLineWidth = mFormLineWidth; + baseDataSet.mFormSize = mFormSize; + baseDataSet.mHighlightEnabled = mHighlightEnabled; + baseDataSet.mIconsOffset = mIconsOffset; + baseDataSet.mValueColors = mValueColors; + baseDataSet.mValueFormatter = mValueFormatter; + baseDataSet.mValueColors = mValueColors; + baseDataSet.mValueTextSize = mValueTextSize; + baseDataSet.mVisible = mVisible; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseEntry.java new file mode 100644 index 0000000..eb1ada8 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseEntry.java @@ -0,0 +1,97 @@ +package com.github.mikephil.charting.data; + +import android.graphics.drawable.Drawable; + +/** + * Created by Philipp Jahoda on 02/06/16. + */ +public abstract class BaseEntry { + + /** the y value */ + private float y = 0f; + + /** optional spot for additional data this Entry represents */ + private Object mData = null; + + /** optional icon image */ + private Drawable mIcon = null; + + public BaseEntry() { + + } + + public BaseEntry(float y) { + this.y = y; + } + + public BaseEntry(float y, Object data) { + this(y); + this.mData = data; + } + + public BaseEntry(float y, Drawable icon) { + this(y); + this.mIcon = icon; + } + + public BaseEntry(float y, Drawable icon, Object data) { + this(y); + this.mIcon = icon; + this.mData = data; + } + + /** + * Returns the y value of this Entry. + * + * @return + */ + public float getY() { + return y; + } + + /** + * Sets the icon drawable + * + * @param icon + */ + public void setIcon(Drawable icon) { + this.mIcon = icon; + } + + /** + * Returns the icon of this Entry. + * + * @return + */ + public Drawable getIcon() { + return mIcon; + } + + /** + * Sets the y-value for the Entry. + * + * @param y + */ + public void setY(float y) { + this.y = y; + } + + /** + * Returns the data, additional information that this Entry represents, or + * null, if no data has been specified. + * + * @return + */ + public Object getData() { + return mData; + } + + /** + * Sets additional data this Entry should represent. + * + * @param data + */ + public void setData(Object data) { + this.mData = data; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleData.java new file mode 100644 index 0000000..8fb72f1 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleData.java @@ -0,0 +1,34 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet; + +import java.util.List; + +public class BubbleData extends BarLineScatterCandleBubbleData { + + public BubbleData() { + super(); + } + + public BubbleData(IBubbleDataSet... dataSets) { + super(dataSets); + } + + public BubbleData(List dataSets) { + super(dataSets); + } + + + /** + * Sets the width of the circle that surrounds the bubble when highlighted + * for all DataSet objects this data object contains, in dp. + * + * @param width + */ + public void setHighlightCircleWidth(float width) { + for (IBubbleDataSet set : mDataSets) { + set.setHighlightCircleWidth(width); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleDataSet.java new file mode 100644 index 0000000..9ef87fb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleDataSet.java @@ -0,0 +1,71 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +public class BubbleDataSet extends BarLineScatterCandleBubbleDataSet implements IBubbleDataSet { + + protected float mMaxSize; + protected boolean mNormalizeSize = true; + + private float mHighlightCircleWidth = 2.5f; + + public BubbleDataSet(List yVals, String label) { + super(yVals, label); + } + + @Override + public void setHighlightCircleWidth(float width) { + mHighlightCircleWidth = Utils.convertDpToPixel(width); + } + + @Override + public float getHighlightCircleWidth() { + return mHighlightCircleWidth; + } + + @Override + protected void calcMinMax(BubbleEntry e) { + super.calcMinMax(e); + + final float size = e.getSize(); + + if (size > mMaxSize) { + mMaxSize = size; + } + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + BubbleDataSet copied = new BubbleDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(BubbleDataSet bubbleDataSet) { + bubbleDataSet.mHighlightCircleWidth = mHighlightCircleWidth; + bubbleDataSet.mNormalizeSize = mNormalizeSize; + } + + @Override + public float getMaxSize() { + return mMaxSize; + } + + @Override + public boolean isNormalizeSizeEnabled() { + return mNormalizeSize; + } + + public void setNormalizeSizeEnabled(boolean normalizeSize) { + mNormalizeSize = normalizeSize; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleEntry.java new file mode 100644 index 0000000..35d8330 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleEntry.java @@ -0,0 +1,91 @@ + +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; + +/** + * Subclass of Entry that holds a value for one entry in a BubbleChart. Bubble + * chart implementation: Copyright 2015 Pierre-Marc Airoldi Licensed under + * Apache License 2.0 + * + * @author Philipp Jahoda + */ +@SuppressLint("ParcelCreator") +public class BubbleEntry extends Entry { + + /** size value */ + private float mSize = 0f; + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + */ + public BubbleEntry(float x, float y, float size) { + super(x, y); + this.mSize = size; + } + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + * @param data Spot for additional data this Entry represents. + */ + public BubbleEntry(float x, float y, float size, Object data) { + super(x, y, data); + this.mSize = size; + } + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + * @param icon Icon image + */ + public BubbleEntry(float x, float y, float size, Drawable icon) { + super(x, y, icon); + this.mSize = size; + } + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + * @param icon Icon image + * @param data Spot for additional data this Entry represents. + */ + public BubbleEntry(float x, float y, float size, Drawable icon, Object data) { + super(x, y, icon, data); + this.mSize = size; + } + + public BubbleEntry copy() { + + BubbleEntry c = new BubbleEntry(getX(), getY(), mSize, getData()); + return c; + } + + /** + * Returns the size of this entry (the size of the bubble). + * + * @return + */ + public float getSize() { + return mSize; + } + + public void setSize(float size) { + this.mSize = size; + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleData.java new file mode 100644 index 0000000..1e90db2 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleData.java @@ -0,0 +1,21 @@ +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; + +import java.util.ArrayList; +import java.util.List; + +public class CandleData extends BarLineScatterCandleBubbleData { + + public CandleData() { + super(); + } + + public CandleData(List dataSets) { + super(dataSets); + } + + public CandleData(ICandleDataSet... dataSets) { + super(dataSets); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleDataSet.java new file mode 100644 index 0000000..dcd5b76 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleDataSet.java @@ -0,0 +1,295 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * DataSet for the CandleStickChart. + * + * @author Philipp Jahoda + */ +public class CandleDataSet extends LineScatterCandleRadarDataSet implements ICandleDataSet { + + /** + * the width of the shadow of the candle + */ + private float mShadowWidth = 3f; + + /** + * should the candle bars show? + * when false, only "ticks" will show + *

+ * - default: true + */ + private boolean mShowCandleBar = true; + + /** + * the space between the candle entries, default 0.1f (10%) + */ + private float mBarSpace = 0.1f; + + /** + * use candle color for the shadow + */ + private boolean mShadowColorSameAsCandle = false; + + /** + * paint style when open < close + * increasing candlesticks are traditionally hollow + */ + protected Paint.Style mIncreasingPaintStyle = Paint.Style.STROKE; + + /** + * paint style when open > close + * descreasing candlesticks are traditionally filled + */ + protected Paint.Style mDecreasingPaintStyle = Paint.Style.FILL; + + /** + * color for open == close + */ + protected int mNeutralColor = ColorTemplate.COLOR_SKIP; + + /** + * color for open < close + */ + protected int mIncreasingColor = ColorTemplate.COLOR_SKIP; + + /** + * color for open > close + */ + protected int mDecreasingColor = ColorTemplate.COLOR_SKIP; + + /** + * shadow line color, set -1 for backward compatibility and uses default + * color + */ + protected int mShadowColor = ColorTemplate.COLOR_SKIP; + + public CandleDataSet(List yVals, String label) { + super(yVals, label); + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + CandleDataSet copied = new CandleDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(CandleDataSet candleDataSet) { + super.copy(candleDataSet); + candleDataSet.mShadowWidth = mShadowWidth; + candleDataSet.mShowCandleBar = mShowCandleBar; + candleDataSet.mBarSpace = mBarSpace; + candleDataSet.mShadowColorSameAsCandle = mShadowColorSameAsCandle; + candleDataSet.mHighLightColor = mHighLightColor; + candleDataSet.mIncreasingPaintStyle = mIncreasingPaintStyle; + candleDataSet.mDecreasingPaintStyle = mDecreasingPaintStyle; + candleDataSet.mNeutralColor = mNeutralColor; + candleDataSet.mIncreasingColor = mIncreasingColor; + candleDataSet.mDecreasingColor = mDecreasingColor; + candleDataSet.mShadowColor = mShadowColor; + } + + @Override + protected void calcMinMax(CandleEntry e) { + + if (e.getLow() < mYMin) + mYMin = e.getLow(); + + if (e.getHigh() > mYMax) + mYMax = e.getHigh(); + + calcMinMaxX(e); + } + + @Override + protected void calcMinMaxY(CandleEntry e) { + + if (e.getHigh() < mYMin) + mYMin = e.getHigh(); + + if (e.getHigh() > mYMax) + mYMax = e.getHigh(); + + if (e.getLow() < mYMin) + mYMin = e.getLow(); + + if (e.getLow() > mYMax) + mYMax = e.getLow(); + } + + /** + * Sets the space that is left out on the left and right side of each + * candle, default 0.1f (10%), max 0.45f, min 0f + * + * @param space + */ + public void setBarSpace(float space) { + + if (space < 0f) + space = 0f; + if (space > 0.45f) + space = 0.45f; + + mBarSpace = space; + } + + @Override + public float getBarSpace() { + return mBarSpace; + } + + /** + * Sets the width of the candle-shadow-line in pixels. Default 3f. + * + * @param width + */ + public void setShadowWidth(float width) { + mShadowWidth = Utils.convertDpToPixel(width); + } + + @Override + public float getShadowWidth() { + return mShadowWidth; + } + + /** + * Sets whether the candle bars should show? + * + * @param showCandleBar + */ + public void setShowCandleBar(boolean showCandleBar) { + mShowCandleBar = showCandleBar; + } + + @Override + public boolean getShowCandleBar() { + return mShowCandleBar; + } + + // TODO + /** + * It is necessary to implement ColorsList class that will encapsulate + * colors list functionality, because It's wrong to copy paste setColor, + * addColor, ... resetColors for each time when we want to add a coloring + * options for one of objects + * + * @author Mesrop + */ + + /** BELOW THIS COLOR HANDLING */ + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open == close. + * + * @param color + */ + public void setNeutralColor(int color) { + mNeutralColor = color; + } + + @Override + public int getNeutralColor() { + return mNeutralColor; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open <= close. + * + * @param color + */ + public void setIncreasingColor(int color) { + mIncreasingColor = color; + } + + @Override + public int getIncreasingColor() { + return mIncreasingColor; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open > close. + * + * @param color + */ + public void setDecreasingColor(int color) { + mDecreasingColor = color; + } + + @Override + public int getDecreasingColor() { + return mDecreasingColor; + } + + @Override + public Paint.Style getIncreasingPaintStyle() { + return mIncreasingPaintStyle; + } + + /** + * Sets paint style when open < close + * + * @param paintStyle + */ + public void setIncreasingPaintStyle(Paint.Style paintStyle) { + this.mIncreasingPaintStyle = paintStyle; + } + + @Override + public Paint.Style getDecreasingPaintStyle() { + return mDecreasingPaintStyle; + } + + /** + * Sets paint style when open > close + * + * @param decreasingPaintStyle + */ + public void setDecreasingPaintStyle(Paint.Style decreasingPaintStyle) { + this.mDecreasingPaintStyle = decreasingPaintStyle; + } + + @Override + public int getShadowColor() { + return mShadowColor; + } + + /** + * Sets shadow color for all entries + * + * @param shadowColor + */ + public void setShadowColor(int shadowColor) { + this.mShadowColor = shadowColor; + } + + @Override + public boolean getShadowColorSameAsCandle() { + return mShadowColorSameAsCandle; + } + + /** + * Sets shadow color to be the same color as the candle color + * + * @param shadowColorSameAsCandle + */ + public void setShadowColorSameAsCandle(boolean shadowColorSameAsCandle) { + this.mShadowColorSameAsCandle = shadowColorSameAsCandle; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleEntry.java new file mode 100644 index 0000000..5a04995 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleEntry.java @@ -0,0 +1,193 @@ + +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; + +/** + * Subclass of Entry that holds all values for one entry in a CandleStickChart. + * + * @author Philipp Jahoda + */ +@SuppressLint("ParcelCreator") +public class CandleEntry extends Entry { + + /** shadow-high value */ + private float mShadowHigh = 0f; + + /** shadow-low value */ + private float mShadowLow = 0f; + + /** close value */ + private float mClose = 0f; + + /** open value */ + private float mOpen = 0f; + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open The open value + * @param close The close value + */ + public CandleEntry(float x, float shadowH, float shadowL, float open, float close) { + super(x, (shadowH + shadowL) / 2f); + + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open + * @param close + * @param data Spot for additional data this Entry represents + */ + public CandleEntry(float x, float shadowH, float shadowL, float open, float close, + Object data) { + super(x, (shadowH + shadowL) / 2f, data); + + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open + * @param close + * @param icon Icon image + */ + public CandleEntry(float x, float shadowH, float shadowL, float open, float close, + Drawable icon) { + super(x, (shadowH + shadowL) / 2f, icon); + + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open + * @param close + * @param icon Icon image + * @param data Spot for additional data this Entry represents + */ + public CandleEntry(float x, float shadowH, float shadowL, float open, float close, + Drawable icon, Object data) { + super(x, (shadowH + shadowL) / 2f, icon, data); + + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + + /** + * Returns the overall range (difference) between shadow-high and + * shadow-low. + * + * @return + */ + public float getShadowRange() { + return Math.abs(mShadowHigh - mShadowLow); + } + + /** + * Returns the body size (difference between open and close). + * + * @return + */ + public float getBodyRange() { + return Math.abs(mOpen - mClose); + } + + /** + * Returns the center value of the candle. (Middle value between high and + * low) + */ + @Override + public float getY() { + return super.getY(); + } + + public CandleEntry copy() { + + CandleEntry c = new CandleEntry(getX(), mShadowHigh, mShadowLow, mOpen, + mClose, getData()); + + return c; + } + + /** + * Returns the upper shadows highest value. + * + * @return + */ + public float getHigh() { + return mShadowHigh; + } + + public void setHigh(float mShadowHigh) { + this.mShadowHigh = mShadowHigh; + } + + /** + * Returns the lower shadows lowest value. + * + * @return + */ + public float getLow() { + return mShadowLow; + } + + public void setLow(float mShadowLow) { + this.mShadowLow = mShadowLow; + } + + /** + * Returns the bodys close value. + * + * @return + */ + public float getClose() { + return mClose; + } + + public void setClose(float mClose) { + this.mClose = mClose; + } + + /** + * Returns the bodys open value. + * + * @return + */ + public float getOpen() { + return mOpen; + } + + public void setOpen(float mOpen) { + this.mOpen = mOpen; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/ChartData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ChartData.java new file mode 100644 index 0000000..bfc5ed7 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ChartData.java @@ -0,0 +1,821 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Typeface; +import android.util.Log; + +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class that holds all relevant data that represents the chart. That involves + * at least one (or more) DataSets, and an array of x-values. + * + * @author Philipp Jahoda + */ +public abstract class ChartData> { + + /** + * maximum y-value in the value array across all axes + */ + protected float mYMax = -Float.MAX_VALUE; + + /** + * the minimum y-value in the value array across all axes + */ + protected float mYMin = Float.MAX_VALUE; + + /** + * maximum x-value in the value array + */ + protected float mXMax = -Float.MAX_VALUE; + + /** + * minimum x-value in the value array + */ + protected float mXMin = Float.MAX_VALUE; + + + protected float mLeftAxisMax = -Float.MAX_VALUE; + + protected float mLeftAxisMin = Float.MAX_VALUE; + + protected float mRightAxisMax = -Float.MAX_VALUE; + + protected float mRightAxisMin = Float.MAX_VALUE; + + /** + * array that holds all DataSets the ChartData object represents + */ + protected List mDataSets; + + /** + * Default constructor. + */ + public ChartData() { + mDataSets = new ArrayList(); + } + + /** + * Constructor taking single or multiple DataSet objects. + * + * @param dataSets + */ + public ChartData(T... dataSets) { + mDataSets = arrayToList(dataSets); + notifyDataChanged(); + } + + /** + * Created because Arrays.asList(...) does not support modification. + * + * @param array + * @return + */ + private List arrayToList(T[] array) { + + List list = new ArrayList<>(); + + for (T set : array) { + list.add(set); + } + + return list; + } + + /** + * constructor for chart data + * + * @param sets the dataset array + */ + public ChartData(List sets) { + this.mDataSets = sets; + notifyDataChanged(); + } + + /** + * Call this method to let the ChartData know that the underlying data has + * changed. Calling this performs all necessary recalculations needed when + * the contained data has changed. + */ + public void notifyDataChanged() { + calcMinMax(); + } + + /** + * Calc minimum and maximum y-values over all DataSets. + * Tell DataSets to recalculate their min and max y-values, this is only needed for autoScaleMinMax. + * + * @param fromX the x-value to start the calculation from + * @param toX the x-value to which the calculation should be performed + */ + public void calcMinMaxY(float fromX, float toX) { + + for (T set : mDataSets) { + set.calcMinMaxY(fromX, toX); + } + + // apply the new data + calcMinMax(); + } + + /** + * Calc minimum and maximum values (both x and y) over all DataSets. + */ + protected void calcMinMax() { + + if (mDataSets == null) + return; + + mYMax = -Float.MAX_VALUE; + mYMin = Float.MAX_VALUE; + mXMax = -Float.MAX_VALUE; + mXMin = Float.MAX_VALUE; + + for (T set : mDataSets) { + calcMinMax(set); + } + + mLeftAxisMax = -Float.MAX_VALUE; + mLeftAxisMin = Float.MAX_VALUE; + mRightAxisMax = -Float.MAX_VALUE; + mRightAxisMin = Float.MAX_VALUE; + + // left axis + T firstLeft = getFirstLeft(mDataSets); + + if (firstLeft != null) { + + mLeftAxisMax = firstLeft.getYMax(); + mLeftAxisMin = firstLeft.getYMin(); + + for (T dataSet : mDataSets) { + if (dataSet.getAxisDependency() == AxisDependency.LEFT) { + if (dataSet.getYMin() < mLeftAxisMin) + mLeftAxisMin = dataSet.getYMin(); + + if (dataSet.getYMax() > mLeftAxisMax) + mLeftAxisMax = dataSet.getYMax(); + } + } + } + + // right axis + T firstRight = getFirstRight(mDataSets); + + if (firstRight != null) { + + mRightAxisMax = firstRight.getYMax(); + mRightAxisMin = firstRight.getYMin(); + + for (T dataSet : mDataSets) { + if (dataSet.getAxisDependency() == AxisDependency.RIGHT) { + if (dataSet.getYMin() < mRightAxisMin) + mRightAxisMin = dataSet.getYMin(); + + if (dataSet.getYMax() > mRightAxisMax) + mRightAxisMax = dataSet.getYMax(); + } + } + } + } + + /** ONLY GETTERS AND SETTERS BELOW THIS */ + + /** + * returns the number of LineDataSets this object contains + * + * @return + */ + public int getDataSetCount() { + if (mDataSets == null) + return 0; + return mDataSets.size(); + } + + /** + * Returns the smallest y-value the data object contains. + * + * @return + */ + public float getYMin() { + return mYMin; + } + + /** + * Returns the minimum y-value for the specified axis. + * + * @param axis + * @return + */ + public float getYMin(AxisDependency axis) { + if (axis == AxisDependency.LEFT) { + + if (mLeftAxisMin == Float.MAX_VALUE) { + return mRightAxisMin; + } else + return mLeftAxisMin; + } else { + if (mRightAxisMin == Float.MAX_VALUE) { + return mLeftAxisMin; + } else + return mRightAxisMin; + } + } + + /** + * Returns the greatest y-value the data object contains. + * + * @return + */ + public float getYMax() { + return mYMax; + } + + /** + * Returns the maximum y-value for the specified axis. + * + * @param axis + * @return + */ + public float getYMax(AxisDependency axis) { + if (axis == AxisDependency.LEFT) { + + if (mLeftAxisMax == -Float.MAX_VALUE) { + return mRightAxisMax; + } else + return mLeftAxisMax; + } else { + if (mRightAxisMax == -Float.MAX_VALUE) { + return mLeftAxisMax; + } else + return mRightAxisMax; + } + } + + /** + * Returns the minimum x-value this data object contains. + * + * @return + */ + public float getXMin() { + return mXMin; + } + + /** + * Returns the maximum x-value this data object contains. + * + * @return + */ + public float getXMax() { + return mXMax; + } + + /** + * Returns all DataSet objects this ChartData object holds. + * + * @return + */ + public List getDataSets() { + return mDataSets; + } + + /** + * Retrieve the index of a DataSet with a specific label from the ChartData. + * Search can be case sensitive or not. IMPORTANT: This method does + * calculations at runtime, do not over-use in performance critical + * situations. + * + * @param dataSets the DataSet array to search + * @param label + * @param ignorecase if true, the search is not case-sensitive + * @return + */ + protected int getDataSetIndexByLabel(List dataSets, String label, + boolean ignorecase) { + + if (ignorecase) { + for (int i = 0; i < dataSets.size(); i++) + if (label.equalsIgnoreCase(dataSets.get(i).getLabel())) + return i; + } else { + for (int i = 0; i < dataSets.size(); i++) + if (label.equals(dataSets.get(i).getLabel())) + return i; + } + + return -1; + } + + /** + * Returns the labels of all DataSets as a string array. + * + * @return + */ + public String[] getDataSetLabels() { + + String[] types = new String[mDataSets.size()]; + + for (int i = 0; i < mDataSets.size(); i++) { + types[i] = mDataSets.get(i).getLabel(); + } + + return types; + } + + /** + * Get the Entry for a corresponding highlight object + * + * @param highlight + * @return the entry that is highlighted + */ + public Entry getEntryForHighlight(Highlight highlight) { + if (highlight.getDataSetIndex() >= mDataSets.size()) + return null; + else { + return mDataSets.get(highlight.getDataSetIndex()).getEntryForXValue(highlight.getX(), highlight.getY()); + } + } + + /** + * Returns the DataSet object with the given label. Search can be case + * sensitive or not. IMPORTANT: This method does calculations at runtime. + * Use with care in performance critical situations. + * + * @param label + * @param ignorecase + * @return + */ + public T getDataSetByLabel(String label, boolean ignorecase) { + + int index = getDataSetIndexByLabel(mDataSets, label, ignorecase); + + if (index < 0 || index >= mDataSets.size()) + return null; + else + return mDataSets.get(index); + } + + public T getDataSetByIndex(int index) { + + if (mDataSets == null || index < 0 || index >= mDataSets.size()) + return null; + + return mDataSets.get(index); + } + + /** + * Adds a DataSet dynamically. + * + * @param d + */ + public void addDataSet(T d) { + + if (d == null) + return; + + calcMinMax(d); + + mDataSets.add(d); + } + + /** + * Removes the given DataSet from this data object. Also recalculates all + * minimum and maximum values. Returns true if a DataSet was removed, false + * if no DataSet could be removed. + * + * @param d + */ + public boolean removeDataSet(T d) { + + if (d == null) + return false; + + boolean removed = mDataSets.remove(d); + + // if a DataSet was removed + if (removed) { + notifyDataChanged(); + } + + return removed; + } + + /** + * Removes the DataSet at the given index in the DataSet array from the data + * object. Also recalculates all minimum and maximum values. Returns true if + * a DataSet was removed, false if no DataSet could be removed. + * + * @param index + */ + public boolean removeDataSet(int index) { + + if (index >= mDataSets.size() || index < 0) + return false; + + T set = mDataSets.get(index); + return removeDataSet(set); + } + + /** + * Adds an Entry to the DataSet at the specified index. + * Entries are added to the end of the list. + * + * @param e + * @param dataSetIndex + */ + public void addEntry(Entry e, int dataSetIndex) { + + if (mDataSets.size() > dataSetIndex && dataSetIndex >= 0) { + + IDataSet set = mDataSets.get(dataSetIndex); + // add the entry to the dataset + if (!set.addEntry(e)) + return; + + calcMinMax(e, set.getAxisDependency()); + + } else { + Log.e("addEntry", "Cannot add Entry because dataSetIndex too high or too low."); + } + } + + /** + * Adjusts the current minimum and maximum values based on the provided Entry object. + * + * @param e + * @param axis + */ + protected void calcMinMax(Entry e, AxisDependency axis) { + + if (mYMax < e.getY()) + mYMax = e.getY(); + if (mYMin > e.getY()) + mYMin = e.getY(); + + if (mXMax < e.getX()) + mXMax = e.getX(); + if (mXMin > e.getX()) + mXMin = e.getX(); + + if (axis == AxisDependency.LEFT) { + + if (mLeftAxisMax < e.getY()) + mLeftAxisMax = e.getY(); + if (mLeftAxisMin > e.getY()) + mLeftAxisMin = e.getY(); + } else { + if (mRightAxisMax < e.getY()) + mRightAxisMax = e.getY(); + if (mRightAxisMin > e.getY()) + mRightAxisMin = e.getY(); + } + } + + /** + * Adjusts the minimum and maximum values based on the given DataSet. + * + * @param d + */ + protected void calcMinMax(T d) { + + if (mYMax < d.getYMax()) + mYMax = d.getYMax(); + if (mYMin > d.getYMin()) + mYMin = d.getYMin(); + + if (mXMax < d.getXMax()) + mXMax = d.getXMax(); + if (mXMin > d.getXMin()) + mXMin = d.getXMin(); + + if (d.getAxisDependency() == AxisDependency.LEFT) { + + if (mLeftAxisMax < d.getYMax()) + mLeftAxisMax = d.getYMax(); + if (mLeftAxisMin > d.getYMin()) + mLeftAxisMin = d.getYMin(); + } else { + if (mRightAxisMax < d.getYMax()) + mRightAxisMax = d.getYMax(); + if (mRightAxisMin > d.getYMin()) + mRightAxisMin = d.getYMin(); + } + } + + /** + * Removes the given Entry object from the DataSet at the specified index. + * + * @param e + * @param dataSetIndex + */ + public boolean removeEntry(Entry e, int dataSetIndex) { + + // entry null, outofbounds + if (e == null || dataSetIndex >= mDataSets.size()) + return false; + + IDataSet set = mDataSets.get(dataSetIndex); + + if (set != null) { + // remove the entry from the dataset + boolean removed = set.removeEntry(e); + + if (removed) { + notifyDataChanged(); + } + + return removed; + } else + return false; + } + + /** + * Removes the Entry object closest to the given DataSet at the + * specified index. Returns true if an Entry was removed, false if no Entry + * was found that meets the specified requirements. + * + * @param xValue + * @param dataSetIndex + * @return + */ + public boolean removeEntry(float xValue, int dataSetIndex) { + + if (dataSetIndex >= mDataSets.size()) + return false; + + IDataSet dataSet = mDataSets.get(dataSetIndex); + Entry e = dataSet.getEntryForXValue(xValue, Float.NaN); + + if (e == null) + return false; + + return removeEntry(e, dataSetIndex); + } + + /** + * Returns the DataSet that contains the provided Entry, or null, if no + * DataSet contains this Entry. + * + * @param e + * @return + */ + public T getDataSetForEntry(Entry e) { + + if (e == null) + return null; + + for (int i = 0; i < mDataSets.size(); i++) { + + T set = mDataSets.get(i); + + for (int j = 0; j < set.getEntryCount(); j++) { + if (e.equalTo(set.getEntryForXValue(e.getX(), e.getY()))) + return set; + } + } + + return null; + } + + /** + * Returns all colors used across all DataSet objects this object + * represents. + * + * @return + */ + public int[] getColors() { + + if (mDataSets == null) + return null; + + int clrcnt = 0; + + for (int i = 0; i < mDataSets.size(); i++) { + clrcnt += mDataSets.get(i).getColors().size(); + } + + int[] colors = new int[clrcnt]; + int cnt = 0; + + for (int i = 0; i < mDataSets.size(); i++) { + + List clrs = mDataSets.get(i).getColors(); + + for (Integer clr : clrs) { + colors[cnt] = clr; + cnt++; + } + } + + return colors; + } + + /** + * Returns the index of the provided DataSet in the DataSet array of this data object, or -1 if it does not exist. + * + * @param dataSet + * @return + */ + public int getIndexOfDataSet(T dataSet) { + return mDataSets.indexOf(dataSet); + } + + /** + * Returns the first DataSet from the datasets-array that has it's dependency on the left axis. + * Returns null if no DataSet with left dependency could be found. + * + * @return + */ + protected T getFirstLeft(List sets) { + for (T dataSet : sets) { + if (dataSet.getAxisDependency() == AxisDependency.LEFT) + return dataSet; + } + return null; + } + + /** + * Returns the first DataSet from the datasets-array that has it's dependency on the right axis. + * Returns null if no DataSet with right dependency could be found. + * + * @return + */ + public T getFirstRight(List sets) { + for (T dataSet : sets) { + if (dataSet.getAxisDependency() == AxisDependency.RIGHT) + return dataSet; + } + return null; + } + + /** + * Sets a custom IValueFormatter for all DataSets this data object contains. + * + * @param f + */ + public void setValueFormatter(IValueFormatter f) { + if (f == null) + return; + else { + for (IDataSet set : mDataSets) { + set.setValueFormatter(f); + } + } + } + + /** + * Sets the color of the value-text (color in which the value-labels are + * drawn) for all DataSets this data object contains. + * + * @param color + */ + public void setValueTextColor(int color) { + for (IDataSet set : mDataSets) { + set.setValueTextColor(color); + } + } + + /** + * Sets the same list of value-colors for all DataSets this + * data object contains. + * + * @param colors + */ + public void setValueTextColors(List colors) { + for (IDataSet set : mDataSets) { + set.setValueTextColors(colors); + } + } + + /** + * Sets the Typeface for all value-labels for all DataSets this data object + * contains. + * + * @param tf + */ + public void setValueTypeface(Typeface tf) { + for (IDataSet set : mDataSets) { + set.setValueTypeface(tf); + } + } + + /** + * Sets the size (in dp) of the value-text for all DataSets this data object + * contains. + * + * @param size + */ + public void setValueTextSize(float size) { + for (IDataSet set : mDataSets) { + set.setValueTextSize(size); + } + } + + /** + * Enables / disables drawing values (value-text) for all DataSets this data + * object contains. + * + * @param enabled + */ + public void setDrawValues(boolean enabled) { + for (IDataSet set : mDataSets) { + set.setDrawValues(enabled); + } + } + + /** + * Enables / disables highlighting values for all DataSets this data object + * contains. If set to true, this means that values can + * be highlighted programmatically or by touch gesture. + */ + public void setHighlightEnabled(boolean enabled) { + for (IDataSet set : mDataSets) { + set.setHighlightEnabled(enabled); + } + } + + /** + * Returns true if highlighting of all underlying values is enabled, false + * if not. + * + * @return + */ + public boolean isHighlightEnabled() { + for (IDataSet set : mDataSets) { + if (!set.isHighlightEnabled()) + return false; + } + return true; + } + + /** + * Clears this data object from all DataSets and removes all Entries. Don't + * forget to invalidate the chart after this. + */ + public void clearValues() { + if (mDataSets != null) { + mDataSets.clear(); + } + notifyDataChanged(); + } + + /** + * Checks if this data object contains the specified DataSet. Returns true + * if so, false if not. + * + * @param dataSet + * @return + */ + public boolean contains(T dataSet) { + + for (T set : mDataSets) { + if (set.equals(dataSet)) + return true; + } + + return false; + } + + /** + * Returns the total entry count across all DataSet objects this data object contains. + * + * @return + */ + public int getEntryCount() { + + int count = 0; + + for (T set : mDataSets) { + count += set.getEntryCount(); + } + + return count; + } + + /** + * Returns the DataSet object with the maximum number of entries or null if there are no DataSets. + * + * @return + */ + public T getMaxEntryCountSet() { + + if (mDataSets == null || mDataSets.isEmpty()) + return null; + + T max = mDataSets.get(0); + + for (T set : mDataSets) { + + if (set.getEntryCount() > max.getEntryCount()) + max = set; + } + + return max; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/CombinedData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CombinedData.java new file mode 100644 index 0000000..0b36aa3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CombinedData.java @@ -0,0 +1,272 @@ + +package com.github.mikephil.charting.data; + +import android.util.Log; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data object that allows the combination of Line-, Bar-, Scatter-, Bubble- and + * CandleData. Used in the CombinedChart class. + * + * @author Philipp Jahoda + */ +public class CombinedData extends BarLineScatterCandleBubbleData> { + + private LineData mLineData; + private BarData mBarData; + private ScatterData mScatterData; + private CandleData mCandleData; + private BubbleData mBubbleData; + + public CombinedData() { + super(); + } + + public void setData(LineData data) { + mLineData = data; + notifyDataChanged(); + } + + public void setData(BarData data) { + mBarData = data; + notifyDataChanged(); + } + + public void setData(ScatterData data) { + mScatterData = data; + notifyDataChanged(); + } + + public void setData(CandleData data) { + mCandleData = data; + notifyDataChanged(); + } + + public void setData(BubbleData data) { + mBubbleData = data; + notifyDataChanged(); + } + + @Override + public void calcMinMax() { + + if(mDataSets == null){ + mDataSets = new ArrayList<>(); + } + mDataSets.clear(); + + mYMax = -Float.MAX_VALUE; + mYMin = Float.MAX_VALUE; + mXMax = -Float.MAX_VALUE; + mXMin = Float.MAX_VALUE; + + mLeftAxisMax = -Float.MAX_VALUE; + mLeftAxisMin = Float.MAX_VALUE; + mRightAxisMax = -Float.MAX_VALUE; + mRightAxisMin = Float.MAX_VALUE; + + List allData = getAllData(); + + for (ChartData data : allData) { + + data.calcMinMax(); + + List> sets = data.getDataSets(); + mDataSets.addAll(sets); + + if (data.getYMax() > mYMax) + mYMax = data.getYMax(); + + if (data.getYMin() < mYMin) + mYMin = data.getYMin(); + + if (data.getXMax() > mXMax) + mXMax = data.getXMax(); + + if (data.getXMin() < mXMin) + mXMin = data.getXMin(); + + for (IBarLineScatterCandleBubbleDataSet dataset : sets) { + if (dataset.getAxisDependency() == YAxis.AxisDependency.LEFT) { + if (dataset.getYMax() > mLeftAxisMax) { + mLeftAxisMax = dataset.getYMax(); + } + + if (dataset.getYMin() < mLeftAxisMin) { + mLeftAxisMin = dataset.getYMin(); + } + } + else { + if (dataset.getYMax() > mRightAxisMax) { + mRightAxisMax = dataset.getYMax(); + } + + if (dataset.getYMin() < mRightAxisMin) { + mRightAxisMin = dataset.getYMin(); + } + } + } + } + } + + public BubbleData getBubbleData() { + return mBubbleData; + } + + public LineData getLineData() { + return mLineData; + } + + public BarData getBarData() { + return mBarData; + } + + public ScatterData getScatterData() { + return mScatterData; + } + + public CandleData getCandleData() { + return mCandleData; + } + + /** + * Returns all data objects in row: line-bar-scatter-candle-bubble if not null. + * + * @return + */ + public List getAllData() { + + List data = new ArrayList(); + if (mLineData != null) + data.add(mLineData); + if (mBarData != null) + data.add(mBarData); + if (mScatterData != null) + data.add(mScatterData); + if (mCandleData != null) + data.add(mCandleData); + if (mBubbleData != null) + data.add(mBubbleData); + + return data; + } + + public BarLineScatterCandleBubbleData getDataByIndex(int index) { + return getAllData().get(index); + } + + @Override + public void notifyDataChanged() { + if (mLineData != null) + mLineData.notifyDataChanged(); + if (mBarData != null) + mBarData.notifyDataChanged(); + if (mCandleData != null) + mCandleData.notifyDataChanged(); + if (mScatterData != null) + mScatterData.notifyDataChanged(); + if (mBubbleData != null) + mBubbleData.notifyDataChanged(); + + calcMinMax(); // recalculate everything + } + + /** + * Get the Entry for a corresponding highlight object + * + * @param highlight + * @return the entry that is highlighted + */ + @Override + public Entry getEntryForHighlight(Highlight highlight) { + + if (highlight.getDataIndex() >= getAllData().size()) + return null; + + ChartData data = getDataByIndex(highlight.getDataIndex()); + + if (highlight.getDataSetIndex() >= data.getDataSetCount()) + return null; + + // The value of the highlighted entry could be NaN - + // if we are not interested in highlighting a specific value. + + List entries = data.getDataSetByIndex(highlight.getDataSetIndex()) + .getEntriesForXValue(highlight.getX()); + for (Entry entry : entries) + if (entry.getY() == highlight.getY() || + Float.isNaN(highlight.getY())) + return entry; + + return null; + } + + /** + * Get dataset for highlight + * + * @param highlight current highlight + * @return dataset related to highlight + */ + public IBarLineScatterCandleBubbleDataSet getDataSetByHighlight(Highlight highlight) { + if (highlight.getDataIndex() >= getAllData().size()) + return null; + + BarLineScatterCandleBubbleData data = getDataByIndex(highlight.getDataIndex()); + + if (highlight.getDataSetIndex() >= data.getDataSetCount()) + return null; + + return (IBarLineScatterCandleBubbleDataSet) + data.getDataSets().get(highlight.getDataSetIndex()); + } + + public int getDataIndex(ChartData data) { + return getAllData().indexOf(data); + } + + @Override + public boolean removeDataSet(IBarLineScatterCandleBubbleDataSet d) { + + List datas = getAllData(); + + boolean success = false; + + for (ChartData data : datas) { + + success = data.removeDataSet(d); + + if (success) { + break; + } + } + + return success; + } + + @Deprecated + @Override + public boolean removeDataSet(int index) { + Log.e("MPAndroidChart", "removeDataSet(int index) not supported for CombinedData"); + return false; + } + + @Deprecated + @Override + public boolean removeEntry(Entry e, int dataSetIndex) { + Log.e("MPAndroidChart", "removeEntry(...) not supported for CombinedData"); + return false; + } + + @Deprecated + @Override + public boolean removeEntry(float xValue, int dataSetIndex) { + Log.e("MPAndroidChart", "removeEntry(...) not supported for CombinedData"); + return false; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/DataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/DataSet.java new file mode 100644 index 0000000..fda07ef --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/DataSet.java @@ -0,0 +1,456 @@ + +package com.github.mikephil.charting.data; + +import java.util.ArrayList; +import java.util.List; + +/** + * The DataSet class represents one group or type of entries (Entry) in the + * Chart that belong together. It is designed to logically separate different + * groups of values inside the Chart (e.g. the values for a specific line in the + * LineChart, or the values of a specific group of bars in the BarChart). + * + * @author Philipp Jahoda + */ +public abstract class DataSet extends BaseDataSet { + + /** + * the entries that this DataSet represents / holds together + */ + protected List mEntries; + + /** + * maximum y-value in the value array + */ + protected float mYMax = -Float.MAX_VALUE; + + /** + * minimum y-value in the value array + */ + protected float mYMin = Float.MAX_VALUE; + + /** + * maximum x-value in the value array + */ + protected float mXMax = -Float.MAX_VALUE; + + /** + * minimum x-value in the value array + */ + protected float mXMin = Float.MAX_VALUE; + + + /** + * Creates a new DataSet object with the given values (entries) it represents. Also, a + * label that describes the DataSet can be specified. The label can also be + * used to retrieve the DataSet from a ChartData object. + * + * @param entries + * @param label + */ + public DataSet(List entries, String label) { + super(label); + this.mEntries = entries; + + if (mEntries == null) + mEntries = new ArrayList(); + + calcMinMax(); + } + + @Override + public void calcMinMax() { + + mYMax = -Float.MAX_VALUE; + mYMin = Float.MAX_VALUE; + mXMax = -Float.MAX_VALUE; + mXMin = Float.MAX_VALUE; + + if (mEntries == null || mEntries.isEmpty()) + return; + + for (T e : mEntries) { + calcMinMax(e); + } + } + + @Override + public void calcMinMaxY(float fromX, float toX) { + mYMax = -Float.MAX_VALUE; + mYMin = Float.MAX_VALUE; + + if (mEntries == null || mEntries.isEmpty()) + return; + + int indexFrom = getEntryIndex(fromX, Float.NaN, Rounding.DOWN); + int indexTo = getEntryIndex(toX, Float.NaN, Rounding.UP); + + if (indexTo < indexFrom) return; + + for (int i = indexFrom; i <= indexTo; i++) { + + // only recalculate y + calcMinMaxY(mEntries.get(i)); + } + } + + /** + * Updates the min and max x and y value of this DataSet based on the given Entry. + * + * @param e + */ + protected void calcMinMax(T e) { + + if (e == null) + return; + + calcMinMaxX(e); + + calcMinMaxY(e); + } + + protected void calcMinMaxX(T e) { + + if (e.getX() < mXMin) + mXMin = e.getX(); + + if (e.getX() > mXMax) + mXMax = e.getX(); + } + + protected void calcMinMaxY(T e) { + + if (e.getY() < mYMin) + mYMin = e.getY(); + + if (e.getY() > mYMax) + mYMax = e.getY(); + } + + @Override + public int getEntryCount() { + return mEntries.size(); + } + + /** + * This method is deprecated. + * Use getEntries() instead. + * + * @return + */ + @Deprecated + public List getValues() { + return mEntries; + } + + /** + * Returns the array of entries that this DataSet represents. + * + * @return + */ + public List getEntries() { + return mEntries; + } + + /** + * This method is deprecated. + * Use setEntries(...) instead. + * + * @param values + */ + @Deprecated + public void setValues(List values) { + setEntries(values); + } + + /** + * Sets the array of entries that this DataSet represents, and calls notifyDataSetChanged() + * + * @return + */ + public void setEntries(List entries) { + mEntries = entries; + notifyDataSetChanged(); + } + + /** + * Provides an exact copy of the DataSet this method is used on. + * + * @return + */ + public abstract DataSet copy(); + + /** + * + * @param dataSet + */ + protected void copy(DataSet dataSet) { + super.copy(dataSet); + } + + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(toSimpleString()); + for (int i = 0; i < mEntries.size(); i++) { + buffer.append(mEntries.get(i).toString() + " "); + } + return buffer.toString(); + } + + /** + * Returns a simple string representation of the DataSet with the type and + * the number of Entries. + * + * @return + */ + public String toSimpleString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("DataSet, label: " + (getLabel() == null ? "" : getLabel()) + ", entries: " + mEntries.size() + + "\n"); + return buffer.toString(); + } + + @Override + public float getYMin() { + return mYMin; + } + + @Override + public float getYMax() { + return mYMax; + } + + @Override + public float getXMin() { + return mXMin; + } + + @Override + public float getXMax() { + return mXMax; + } + + @Override + public void addEntryOrdered(T e) { + + if (e == null) + return; + + if (mEntries == null) { + mEntries = new ArrayList(); + } + + calcMinMax(e); + + if (mEntries.size() > 0 && mEntries.get(mEntries.size() - 1).getX() > e.getX()) { + int closestIndex = getEntryIndex(e.getX(), e.getY(), Rounding.UP); + mEntries.add(closestIndex, e); + } else { + mEntries.add(e); + } + } + + @Override + public void clear() { + mEntries.clear(); + notifyDataSetChanged(); + } + + @Override + public boolean addEntry(T e) { + + if (e == null) + return false; + + List values = getEntries(); + if (values == null) { + values = new ArrayList<>(); + } + + calcMinMax(e); + + // add the entry + return values.add(e); + } + + @Override + public boolean removeEntry(T e) { + + if (e == null) + return false; + + if (mEntries == null) + return false; + + // remove the entry + boolean removed = mEntries.remove(e); + + if (removed) { + calcMinMax(); + } + + return removed; + } + + @Override + public int getEntryIndex(Entry e) { + return mEntries.indexOf(e); + } + + @Override + public T getEntryForXValue(float xValue, float closestToY, Rounding rounding) { + + int index = getEntryIndex(xValue, closestToY, rounding); + if (index > -1) + return mEntries.get(index); + return null; + } + + @Override + public T getEntryForXValue(float xValue, float closestToY) { + return getEntryForXValue(xValue, closestToY, Rounding.CLOSEST); + } + + @Override + public T getEntryForIndex(int index) { + return mEntries.get(index); + } + + @Override + public int getEntryIndex(float xValue, float closestToY, Rounding rounding) { + + if (mEntries == null || mEntries.isEmpty()) + return -1; + + int low = 0; + int high = mEntries.size() - 1; + int closest = high; + + while (low < high) { + int m = (low + high) / 2; + + final float d1 = mEntries.get(m).getX() - xValue, + d2 = mEntries.get(m + 1).getX() - xValue, + ad1 = Math.abs(d1), ad2 = Math.abs(d2); + + if (ad2 < ad1) { + // [m + 1] is closer to xValue + // Search in an higher place + low = m + 1; + } else if (ad1 < ad2) { + // [m] is closer to xValue + // Search in a lower place + high = m; + } else { + // We have multiple sequential x-value with same distance + + if (d1 >= 0.0) { + // Search in a lower place + high = m; + } else if (d1 < 0.0) { + // Search in an higher place + low = m + 1; + } + } + + closest = high; + } + + if (closest != -1) { + float closestXValue = mEntries.get(closest).getX(); + if (rounding == Rounding.UP) { + // If rounding up, and found x-value is lower than specified x, and we can go upper... + if (closestXValue < xValue && closest < mEntries.size() - 1) { + ++closest; + } + } else if (rounding == Rounding.DOWN) { + // If rounding down, and found x-value is upper than specified x, and we can go lower... + if (closestXValue > xValue && closest > 0) { + --closest; + } + } + + // Search by closest to y-value + if (!Float.isNaN(closestToY)) { + while (closest > 0 && mEntries.get(closest - 1).getX() == closestXValue) + closest -= 1; + + float closestYValue = mEntries.get(closest).getY(); + int closestYIndex = closest; + + while (true) { + closest += 1; + if (closest >= mEntries.size()) + break; + + final Entry value = mEntries.get(closest); + + if (value.getX() != closestXValue) + break; + + if (Math.abs(value.getY() - closestToY) <= Math.abs(closestYValue - closestToY)) { + closestYValue = closestToY; + closestYIndex = closest; + } + } + + closest = closestYIndex; + } + } + + return closest; + } + + @Override + public List getEntriesForXValue(float xValue) { + + List entries = new ArrayList(); + + int low = 0; + int high = mEntries.size() - 1; + + while (low <= high) { + int m = (high + low) / 2; + T entry = mEntries.get(m); + + // if we have a match + if (xValue == entry.getX()) { + while (m > 0 && mEntries.get(m - 1).getX() == xValue) + m--; + + high = mEntries.size(); + + // loop over all "equal" entries + for (; m < high; m++) { + entry = mEntries.get(m); + if (entry.getX() == xValue) { + entries.add(entry); + } else { + break; + } + } + + break; + } else { + if (xValue > entry.getX()) + low = m + 1; + else + high = m - 1; + } + } + + return entries; + } + + /** + * Determines how to round DataSet index values for + * {@link DataSet#getEntryIndex(float, float, Rounding)} DataSet.getEntryIndex()} + * when an exact x-index is not found. + */ + public enum Rounding { + UP, + DOWN, + CLOSEST, + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/Entry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/Entry.java new file mode 100644 index 0000000..b7a8879 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/Entry.java @@ -0,0 +1,173 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.ParcelFormatException; +import android.os.Parcelable; + +import com.github.mikephil.charting.utils.Utils; + +/** + * Class representing one entry in the chart. Might contain multiple values. + * Might only contain a single value depending on the used constructor. + * + * @author Philipp Jahoda + */ +public class Entry extends BaseEntry implements Parcelable { + + /** the x value */ + private float x = 0f; + + public Entry() { + + } + + /** + * A Entry represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + */ + public Entry(float x, float y) { + super(y); + this.x = x; + } + + /** + * A Entry represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param data Spot for additional data this Entry represents. + */ + public Entry(float x, float y, Object data) { + super(y, data); + this.x = x; + } + + /** + * A Entry represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param icon icon image + */ + public Entry(float x, float y, Drawable icon) { + super(y, icon); + this.x = x; + } + + /** + * A Entry represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param icon icon image + * @param data Spot for additional data this Entry represents. + */ + public Entry(float x, float y, Drawable icon, Object data) { + super(y, icon, data); + this.x = x; + } + + /** + * Returns the x-value of this Entry object. + * + * @return + */ + public float getX() { + return x; + } + + /** + * Sets the x-value of this Entry object. + * + * @param x + */ + public void setX(float x) { + this.x = x; + } + + /** + * returns an exact copy of the entry + * + * @return + */ + public Entry copy() { + Entry e = new Entry(x, getY(), getData()); + return e; + } + + /** + * Compares value, xIndex and data of the entries. Returns true if entries + * are equal in those points, false if not. Does not check by hash-code like + * it's done by the "equals" method. + * + * @param e + * @return + */ + public boolean equalTo(Entry e) { + + if (e == null) + return false; + + if (e.getData() != this.getData()) + return false; + + if (Math.abs(e.x - this.x) > Utils.FLOAT_EPSILON) + return false; + + if (Math.abs(e.getY() - this.getY()) > Utils.FLOAT_EPSILON) + return false; + + return true; + } + + /** + * returns a string representation of the entry containing x-index and value + */ + @Override + public String toString() { + return "Entry, x: " + x + " y: " + getY(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(this.x); + dest.writeFloat(this.getY()); + if (getData() != null) { + if (getData() instanceof Parcelable) { + dest.writeInt(1); + dest.writeParcelable((Parcelable) this.getData(), flags); + } else { + throw new ParcelFormatException("Cannot parcel an Entry with non-parcelable data"); + } + } else { + dest.writeInt(0); + } + } + + protected Entry(Parcel in) { + this.x = in.readFloat(); + this.setY(in.readFloat()); + if (in.readInt() == 1) { + this.setData(in.readParcelable(Object.class.getClassLoader())); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Entry createFromParcel(Parcel source) { + return new Entry(source); + } + + public Entry[] newArray(int size) { + return new Entry[size]; + } + }; +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineData.java new file mode 100644 index 0000000..4cf5448 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineData.java @@ -0,0 +1,27 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data object that encapsulates all data associated with a LineChart. + * + * @author Philipp Jahoda + */ +public class LineData extends BarLineScatterCandleBubbleData { + + public LineData() { + super(); + } + + public LineData(ILineDataSet... dataSets) { + super(dataSets); + } + + public LineData(List dataSets) { + super(dataSets); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineDataSet.java new file mode 100644 index 0000000..10d1837 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineDataSet.java @@ -0,0 +1,417 @@ + +package com.github.mikephil.charting.data; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.util.Log; + +import com.github.mikephil.charting.formatter.DefaultFillFormatter; +import com.github.mikephil.charting.formatter.IFillFormatter; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +public class LineDataSet extends LineRadarDataSet implements ILineDataSet { + + /** + * Drawing mode for this line dataset + **/ + private LineDataSet.Mode mMode = Mode.LINEAR; + + /** + * List representing all colors that are used for the circles + */ + private List mCircleColors = null; + + /** + * the color of the inner circles + */ + private int mCircleHoleColor = Color.WHITE; + + /** + * the radius of the circle-shaped value indicators + */ + private float mCircleRadius = 8f; + + /** + * the hole radius of the circle-shaped value indicators + */ + private float mCircleHoleRadius = 4f; + + /** + * sets the intensity of the cubic lines + */ + private float mCubicIntensity = 0.2f; + + /** + * the path effect of this DataSet that makes dashed lines possible + */ + private DashPathEffect mDashPathEffect = null; + + /** + * formatter for customizing the position of the fill-line + */ + private IFillFormatter mFillFormatter = new DefaultFillFormatter(); + + /** + * if true, drawing circles is enabled + */ + private boolean mDrawCircles = true; + + private boolean mDrawCircleHole = true; + + + public LineDataSet(List yVals, String label) { + super(yVals, label); + + // mCircleRadius = Utils.convertDpToPixel(4f); + // mLineWidth = Utils.convertDpToPixel(1f); + + if (mCircleColors == null) { + mCircleColors = new ArrayList(); + } + mCircleColors.clear(); + + // default colors + // mColors.add(Color.rgb(192, 255, 140)); + // mColors.add(Color.rgb(255, 247, 140)); + mCircleColors.add(Color.rgb(140, 234, 255)); + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + LineDataSet copied = new LineDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(LineDataSet lineDataSet) { + super.copy(lineDataSet); + lineDataSet.mCircleColors = mCircleColors; + lineDataSet.mCircleHoleColor = mCircleHoleColor; + lineDataSet.mCircleHoleRadius = mCircleHoleRadius; + lineDataSet.mCircleRadius = mCircleRadius; + lineDataSet.mCubicIntensity = mCubicIntensity; + lineDataSet.mDashPathEffect = mDashPathEffect; + lineDataSet.mDrawCircleHole = mDrawCircleHole; + lineDataSet.mDrawCircles = mDrawCircleHole; + lineDataSet.mFillFormatter = mFillFormatter; + lineDataSet.mMode = mMode; + } + + /** + * Returns the drawing mode for this line dataset + * + * @return + */ + @Override + public LineDataSet.Mode getMode() { + return mMode; + } + + /** + * Returns the drawing mode for this LineDataSet + * + * @return + */ + public void setMode(LineDataSet.Mode mode) { + mMode = mode; + } + + /** + * Sets the intensity for cubic lines (if enabled). Max = 1f = very cubic, + * Min = 0.05f = low cubic effect, Default: 0.2f + * + * @param intensity + */ + public void setCubicIntensity(float intensity) { + + if (intensity > 1f) + intensity = 1f; + if (intensity < 0.05f) + intensity = 0.05f; + + mCubicIntensity = intensity; + } + + @Override + public float getCubicIntensity() { + return mCubicIntensity; + } + + + /** + * Sets the radius of the drawn circles. + * Default radius = 4f, Min = 1f + * + * @param radius + */ + public void setCircleRadius(float radius) { + + if (radius >= 1f) { + mCircleRadius = Utils.convertDpToPixel(radius); + } else { + Log.e("LineDataSet", "Circle radius cannot be < 1"); + } + } + + @Override + public float getCircleRadius() { + return mCircleRadius; + } + + /** + * Sets the hole radius of the drawn circles. + * Default radius = 2f, Min = 0.5f + * + * @param holeRadius + */ + public void setCircleHoleRadius(float holeRadius) { + + if (holeRadius >= 0.5f) { + mCircleHoleRadius = Utils.convertDpToPixel(holeRadius); + } else { + Log.e("LineDataSet", "Circle radius cannot be < 0.5"); + } + } + + @Override + public float getCircleHoleRadius() { + return mCircleHoleRadius; + } + + /** + * sets the size (radius) of the circle shpaed value indicators, + * default size = 4f + *

+ * This method is deprecated because of unclarity. Use setCircleRadius instead. + * + * @param size + */ + @Deprecated + public void setCircleSize(float size) { + setCircleRadius(size); + } + + /** + * This function is deprecated because of unclarity. Use getCircleRadius instead. + */ + @Deprecated + public float getCircleSize() { + return getCircleRadius(); + } + + /** + * Enables the line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space in between the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableDashedLine(float lineLength, float spaceLength, float phase) { + mDashPathEffect = new DashPathEffect(new float[]{ + lineLength, spaceLength + }, phase); + } + + /** + * Disables the line to be drawn in dashed mode. + */ + public void disableDashedLine() { + mDashPathEffect = null; + } + + @Override + public boolean isDashedLineEnabled() { + return mDashPathEffect == null ? false : true; + } + + @Override + public DashPathEffect getDashPathEffect() { + return mDashPathEffect; + } + + /** + * set this to true to enable the drawing of circle indicators for this + * DataSet, default true + * + * @param enabled + */ + public void setDrawCircles(boolean enabled) { + this.mDrawCircles = enabled; + } + + @Override + public boolean isDrawCirclesEnabled() { + return mDrawCircles; + } + + @Deprecated + @Override + public boolean isDrawCubicEnabled() { + return mMode == Mode.CUBIC_BEZIER; + } + + @Deprecated + @Override + public boolean isDrawSteppedEnabled() { + return mMode == Mode.STEPPED; + } + + /** ALL CODE BELOW RELATED TO CIRCLE-COLORS */ + + /** + * returns all colors specified for the circles + * + * @return + */ + public List getCircleColors() { + return mCircleColors; + } + + @Override + public int getCircleColor(int index) { + return mCircleColors.get(index); + } + + @Override + public int getCircleColorCount() { + return mCircleColors.size(); + } + + /** + * Sets the colors that should be used for the circles of this DataSet. + * Colors are reused as soon as the number of Entries the DataSet represents + * is higher than the size of the colors array. Make sure that the colors + * are already prepared (by calling getResources().getColor(...)) before + * adding them to the DataSet. + * + * @param colors + */ + public void setCircleColors(List colors) { + mCircleColors = colors; + } + + /** + * Sets the colors that should be used for the circles of this DataSet. + * Colors are reused as soon as the number of Entries the DataSet represents + * is higher than the size of the colors array. Make sure that the colors + * are already prepared (by calling getResources().getColor(...)) before + * adding them to the DataSet. + * + * @param colors + */ + public void setCircleColors(int... colors) { + this.mCircleColors = ColorTemplate.createColors(colors); + } + + /** + * ets the colors that should be used for the circles of this DataSet. + * Colors are reused as soon as the number of Entries the DataSet represents + * is higher than the size of the colors array. You can use + * "new String[] { R.color.red, R.color.green, ... }" to provide colors for + * this method. Internally, the colors are resolved using + * getResources().getColor(...) + * + * @param colors + */ + public void setCircleColors(int[] colors, Context c) { + + List clrs = mCircleColors; + if (clrs == null) { + clrs = new ArrayList<>(); + } + clrs.clear(); + + for (int color : colors) { + clrs.add(c.getResources().getColor(color)); + } + + mCircleColors = clrs; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet. + * Internally, this recreates the colors array and adds the specified color. + * + * @param color + */ + public void setCircleColor(int color) { + resetCircleColors(); + mCircleColors.add(color); + } + + /** + * resets the circle-colors array and creates a new one + */ + public void resetCircleColors() { + if (mCircleColors == null) { + mCircleColors = new ArrayList(); + } + mCircleColors.clear(); + } + + /** + * Sets the color of the inner circle of the line-circles. + * + * @param color + */ + public void setCircleHoleColor(int color) { + mCircleHoleColor = color; + } + + @Override + public int getCircleHoleColor() { + return mCircleHoleColor; + } + + /** + * Set this to true to allow drawing a hole in each data circle. + * + * @param enabled + */ + public void setDrawCircleHole(boolean enabled) { + mDrawCircleHole = enabled; + } + + @Override + public boolean isDrawCircleHoleEnabled() { + return mDrawCircleHole; + } + + /** + * Sets a custom IFillFormatter to the chart that handles the position of the + * filled-line for each DataSet. Set this to null to use the default logic. + * + * @param formatter + */ + public void setFillFormatter(IFillFormatter formatter) { + + if (formatter == null) + mFillFormatter = new DefaultFillFormatter(); + else + mFillFormatter = formatter; + } + + @Override + public IFillFormatter getFillFormatter() { + return mFillFormatter; + } + + public enum Mode { + LINEAR, + STEPPED, + CUBIC_BEZIER, + HORIZONTAL_BEZIER + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineRadarDataSet.java new file mode 100644 index 0000000..b4347e4 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineRadarDataSet.java @@ -0,0 +1,135 @@ + +package com.github.mikephil.charting.data; + +import android.annotation.TargetApi; +import android.graphics.Color; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.interfaces.datasets.ILineRadarDataSet; +import com.github.mikephil.charting.utils.Utils; + +import java.util.List; + +/** + * Base dataset for line and radar DataSets. + * + * @author Philipp Jahoda + */ +public abstract class LineRadarDataSet extends LineScatterCandleRadarDataSet implements ILineRadarDataSet { + + // TODO: Move to using `Fill` class + /** + * the color that is used for filling the line surface + */ + private int mFillColor = Color.rgb(140, 234, 255); + + /** + * the drawable to be used for filling the line surface + */ + protected Drawable mFillDrawable; + + /** + * transparency used for filling line surface + */ + private int mFillAlpha = 85; + + /** + * the width of the drawn data lines + */ + private float mLineWidth = 2.5f; + + /** + * if true, the data will also be drawn filled + */ + private boolean mDrawFilled = false; + + + public LineRadarDataSet(List yVals, String label) { + super(yVals, label); + } + + @Override + public int getFillColor() { + return mFillColor; + } + + /** + * Sets the color that is used for filling the area below the line. + * Resets an eventually set "fillDrawable". + * + * @param color + */ + public void setFillColor(int color) { + mFillColor = color; + mFillDrawable = null; + } + + @Override + public Drawable getFillDrawable() { + return mFillDrawable; + } + + /** + * Sets the drawable to be used to fill the area below the line. + * + * @param drawable + */ + @TargetApi(18) + public void setFillDrawable(Drawable drawable) { + this.mFillDrawable = drawable; + } + + @Override + public int getFillAlpha() { + return mFillAlpha; + } + + /** + * sets the alpha value (transparency) that is used for filling the line + * surface (0-255), default: 85 + * + * @param alpha + */ + public void setFillAlpha(int alpha) { + mFillAlpha = alpha; + } + + /** + * set the line width of the chart (min = 0.2f, max = 10f); default 1f NOTE: + * thinner line == better performance, thicker line == worse performance + * + * @param width + */ + public void setLineWidth(float width) { + + if (width < 0.0f) + width = 0.0f; + if (width > 10.0f) + width = 10.0f; + mLineWidth = Utils.convertDpToPixel(width); + } + + @Override + public float getLineWidth() { + return mLineWidth; + } + + @Override + public void setDrawFilled(boolean filled) { + mDrawFilled = filled; + } + + @Override + public boolean isDrawFilledEnabled() { + return mDrawFilled; + } + + protected void copy(LineRadarDataSet lineRadarDataSet) { + super.copy(lineRadarDataSet); + lineRadarDataSet.mDrawFilled = mDrawFilled; + lineRadarDataSet.mFillAlpha = mFillAlpha; + lineRadarDataSet.mFillColor = mFillColor; + lineRadarDataSet.mFillDrawable = mFillDrawable; + lineRadarDataSet.mLineWidth = mLineWidth; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineScatterCandleRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineScatterCandleRadarDataSet.java new file mode 100644 index 0000000..d4618d8 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineScatterCandleRadarDataSet.java @@ -0,0 +1,120 @@ +package com.github.mikephil.charting.data; + +import android.graphics.DashPathEffect; + +import com.github.mikephil.charting.interfaces.datasets.ILineScatterCandleRadarDataSet; +import com.github.mikephil.charting.utils.Utils; + +import java.util.List; + +/** + * Created by Philipp Jahoda on 11/07/15. + */ +public abstract class LineScatterCandleRadarDataSet extends BarLineScatterCandleBubbleDataSet implements ILineScatterCandleRadarDataSet { + + protected boolean mDrawVerticalHighlightIndicator = true; + protected boolean mDrawHorizontalHighlightIndicator = true; + + /** the width of the highlight indicator lines */ + protected float mHighlightLineWidth = 0.5f; + + /** the path effect for dashed highlight-lines */ + protected DashPathEffect mHighlightDashPathEffect = null; + + + public LineScatterCandleRadarDataSet(List yVals, String label) { + super(yVals, label); + mHighlightLineWidth = Utils.convertDpToPixel(0.5f); + } + + /** + * Enables / disables the horizontal highlight-indicator. If disabled, the indicator is not drawn. + * @param enabled + */ + public void setDrawHorizontalHighlightIndicator(boolean enabled) { + this.mDrawHorizontalHighlightIndicator = enabled; + } + + /** + * Enables / disables the vertical highlight-indicator. If disabled, the indicator is not drawn. + * @param enabled + */ + public void setDrawVerticalHighlightIndicator(boolean enabled) { + this.mDrawVerticalHighlightIndicator = enabled; + } + + /** + * Enables / disables both vertical and horizontal highlight-indicators. + * @param enabled + */ + public void setDrawHighlightIndicators(boolean enabled) { + setDrawVerticalHighlightIndicator(enabled); + setDrawHorizontalHighlightIndicator(enabled); + } + + @Override + public boolean isVerticalHighlightIndicatorEnabled() { + return mDrawVerticalHighlightIndicator; + } + + @Override + public boolean isHorizontalHighlightIndicatorEnabled() { + return mDrawHorizontalHighlightIndicator; + } + + /** + * Sets the width of the highlight line in dp. + * @param width + */ + public void setHighlightLineWidth(float width) { + mHighlightLineWidth = Utils.convertDpToPixel(width); + } + + @Override + public float getHighlightLineWidth() { + return mHighlightLineWidth; + } + + /** + * Enables the highlight-line to be drawn in dashed mode, e.g. like this "- - - - - -" + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space inbetween the line-pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableDashedHighlightLine(float lineLength, float spaceLength, float phase) { + mHighlightDashPathEffect = new DashPathEffect(new float[] { + lineLength, spaceLength + }, phase); + } + + /** + * Disables the highlight-line to be drawn in dashed mode. + */ + public void disableDashedHighlightLine() { + mHighlightDashPathEffect = null; + } + + /** + * Returns true if the dashed-line effect is enabled for highlight lines, false if not. + * Default: disabled + * + * @return + */ + public boolean isDashedHighlightLineEnabled() { + return mHighlightDashPathEffect == null ? false : true; + } + + @Override + public DashPathEffect getDashPathEffectHighlight() { + return mHighlightDashPathEffect; + } + + protected void copy(LineScatterCandleRadarDataSet lineScatterCandleRadarDataSet) { + super.copy(lineScatterCandleRadarDataSet); + lineScatterCandleRadarDataSet.mDrawHorizontalHighlightIndicator = mDrawHorizontalHighlightIndicator; + lineScatterCandleRadarDataSet.mDrawVerticalHighlightIndicator = mDrawVerticalHighlightIndicator; + lineScatterCandleRadarDataSet.mHighlightLineWidth = mHighlightLineWidth; + lineScatterCandleRadarDataSet.mHighlightDashPathEffect = mHighlightDashPathEffect; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieData.java new file mode 100644 index 0000000..423ce19 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieData.java @@ -0,0 +1,100 @@ + +package com.github.mikephil.charting.data; + +import android.util.Log; + +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * A PieData object can only represent one DataSet. Unlike all other charts, the + * legend labels of the PieChart are created from the x-values array, and not + * from the DataSet labels. Each PieData object can only represent one + * PieDataSet (multiple PieDataSets inside a single PieChart are not possible). + * + * @author Philipp Jahoda + */ +public class PieData extends ChartData { + + public PieData() { + super(); + } + + public PieData(IPieDataSet dataSet) { + super(dataSet); + } + + /** + * Sets the PieDataSet this data object should represent. + * + * @param dataSet + */ + public void setDataSet(IPieDataSet dataSet) { + mDataSets.clear(); + mDataSets.add(dataSet); + notifyDataChanged(); + } + + /** + * Returns the DataSet this PieData object represents. A PieData object can + * only contain one DataSet. + * + * @return + */ + public IPieDataSet getDataSet() { + return mDataSets.get(0); + } + + @Override + public List getDataSets() { + List dataSets = super.getDataSets(); + + if (dataSets.size() < 1) { + Log.e("MPAndroidChart", + "Found multiple data sets while pie chart only allows one"); + } + + return dataSets; + } + + /** + * The PieData object can only have one DataSet. Use getDataSet() method instead. + * + * @param index + * @return + */ + @Override + public IPieDataSet getDataSetByIndex(int index) { + return index == 0 ? getDataSet() : null; + } + + @Override + public IPieDataSet getDataSetByLabel(String label, boolean ignorecase) { + return ignorecase ? label.equalsIgnoreCase(mDataSets.get(0).getLabel()) ? mDataSets.get(0) + : null : label.equals(mDataSets.get(0).getLabel()) ? mDataSets.get(0) : null; + } + + @Override + public Entry getEntryForHighlight(Highlight highlight) { + return getDataSet().getEntryForIndex((int) highlight.getX()); + } + + /** + * Returns the sum of all values in this PieData object. + * + * @return + */ + public float getYValueSum() { + + float sum = 0; + + for (int i = 0; i < getDataSet().getEntryCount(); i++) + sum += getDataSet().getEntryForIndex(i).getY(); + + + return sum; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieDataSet.java new file mode 100644 index 0000000..c83b245 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieDataSet.java @@ -0,0 +1,261 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; +import com.github.mikephil.charting.utils.Utils; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class PieDataSet extends DataSet implements IPieDataSet { + + /** + * the space in pixels between the chart-slices, default 0f + */ + private float mSliceSpace = 0f; + private boolean mAutomaticallyDisableSliceSpacing; + + /** + * indicates the selection distance of a pie slice + */ + private float mShift = 18f; + + private ValuePosition mXValuePosition = ValuePosition.INSIDE_SLICE; + private ValuePosition mYValuePosition = ValuePosition.INSIDE_SLICE; + private int mValueLineColor = 0xff000000; + private boolean mUseValueColorForLine = false; + private float mValueLineWidth = 1.0f; + private float mValueLinePart1OffsetPercentage = 75.f; + private float mValueLinePart1Length = 0.3f; + private float mValueLinePart2Length = 0.4f; + private boolean mValueLineVariableLength = true; + private Integer mHighlightColor = null; + + public PieDataSet(List yVals, String label) { + super(yVals, label); +// mShift = Utils.convertDpToPixel(12f); + } + + @Override + public DataSet copy() { + List entries = new ArrayList<>(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + PieDataSet copied = new PieDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(PieDataSet pieDataSet) { + super.copy(pieDataSet); + } + + @Override + protected void calcMinMax(PieEntry e) { + + if (e == null) + return; + + calcMinMaxY(e); + } + + /** + * Sets the space that is left out between the piechart-slices in dp. + * Default: 0 --> no space, maximum 20f + * + * @param spaceDp + */ + public void setSliceSpace(float spaceDp) { + + if (spaceDp > 20) + spaceDp = 20f; + if (spaceDp < 0) + spaceDp = 0f; + + mSliceSpace = Utils.convertDpToPixel(spaceDp); + } + + @Override + public float getSliceSpace() { + return mSliceSpace; + } + + /** + * When enabled, slice spacing will be 0.0 when the smallest value is going to be + * smaller than the slice spacing itself. + * + * @param autoDisable + */ + public void setAutomaticallyDisableSliceSpacing(boolean autoDisable) { + mAutomaticallyDisableSliceSpacing = autoDisable; + } + + /** + * When enabled, slice spacing will be 0.0 when the smallest value is going to be + * smaller than the slice spacing itself. + * + * @return + */ + @Override + public boolean isAutomaticallyDisableSliceSpacingEnabled() { + return mAutomaticallyDisableSliceSpacing; + } + + /** + * sets the distance the highlighted piechart-slice of this DataSet is + * "shifted" away from the center of the chart, default 12f + * + * @param shift + */ + public void setSelectionShift(float shift) { + mShift = Utils.convertDpToPixel(shift); + } + + @Override + public float getSelectionShift() { + return mShift; + } + + @Override + public ValuePosition getXValuePosition() { + return mXValuePosition; + } + + public void setXValuePosition(ValuePosition xValuePosition) { + this.mXValuePosition = xValuePosition; + } + + @Override + public ValuePosition getYValuePosition() { + return mYValuePosition; + } + + public void setYValuePosition(ValuePosition yValuePosition) { + this.mYValuePosition = yValuePosition; + } + + /** + * This method is deprecated. + * Use isUseValueColorForLineEnabled() instead. + */ + @Deprecated + public boolean isUsingSliceColorAsValueLineColor() { + return isUseValueColorForLineEnabled(); + } + + /** + * This method is deprecated. + * Use setUseValueColorForLine(...) instead. + * + * @param enabled + */ + @Deprecated + public void setUsingSliceColorAsValueLineColor(boolean enabled) { + setUseValueColorForLine(enabled); + } + + /** + * When valuePosition is OutsideSlice, indicates line color + */ + @Override + public int getValueLineColor() { + return mValueLineColor; + } + + public void setValueLineColor(int valueLineColor) { + this.mValueLineColor = valueLineColor; + } + + @Override + public boolean isUseValueColorForLineEnabled() + { + return mUseValueColorForLine; + } + + public void setUseValueColorForLine(boolean enabled) + { + mUseValueColorForLine = enabled; + } + + /** + * When valuePosition is OutsideSlice, indicates line width + */ + @Override + public float getValueLineWidth() { + return mValueLineWidth; + } + + public void setValueLineWidth(float valueLineWidth) { + this.mValueLineWidth = valueLineWidth; + } + + /** + * When valuePosition is OutsideSlice, indicates offset as percentage out of the slice size + */ + @Override + public float getValueLinePart1OffsetPercentage() { + return mValueLinePart1OffsetPercentage; + } + + public void setValueLinePart1OffsetPercentage(float valueLinePart1OffsetPercentage) { + this.mValueLinePart1OffsetPercentage = valueLinePart1OffsetPercentage; + } + + /** + * When valuePosition is OutsideSlice, indicates length of first half of the line + */ + @Override + public float getValueLinePart1Length() { + return mValueLinePart1Length; + } + + public void setValueLinePart1Length(float valueLinePart1Length) { + this.mValueLinePart1Length = valueLinePart1Length; + } + + /** + * When valuePosition is OutsideSlice, indicates length of second half of the line + */ + @Override + public float getValueLinePart2Length() { + return mValueLinePart2Length; + } + + public void setValueLinePart2Length(float valueLinePart2Length) { + this.mValueLinePart2Length = valueLinePart2Length; + } + + /** + * When valuePosition is OutsideSlice, this allows variable line length + */ + @Override + public boolean isValueLineVariableLength() { + return mValueLineVariableLength; + } + + public void setValueLineVariableLength(boolean valueLineVariableLength) { + this.mValueLineVariableLength = valueLineVariableLength; + } + + /** Gets the color for the highlighted sector */ + @Override + @Nullable + public Integer getHighlightColor() + { + return mHighlightColor; + } + + /** Sets the color for the highlighted sector (null for using entry color) */ + public void setHighlightColor(@Nullable Integer color) + { + this.mHighlightColor = color; + } + + + public enum ValuePosition { + INSIDE_SLICE, + OUTSIDE_SLICE + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieEntry.java new file mode 100644 index 0000000..65741ef --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieEntry.java @@ -0,0 +1,86 @@ +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; +import android.util.Log; + +/** + * @author Philipp Jahoda + */ +@SuppressLint("ParcelCreator") +public class PieEntry extends Entry { + + private String label; + + public PieEntry(float value) { + super(0f, value); + } + + public PieEntry(float value, Object data) { + super(0f, value, data); + } + + public PieEntry(float value, Drawable icon) { + super(0f, value, icon); + } + + public PieEntry(float value, Drawable icon, Object data) { + super(0f, value, icon, data); + } + + public PieEntry(float value, String label) { + super(0f, value); + this.label = label; + } + + public PieEntry(float value, String label, Object data) { + super(0f, value, data); + this.label = label; + } + + public PieEntry(float value, String label, Drawable icon) { + super(0f, value, icon); + this.label = label; + } + + public PieEntry(float value, String label, Drawable icon, Object data) { + super(0f, value, icon, data); + this.label = label; + } + + /** + * This is the same as getY(). Returns the value of the PieEntry. + * + * @return + */ + public float getValue() { + return getY(); + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + @Deprecated + @Override + public void setX(float x) { + super.setX(x); + Log.i("DEPRECATED", "Pie entries do not have x values"); + } + + @Deprecated + @Override + public float getX() { + Log.i("DEPRECATED", "Pie entries do not have x values"); + return super.getX(); + } + + public PieEntry copy() { + PieEntry e = new PieEntry(getY(), label, getData()); + return e; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarData.java new file mode 100644 index 0000000..0c1dbe5 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarData.java @@ -0,0 +1,58 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IRadarDataSet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Data container for the RadarChart. + * + * @author Philipp Jahoda + */ +public class RadarData extends ChartData { + + private List mLabels; + + public RadarData() { + super(); + } + + public RadarData(List dataSets) { + super(dataSets); + } + + public RadarData(IRadarDataSet... dataSets) { + super(dataSets); + } + + /** + * Sets the labels that should be drawn around the RadarChart at the end of each web line. + * + * @param labels + */ + public void setLabels(List labels) { + this.mLabels = labels; + } + + /** + * Sets the labels that should be drawn around the RadarChart at the end of each web line. + * + * @param labels + */ + public void setLabels(String... labels) { + this.mLabels = Arrays.asList(labels); + } + + public List getLabels() { + return mLabels; + } + + @Override + public Entry getEntryForHighlight(Highlight highlight) { + return getDataSetByIndex(highlight.getDataSetIndex()).getEntryForIndex((int) highlight.getX()); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarDataSet.java new file mode 100644 index 0000000..8a9740b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarDataSet.java @@ -0,0 +1,122 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Color; + +import com.github.mikephil.charting.interfaces.datasets.IRadarDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; + +import java.util.ArrayList; +import java.util.List; + +public class RadarDataSet extends LineRadarDataSet implements IRadarDataSet { + + /// flag indicating whether highlight circle should be drawn or not + protected boolean mDrawHighlightCircleEnabled = false; + + protected int mHighlightCircleFillColor = Color.WHITE; + + /// The stroke color for highlight circle. + /// If Utils.COLOR_NONE, the color of the dataset is taken. + protected int mHighlightCircleStrokeColor = ColorTemplate.COLOR_NONE; + + protected int mHighlightCircleStrokeAlpha = (int) (0.3 * 255); + protected float mHighlightCircleInnerRadius = 3.0f; + protected float mHighlightCircleOuterRadius = 4.0f; + protected float mHighlightCircleStrokeWidth = 2.0f; + + public RadarDataSet(List yVals, String label) { + super(yVals, label); + } + + /// Returns true if highlight circle should be drawn, false if not + @Override + public boolean isDrawHighlightCircleEnabled() { + return mDrawHighlightCircleEnabled; + } + + /// Sets whether highlight circle should be drawn or not + @Override + public void setDrawHighlightCircleEnabled(boolean enabled) { + mDrawHighlightCircleEnabled = enabled; + } + + @Override + public int getHighlightCircleFillColor() { + return mHighlightCircleFillColor; + } + + public void setHighlightCircleFillColor(int color) { + mHighlightCircleFillColor = color; + } + + /// Returns the stroke color for highlight circle. + /// If Utils.COLOR_NONE, the color of the dataset is taken. + @Override + public int getHighlightCircleStrokeColor() { + return mHighlightCircleStrokeColor; + } + + /// Sets the stroke color for highlight circle. + /// Set to Utils.COLOR_NONE in order to use the color of the dataset; + public void setHighlightCircleStrokeColor(int color) { + mHighlightCircleStrokeColor = color; + } + + @Override + public int getHighlightCircleStrokeAlpha() { + return mHighlightCircleStrokeAlpha; + } + + public void setHighlightCircleStrokeAlpha(int alpha) { + mHighlightCircleStrokeAlpha = alpha; + } + + @Override + public float getHighlightCircleInnerRadius() { + return mHighlightCircleInnerRadius; + } + + public void setHighlightCircleInnerRadius(float radius) { + mHighlightCircleInnerRadius = radius; + } + + @Override + public float getHighlightCircleOuterRadius() { + return mHighlightCircleOuterRadius; + } + + public void setHighlightCircleOuterRadius(float radius) { + mHighlightCircleOuterRadius = radius; + } + + @Override + public float getHighlightCircleStrokeWidth() { + return mHighlightCircleStrokeWidth; + } + + public void setHighlightCircleStrokeWidth(float strokeWidth) { + mHighlightCircleStrokeWidth = strokeWidth; + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + RadarDataSet copied = new RadarDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(RadarDataSet radarDataSet) { + super.copy(radarDataSet); + radarDataSet.mDrawHighlightCircleEnabled = mDrawHighlightCircleEnabled; + radarDataSet.mHighlightCircleFillColor = mHighlightCircleFillColor; + radarDataSet.mHighlightCircleInnerRadius = mHighlightCircleInnerRadius; + radarDataSet.mHighlightCircleStrokeAlpha = mHighlightCircleStrokeAlpha; + radarDataSet.mHighlightCircleStrokeColor = mHighlightCircleStrokeColor; + radarDataSet.mHighlightCircleStrokeWidth = mHighlightCircleStrokeWidth; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarEntry.java new file mode 100644 index 0000000..02fdce7 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarEntry.java @@ -0,0 +1,44 @@ +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; + +/** + * Created by philipp on 13/06/16. + */ +@SuppressLint("ParcelCreator") +public class RadarEntry extends Entry { + + public RadarEntry(float value) { + super(0f, value); + } + + public RadarEntry(float value, Object data) { + super(0f, value, data); + } + + /** + * This is the same as getY(). Returns the value of the RadarEntry. + * + * @return + */ + public float getValue() { + return getY(); + } + + public RadarEntry copy() { + RadarEntry e = new RadarEntry(getY(), getData()); + return e; + } + + @Deprecated + @Override + public void setX(float x) { + super.setX(x); + } + + @Deprecated + @Override + public float getX() { + return super.getX(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterData.java new file mode 100644 index 0000000..ba14236 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterData.java @@ -0,0 +1,40 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; + +import java.util.List; + +public class ScatterData extends BarLineScatterCandleBubbleData { + + public ScatterData() { + super(); + } + + public ScatterData(List dataSets) { + super(dataSets); + } + + public ScatterData(IScatterDataSet... dataSets) { + super(dataSets); + } + + /** + * Returns the maximum shape-size across all DataSets. + * + * @return + */ + public float getGreatestShapeSize() { + + float max = 0f; + + for (IScatterDataSet set : mDataSets) { + float size = set.getScatterShapeSize(); + + if (size > max) + max = size; + } + + return max; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterDataSet.java new file mode 100644 index 0000000..85ed808 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterDataSet.java @@ -0,0 +1,157 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.charts.ScatterChart; +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.renderer.scatter.ChevronDownShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.ChevronUpShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.CircleShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.CrossShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.IShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.SquareShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.TriangleShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.XShapeRenderer; +import com.github.mikephil.charting.utils.ColorTemplate; + +import java.util.ArrayList; +import java.util.List; + +public class ScatterDataSet extends LineScatterCandleRadarDataSet implements IScatterDataSet { + + /** + * the size the scattershape will have, in density pixels + */ + private float mShapeSize = 15f; + + /** + * Renderer responsible for rendering this DataSet, default: square + */ + protected IShapeRenderer mShapeRenderer = new SquareShapeRenderer(); + + /** + * The radius of the hole in the shape (applies to Square, Circle and Triangle) + * - default: 0.0 + */ + private float mScatterShapeHoleRadius = 0f; + + /** + * Color for the hole in the shape. + * Setting to `ColorTemplate.COLOR_NONE` will behave as transparent. + * - default: ColorTemplate.COLOR_NONE + */ + private int mScatterShapeHoleColor = ColorTemplate.COLOR_NONE; + + public ScatterDataSet(List yVals, String label) { + super(yVals, label); + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + ScatterDataSet copied = new ScatterDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(ScatterDataSet scatterDataSet) { + super.copy(scatterDataSet); + scatterDataSet.mShapeSize = mShapeSize; + scatterDataSet.mShapeRenderer = mShapeRenderer; + scatterDataSet.mScatterShapeHoleRadius = mScatterShapeHoleRadius; + scatterDataSet.mScatterShapeHoleColor = mScatterShapeHoleColor; + } + + /** + * Sets the size in density pixels the drawn scattershape will have. This + * only applies for non custom shapes. + * + * @param size + */ + public void setScatterShapeSize(float size) { + mShapeSize = size; + } + + @Override + public float getScatterShapeSize() { + return mShapeSize; + } + + /** + * Sets the ScatterShape this DataSet should be drawn with. This will search for an available IShapeRenderer and set this + * renderer for the DataSet. + * + * @param shape + */ + public void setScatterShape(ScatterChart.ScatterShape shape) { + mShapeRenderer = getRendererForShape(shape); + } + + /** + * Sets a new IShapeRenderer responsible for drawing this DataSet. + * This can also be used to set a custom IShapeRenderer aside from the default ones. + * + * @param shapeRenderer + */ + public void setShapeRenderer(IShapeRenderer shapeRenderer) { + mShapeRenderer = shapeRenderer; + } + + @Override + public IShapeRenderer getShapeRenderer() { + return mShapeRenderer; + } + + /** + * Sets the radius of the hole in the shape (applies to Square, Circle and Triangle) + * Set this to <= 0 to remove holes. + * + * @param holeRadius + */ + public void setScatterShapeHoleRadius(float holeRadius) { + mScatterShapeHoleRadius = holeRadius; + } + + @Override + public float getScatterShapeHoleRadius() { + return mScatterShapeHoleRadius; + } + + /** + * Sets the color for the hole in the shape. + * + * @param holeColor + */ + public void setScatterShapeHoleColor(int holeColor) { + mScatterShapeHoleColor = holeColor; + } + + @Override + public int getScatterShapeHoleColor() { + return mScatterShapeHoleColor; + } + + public static IShapeRenderer getRendererForShape(ScatterChart.ScatterShape shape) { + + switch (shape) { + case SQUARE: + return new SquareShapeRenderer(); + case CIRCLE: + return new CircleShapeRenderer(); + case TRIANGLE: + return new TriangleShapeRenderer(); + case CROSS: + return new CrossShapeRenderer(); + case X: + return new XShapeRenderer(); + case CHEVRON_UP: + return new ChevronUpShapeRenderer(); + case CHEVRON_DOWN: + return new ChevronDownShapeRenderer(); + } + + return null; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/Approximator.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/Approximator.java new file mode 100644 index 0000000..542188e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/Approximator.java @@ -0,0 +1,102 @@ + +package com.github.mikephil.charting.data.filter; + +import android.annotation.TargetApi; +import android.os.Build; + +import java.util.Arrays; + +/** + * Implemented according to Wiki-Pseudocode {@link} + * http://en.wikipedia.org/wiki/Ramer�Douglas�Peucker_algorithm + * + * @author Philipp Baldauf & Phliipp Jahoda + */ +public class Approximator { + + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + public float[] reduceWithDouglasPeucker(float[] points, float tolerance) { + + int greatestIndex = 0; + float greatestDistance = 0f; + + Line line = new Line(points[0], points[1], points[points.length - 2], points[points.length - 1]); + + for (int i = 2; i < points.length - 2; i += 2) { + + float distance = line.distance(points[i], points[i + 1]); + + if (distance > greatestDistance) { + greatestDistance = distance; + greatestIndex = i; + } + } + + if (greatestDistance > tolerance) { + + float[] reduced1 = reduceWithDouglasPeucker(Arrays.copyOfRange(points, 0, greatestIndex + 2), tolerance); + float[] reduced2 = reduceWithDouglasPeucker(Arrays.copyOfRange(points, greatestIndex, points.length), + tolerance); + + float[] result1 = reduced1; + float[] result2 = Arrays.copyOfRange(reduced2, 2, reduced2.length); + + return concat(result1, result2); + } else { + return line.getPoints(); + } + } + + /** + * Combine arrays. + * + * @param arrays + * @return + */ + float[] concat(float[]... arrays) { + int length = 0; + for (float[] array : arrays) { + length += array.length; + } + float[] result = new float[length]; + int pos = 0; + for (float[] array : arrays) { + for (float element : array) { + result[pos] = element; + pos++; + } + } + return result; + } + + private class Line { + + private float[] points; + + private float sxey; + private float exsy; + + private float dx; + private float dy; + + private float length; + + public Line(float x1, float y1, float x2, float y2) { + dx = x1 - x2; + dy = y1 - y2; + sxey = x1 * y2; + exsy = x2 * y1; + length = (float) Math.sqrt(dx * dx + dy * dy); + + points = new float[]{x1, y1, x2, y2}; + } + + public float distance(float x, float y) { + return Math.abs(dy * x - dx * y + sxey - exsy) / length; + } + + public float[] getPoints() { + return points; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/ApproximatorN.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/ApproximatorN.java new file mode 100644 index 0000000..9351341 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/ApproximatorN.java @@ -0,0 +1,146 @@ + +package com.github.mikephil.charting.data.filter; + +import java.util.ArrayList; + +/** + * Implemented according to modified Douglas Peucker {@link} + * http://psimpl.sourceforge.net/douglas-peucker.html + */ +public class ApproximatorN +{ + public float[] reduceWithDouglasPeucker(float[] points, float resultCount) { + + int pointCount = points.length / 2; + + // if a shape has 2 or less points it cannot be reduced + if (resultCount <= 2 || resultCount >= pointCount) + return points; + + boolean[] keep = new boolean[pointCount]; + + // first and last always stay + keep[0] = true; + keep[pointCount - 1] = true; + + int currentStoredPoints = 2; + + ArrayList queue = new ArrayList<>(); + Line line = new Line(0, pointCount - 1, points); + queue.add(line); + + do { + line = queue.remove(queue.size() - 1); + + // store the key + keep[line.index] = true; + + // check point count tolerance + currentStoredPoints += 1; + + if (currentStoredPoints == resultCount) + break; + + // split the polyline at the key and recurse + Line left = new Line(line.start, line.index, points); + if (left.index > 0) { + int insertionIndex = insertionIndex(left, queue); + queue.add(insertionIndex, left); + } + + Line right = new Line(line.index, line.end, points); + if (right.index > 0) { + int insertionIndex = insertionIndex(right, queue); + queue.add(insertionIndex, right); + } + } while (queue.isEmpty()); + + float[] reducedEntries = new float[currentStoredPoints * 2]; + + for (int i = 0, i2 = 0, r2 = 0; i < currentStoredPoints; i++, r2 += 2) { + if (keep[i]) { + reducedEntries[i2++] = points[r2]; + reducedEntries[i2++] = points[r2 + 1]; + } + } + + return reducedEntries; + } + + private static float distanceToLine( + float ptX, float ptY, float[] + fromLinePoint1, float[] fromLinePoint2) { + float dx = fromLinePoint2[0] - fromLinePoint1[0]; + float dy = fromLinePoint2[1] - fromLinePoint1[1]; + + float dividend = Math.abs( + dy * ptX - + dx * ptY - + fromLinePoint1[0] * fromLinePoint2[1] + + fromLinePoint2[0] * fromLinePoint1[1]); + double divisor = Math.sqrt(dx * dx + dy * dy); + + return (float)(dividend / divisor); + } + + private static class Line { + int start; + int end; + + float distance = 0; + int index = 0; + + Line(int start, int end, float[] points) { + this.start = start; + this.end = end; + + float[] startPoint = new float[]{points[start * 2], points[start * 2 + 1]}; + float[] endPoint = new float[]{points[end * 2], points[end * 2 + 1]}; + + if (end <= start + 1) return; + + for (int i = start + 1, i2 = i * 2; i < end; i++, i2 += 2) { + float distance = distanceToLine( + points[i2], points[i2 + 1], + startPoint, endPoint); + + if (distance > this.distance) { + this.index = i; + this.distance = distance; + } + } + } + + boolean equals(final Line rhs) { + return (start == rhs.start) && (end == rhs.end) && index == rhs.index; + } + + boolean lessThan(final Line rhs) { + return distance < rhs.distance; + } + } + + private static int insertionIndex(Line line, ArrayList queue) { + int min = 0; + int max = queue.size(); + + while (!queue.isEmpty()) { + int midIndex = min + (max - min) / 2; + Line midLine = queue.get(midIndex); + + if (midLine.equals(line)) { + return midIndex; + } + else if (line.lessThan(midLine)) { + // perform search in left half + max = midIndex; + } + else { + // perform search in right half + min = midIndex + 1; + } + } + + return min; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/exception/DrawingDataSetNotCreatedException.java b/MPChartLib/src/main/java/com/github/mikephil/charting/exception/DrawingDataSetNotCreatedException.java new file mode 100644 index 0000000..d6c1237 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/exception/DrawingDataSetNotCreatedException.java @@ -0,0 +1,14 @@ +package com.github.mikephil.charting.exception; + +public class DrawingDataSetNotCreatedException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public DrawingDataSetNotCreatedException() { + super("Have to create a new drawing set first. Call ChartData's createNewDrawingDataSet() method"); + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/ColorFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/ColorFormatter.java new file mode 100644 index 0000000..2db66fd --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/ColorFormatter.java @@ -0,0 +1,24 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; + +/** + * Interface that can be used to return a customized color instead of setting + * colors via the setColor(...) method of the DataSet. + * + * @author Philipp Jahoda + */ +public interface ColorFormatter { + + /** + * Returns the color to be used for the given Entry at the given index (in the entries array) + * + * @param index index in the entries array + * @param e the entry to color + * @param set the DataSet the entry belongs to + * @return + */ + int getColor(int index, Entry e, IDataSet set); +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultAxisValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultAxisValueFormatter.java new file mode 100644 index 0000000..552c150 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultAxisValueFormatter.java @@ -0,0 +1,56 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; + +import java.text.DecimalFormat; + +/** + * Created by philipp on 02/06/16. + */ +public class DefaultAxisValueFormatter implements IAxisValueFormatter +{ + + /** + * decimalformat for formatting + */ + protected DecimalFormat mFormat; + + /** + * the number of decimal digits this formatter uses + */ + protected int digits = 0; + + /** + * Constructor that specifies to how many digits the value should be + * formatted. + * + * @param digits + */ + public DefaultAxisValueFormatter(int digits) { + this.digits = digits; + + StringBuffer b = new StringBuffer(); + for (int i = 0; i < digits; i++) { + if (i == 0) + b.append("."); + b.append("0"); + } + + mFormat = new DecimalFormat("###,###,###,##0" + b.toString()); + } + + @Override + public String getFormattedValue(float value, AxisBase axis) { + // avoid memory allocations here (for performance) + return mFormat.format(value); + } + + /** + * Returns the number of decimal digits this formatter uses or -1, if unspecified. + * + * @return + */ + public int getDecimalDigits() { + return digits; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultFillFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultFillFormatter.java new file mode 100644 index 0000000..851faeb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultFillFormatter.java @@ -0,0 +1,45 @@ +package com.github.mikephil.charting.formatter; + + +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +/** + * Default formatter that calculates the position of the filled line. + * + * @author Philipp Jahoda + */ +public class DefaultFillFormatter implements IFillFormatter +{ + + @Override + public float getFillLinePosition(ILineDataSet dataSet, LineDataProvider dataProvider) { + + float fillMin = 0f; + float chartMaxY = dataProvider.getYChartMax(); + float chartMinY = dataProvider.getYChartMin(); + + LineData data = dataProvider.getLineData(); + + if (dataSet.getYMax() > 0 && dataSet.getYMin() < 0) { + fillMin = 0f; + } else { + + float max, min; + + if (data.getYMax() > 0) + max = 0f; + else + max = chartMaxY; + if (data.getYMin() < 0) + min = 0f; + else + min = chartMinY; + + fillMin = dataSet.getYMin() >= 0 ? min : max; + } + + return fillMin; + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultValueFormatter.java new file mode 100644 index 0000000..e2fea4b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultValueFormatter.java @@ -0,0 +1,71 @@ + +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; + +/** + * Default formatter used for formatting values inside the chart. Uses a DecimalFormat with + * pre-calculated number of digits (depending on max and min value). + * + * @author Philipp Jahoda + */ +public class DefaultValueFormatter implements IValueFormatter +{ + + /** + * DecimalFormat for formatting + */ + protected DecimalFormat mFormat; + + protected int mDecimalDigits; + + /** + * Constructor that specifies to how many digits the value should be + * formatted. + * + * @param digits + */ + public DefaultValueFormatter(int digits) { + setup(digits); + } + + /** + * Sets up the formatter with a given number of decimal digits. + * + * @param digits + */ + public void setup(int digits) { + + this.mDecimalDigits = digits; + + StringBuffer b = new StringBuffer(); + for (int i = 0; i < digits; i++) { + if (i == 0) + b.append("."); + b.append("0"); + } + + mFormat = new DecimalFormat("###,###,###,##0" + b.toString()); + } + + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + + // put more logic here ... + // avoid memory allocations here (for performance reasons) + + return mFormat.format(value); + } + + /** + * Returns the number of decimal digits this formatter uses. + * + * @return + */ + public int getDecimalDigits() { + return mDecimalDigits; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IAxisValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IAxisValueFormatter.java new file mode 100644 index 0000000..51939b5 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IAxisValueFormatter.java @@ -0,0 +1,23 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; + +/** + * Created by Philipp Jahoda on 20/09/15. + * Custom formatter interface that allows formatting of + * axis labels before they are being drawn. + */ +public interface IAxisValueFormatter +{ + + /** + * Called when a value from an axis is to be formatted + * before being drawn. For performance reasons, avoid excessive calculations + * and memory allocations inside this method. + * + * @param value the value to be formatted + * @param axis the axis the value belongs to + * @return + */ + String getFormattedValue(float value, AxisBase axis); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IFillFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IFillFormatter.java new file mode 100644 index 0000000..e096cc6 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IFillFormatter.java @@ -0,0 +1,24 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; + +/** + * Interface for providing a custom logic to where the filling line of a LineDataSet + * should end. This of course only works if setFillEnabled(...) is set to true. + * + * @author Philipp Jahoda + */ +public interface IFillFormatter +{ + + /** + * Returns the vertical (y-axis) position where the filled-line of the + * LineDataSet should end. + * + * @param dataSet the ILineDataSet that is currently drawn + * @param dataProvider + * @return + */ + float getFillLinePosition(ILineDataSet dataSet, LineDataProvider dataProvider); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IValueFormatter.java new file mode 100644 index 0000000..75d2363 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IValueFormatter.java @@ -0,0 +1,29 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Interface that allows custom formatting of all values inside the chart before they are + * being drawn to the screen. Simply create your own formatting class and let + * it implement IValueFormatter. Then override the getFormattedValue(...) method + * and return whatever you want. + * + * @author Philipp Jahoda + */ +public interface IValueFormatter +{ + + /** + * Called when a value (from labels inside the chart) is formatted + * before being drawn. For performance reasons, avoid excessive calculations + * and memory allocations inside this method. + * + * @param value the value to be formatted + * @param entry the entry the value belongs to - in e.g. BarChart, this is of class BarEntry + * @param dataSetIndex the index of the DataSet the entry in focus belongs to + * @param viewPortHandler provides information about the current chart state (scale, translation, ...) + * @return the formatted label ready for being drawn + */ + String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IndexAxisValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IndexAxisValueFormatter.java new file mode 100644 index 0000000..07349a6 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IndexAxisValueFormatter.java @@ -0,0 +1,69 @@ + +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.Collection; + +/** + * This formatter is used for passing an array of x-axis labels, on whole x steps. + */ +public class IndexAxisValueFormatter implements IAxisValueFormatter +{ + private String[] mValues = new String[] {}; + private int mValueCount = 0; + + /** + * An empty constructor. + * Use `setValues` to set the axis labels. + */ + public IndexAxisValueFormatter() { + } + + /** + * Constructor that specifies axis labels. + * + * @param values The values string array + */ + public IndexAxisValueFormatter(String[] values) { + if (values != null) + setValues(values); + } + + /** + * Constructor that specifies axis labels. + * + * @param values The values string array + */ + public IndexAxisValueFormatter(Collection values) { + if (values != null) + setValues(values.toArray(new String[values.size()])); + } + + public String getFormattedValue(float value, AxisBase axis) { + int index = Math.round(value); + + if (index < 0 || index >= mValueCount || index != (int)value) + return ""; + + return mValues[index]; + } + + public String[] getValues() + { + return mValues; + } + + public void setValues(String[] values) + { + if (values == null) + values = new String[] {}; + + this.mValues = values; + this.mValueCount = values.length; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/LargeValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/LargeValueFormatter.java new file mode 100644 index 0000000..211401a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/LargeValueFormatter.java @@ -0,0 +1,103 @@ + +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; + +/** + * Predefined value-formatter that formats large numbers in a pretty way. + * Outputs: 856 = 856; 1000 = 1k; 5821 = 5.8k; 10500 = 10k; 101800 = 102k; + * 2000000 = 2m; 7800000 = 7.8m; 92150000 = 92m; 123200000 = 123m; 9999999 = + * 10m; 1000000000 = 1b; Special thanks to Roman Gromov + * (https://github.com/romangromov) for this piece of code. + * + * @author Philipp Jahoda + * @author Oleksandr Tyshkovets + */ +public class LargeValueFormatter implements IValueFormatter, IAxisValueFormatter +{ + + private String[] mSuffix = new String[]{ + "", "k", "m", "b", "t" + }; + private int mMaxLength = 5; + private DecimalFormat mFormat; + private String mText = ""; + + public LargeValueFormatter() { + mFormat = new DecimalFormat("###E00"); + } + + /** + * Creates a formatter that appends a specified text to the result string + * + * @param appendix a text that will be appended + */ + public LargeValueFormatter(String appendix) { + this(); + mText = appendix; + } + + // IValueFormatter + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + return makePretty(value) + mText; + } + + // IAxisValueFormatter + @Override + public String getFormattedValue(float value, AxisBase axis) { + return makePretty(value) + mText; + } + + /** + * Set an appendix text to be added at the end of the formatted value. + * + * @param appendix + */ + public void setAppendix(String appendix) { + this.mText = appendix; + } + + /** + * Set custom suffix to be appended after the values. + * Default suffix: ["", "k", "m", "b", "t"] + * + * @param suffix new suffix + */ + public void setSuffix(String[] suffix) { + this.mSuffix = suffix; + } + + public void setMaxLength(int maxLength) { + this.mMaxLength = maxLength; + } + + /** + * Formats each number properly. Special thanks to Roman Gromov + * (https://github.com/romangromov) for this piece of code. + */ + private String makePretty(double number) { + + String r = mFormat.format(number); + + int numericValue1 = Character.getNumericValue(r.charAt(r.length() - 1)); + int numericValue2 = Character.getNumericValue(r.charAt(r.length() - 2)); + int combined = Integer.valueOf(numericValue2 + "" + numericValue1); + + r = r.replaceAll("E[0-9][0-9]", mSuffix[combined / 3]); + + while (r.length() > mMaxLength || r.matches("[0-9]+\\.[a-z]")) { + r = r.substring(0, r.length() - 2) + r.substring(r.length() - 1); + } + + return r; + } + + public int getDecimalDigits() { + return 0; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/PercentFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/PercentFormatter.java new file mode 100644 index 0000000..c00495f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/PercentFormatter.java @@ -0,0 +1,50 @@ + +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; + +/** + * This IValueFormatter is just for convenience and simply puts a "%" sign after + * each value. (Recommeded for PieChart) + * + * @author Philipp Jahoda + */ +public class PercentFormatter implements IValueFormatter, IAxisValueFormatter +{ + + protected DecimalFormat mFormat; + + public PercentFormatter() { +// mFormat = new DecimalFormat("###,###,##0.0"); + mFormat = new DecimalFormat("###,###,##0"); + } + + /** + * Allow a custom decimalformat + * + * @param format + */ + public PercentFormatter(DecimalFormat format) { + this.mFormat = format; + } + + // IValueFormatter + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + return mFormat.format(value) + "%"; + } + + // IAxisValueFormatter + @Override + public String getFormattedValue(float value, AxisBase axis) { + return mFormat.format(value) + "%"; + } + + public int getDecimalDigits() { + return 1; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/StackedValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/StackedValueFormatter.java new file mode 100644 index 0000000..0e83516 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/StackedValueFormatter.java @@ -0,0 +1,75 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; + +/** + * Created by Philipp Jahoda on 28/01/16. + *

+ * A formatter specifically for stacked BarChart that allows to specify whether the all stack values + * or just the top value should be drawn. + */ +public class StackedValueFormatter implements IValueFormatter +{ + + /** + * if true, all stack values of the stacked bar entry are drawn, else only top + */ + private boolean mDrawWholeStack; + + /** + * a string that should be appended behind the value + */ + private String mAppendix; + + private DecimalFormat mFormat; + + /** + * Constructor. + * + * @param drawWholeStack if true, all stack values of the stacked bar entry are drawn, else only top + * @param appendix a string that should be appended behind the value + * @param decimals the number of decimal digits to use + */ + public StackedValueFormatter(boolean drawWholeStack, String appendix, int decimals) { + this.mDrawWholeStack = drawWholeStack; + this.mAppendix = appendix; + + StringBuffer b = new StringBuffer(); + for (int i = 0; i < decimals; i++) { + if (i == 0) + b.append("."); + b.append("0"); + } + + this.mFormat = new DecimalFormat("###,###,###,##0" + b.toString()); + } + + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + + if (!mDrawWholeStack && entry instanceof BarEntry) { + + BarEntry barEntry = (BarEntry) entry; + float[] vals = barEntry.getYVals(); + + if (vals != null) { + + // find out if we are on top of the stack + if (vals[vals.length - 1] == value) { + + // return the "sum" across all stack values + return mFormat.format(barEntry.getY()) + mAppendix; + } else { + return ""; // return empty + } + } + } + + // return the "proposed" value + return mFormat.format(value) + mAppendix; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java new file mode 100644 index 0000000..af83a45 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java @@ -0,0 +1,163 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.MPPointD; + +/** + * Created by Philipp Jahoda on 22/07/15. + */ +public class BarHighlighter extends ChartHighlighter { + + public BarHighlighter(BarDataProvider chart) { + super(chart); + } + + @Override + public Highlight getHighlight(float x, float y) { + Highlight high = super.getHighlight(x, y); + + if(high == null) { + return null; + } + + MPPointD pos = getValsForTouch(x, y); + + BarData barData = mChart.getBarData(); + + IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); + if (set.isStacked()) { + + return getStackedHighlight(high, + set, + (float) pos.x, + (float) pos.y); + } + + MPPointD.recycleInstance(pos); + + return high; + } + + /** + * This method creates the Highlight object that also indicates which value of a stacked BarEntry has been + * selected. + * + * @param high the Highlight to work with looking for stacked values + * @param set + * @param xVal + * @param yVal + * @return + */ + public Highlight getStackedHighlight(Highlight high, IBarDataSet set, float xVal, float yVal) { + + BarEntry entry = set.getEntryForXValue(xVal, yVal); + + if (entry == null) + return null; + + // not stacked + if (entry.getYVals() == null) { + return high; + } else { + Range[] ranges = entry.getRanges(); + + if (ranges.length > 0) { + int stackIndex = getClosestStackIndex(ranges, yVal); + + MPPointD pixels = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(high.getX(), ranges[stackIndex].to); + + Highlight stackedHigh = new Highlight( + entry.getX(), + entry.getY(), + (float) pixels.x, + (float) pixels.y, + high.getDataSetIndex(), + stackIndex, + high.getAxis() + ); + + MPPointD.recycleInstance(pixels); + + return stackedHigh; + } + } + + return null; + } + + /** + * Returns the index of the closest value inside the values array / ranges (stacked barchart) to the value + * given as + * a parameter. + * + * @param ranges + * @param value + * @return + */ + protected int getClosestStackIndex(Range[] ranges, float value) { + + if (ranges == null || ranges.length == 0) + return 0; + + int stackIndex = 0; + + for (Range range : ranges) { + if (range.contains(value)) + return stackIndex; + else + stackIndex++; + } + + int length = Math.max(ranges.length - 1, 0); + + return (value > ranges[length].to) ? length : 0; + } + +// /** +// * Splits up the stack-values of the given bar-entry into Range objects. +// * +// * @param entry +// * @return +// */ +// protected Range[] getRanges(BarEntry entry) { +// +// float[] values = entry.getYVals(); +// +// if (values == null || values.length == 0) +// return new Range[0]; +// +// Range[] ranges = new Range[values.length]; +// +// float negRemain = -entry.getNegativeSum(); +// float posRemain = 0f; +// +// for (int i = 0; i < ranges.length; i++) { +// +// float value = values[i]; +// +// if (value < 0) { +// ranges[i] = new Range(negRemain, negRemain + Math.abs(value)); +// negRemain += Math.abs(value); +// } else { +// ranges[i] = new Range(posRemain, posRemain + value); +// posRemain += value; +// } +// } +// +// return ranges; +// } + + @Override + protected float getDistance(float x1, float y1, float x2, float y2) { + return Math.abs(x1 - x2); + } + + @Override + protected BarLineScatterCandleBubbleData getData() { + return mChart.getBarData(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java new file mode 100644 index 0000000..f889bf1 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java @@ -0,0 +1,246 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointD; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Philipp Jahoda on 21/07/15. + */ +public class ChartHighlighter implements IHighlighter +{ + + /** + * instance of the data-provider + */ + protected T mChart; + + /** + * buffer for storing previously highlighted values + */ + protected List mHighlightBuffer = new ArrayList(); + + public ChartHighlighter(T chart) { + this.mChart = chart; + } + + @Override + public Highlight getHighlight(float x, float y) { + + MPPointD pos = getValsForTouch(x, y); + float xVal = (float) pos.x; + MPPointD.recycleInstance(pos); + + Highlight high = getHighlightForX(xVal, x, y); + return high; + } + + /** + * Returns a recyclable MPPointD instance. + * Returns the corresponding xPos for a given touch-position in pixels. + * + * @param x + * @param y + * @return + */ + protected MPPointD getValsForTouch(float x, float y) { + + // take any transformer to determine the x-axis value + MPPointD pos = mChart.getTransformer(YAxis.AxisDependency.LEFT).getValuesByTouchPoint(x, y); + return pos; + } + + /** + * Returns the corresponding Highlight for a given xVal and x- and y-touch position in pixels. + * + * @param xVal + * @param x + * @param y + * @return + */ + protected Highlight getHighlightForX(float xVal, float x, float y) { + + List closestValues = getHighlightsAtXValue(xVal, x, y); + + if(closestValues.isEmpty()) { + return null; + } + + float leftAxisMinDist = getMinimumDistance(closestValues, y, YAxis.AxisDependency.LEFT); + float rightAxisMinDist = getMinimumDistance(closestValues, y, YAxis.AxisDependency.RIGHT); + + YAxis.AxisDependency axis = leftAxisMinDist < rightAxisMinDist ? YAxis.AxisDependency.LEFT : YAxis.AxisDependency.RIGHT; + + Highlight detail = getClosestHighlightByPixel(closestValues, x, y, axis, mChart.getMaxHighlightDistance()); + + return detail; + } + + /** + * Returns the minimum distance from a touch value (in pixels) to the + * closest value (in pixels) that is displayed in the chart. + * + * @param closestValues + * @param pos + * @param axis + * @return + */ + protected float getMinimumDistance(List closestValues, float pos, YAxis.AxisDependency axis) { + + float distance = Float.MAX_VALUE; + + for (int i = 0; i < closestValues.size(); i++) { + + Highlight high = closestValues.get(i); + + if (high.getAxis() == axis) { + + float tempDistance = Math.abs(getHighlightPos(high) - pos); + if (tempDistance < distance) { + distance = tempDistance; + } + } + } + + return distance; + } + + protected float getHighlightPos(Highlight h) { + return h.getYPx(); + } + + /** + * Returns a list of Highlight objects representing the entries closest to the given xVal. + * The returned list contains two objects per DataSet (closest rounding up, closest rounding down). + * + * @param xVal the transformed x-value of the x-touch position + * @param x touch position + * @param y touch position + * @return + */ + protected List getHighlightsAtXValue(float xVal, float x, float y) { + + mHighlightBuffer.clear(); + + BarLineScatterCandleBubbleData data = getData(); + + if (data == null) + return mHighlightBuffer; + + for (int i = 0, dataSetCount = data.getDataSetCount(); i < dataSetCount; i++) { + + IDataSet dataSet = data.getDataSetByIndex(i); + + // don't include DataSets that cannot be highlighted + if (!dataSet.isHighlightEnabled()) + continue; + + mHighlightBuffer.addAll(buildHighlights(dataSet, i, xVal, DataSet.Rounding.CLOSEST)); + } + + return mHighlightBuffer; + } + + /** + * An array of `Highlight` objects corresponding to the selected xValue and dataSetIndex. + * + * @param set + * @param dataSetIndex + * @param xVal + * @param rounding + * @return + */ + protected List buildHighlights(IDataSet set, int dataSetIndex, float xVal, DataSet.Rounding rounding) { + + ArrayList highlights = new ArrayList<>(); + + //noinspection unchecked + List entries = set.getEntriesForXValue(xVal); + if (entries.size() == 0) { + // Try to find closest x-value and take all entries for that x-value + final Entry closest = set.getEntryForXValue(xVal, Float.NaN, rounding); + if (closest != null) + { + //noinspection unchecked + entries = set.getEntriesForXValue(closest.getX()); + } + } + + if (entries.size() == 0) + return highlights; + + for (Entry e : entries) { + MPPointD pixels = mChart.getTransformer( + set.getAxisDependency()).getPixelForValues(e.getX(), e.getY()); + + highlights.add(new Highlight( + e.getX(), e.getY(), + (float) pixels.x, (float) pixels.y, + dataSetIndex, set.getAxisDependency())); + } + + return highlights; + } + + /** + * Returns the Highlight of the DataSet that contains the closest value on the + * y-axis. + * + * @param closestValues contains two Highlight objects per DataSet closest to the selected x-position (determined by + * rounding up an down) + * @param x + * @param y + * @param axis the closest axis + * @param minSelectionDistance + * @return + */ + public Highlight getClosestHighlightByPixel(List closestValues, float x, float y, + YAxis.AxisDependency axis, float minSelectionDistance) { + + Highlight closest = null; + float distance = minSelectionDistance; + + for (int i = 0; i < closestValues.size(); i++) { + + Highlight high = closestValues.get(i); + + if (axis == null || high.getAxis() == axis) { + + float cDistance = getDistance(x, y, high.getXPx(), high.getYPx()); + + if (cDistance < distance) { + closest = high; + distance = cDistance; + } + } + } + + return closest; + } + + /** + * Calculates the distance between the two given points. + * + * @param x1 + * @param y1 + * @param x2 + * @param y2 + * @return + */ + protected float getDistance(float x1, float y1, float x2, float y2) { + //return Math.abs(y1 - y2); + //return Math.abs(x1 - x2); + return (float) Math.hypot(x1 - x2, y1 - y2); + } + + protected BarLineScatterCandleBubbleData getData() { + return mChart.getData(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/CombinedHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/CombinedHighlighter.java new file mode 100644 index 0000000..76788af --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/CombinedHighlighter.java @@ -0,0 +1,93 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.dataprovider.CombinedDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; + +import java.util.List; + +/** + * Created by Philipp Jahoda on 12/09/15. + */ +public class CombinedHighlighter extends ChartHighlighter implements IHighlighter +{ + + /** + * bar highlighter for supporting stacked highlighting + */ + protected BarHighlighter barHighlighter; + + public CombinedHighlighter(CombinedDataProvider chart, BarDataProvider barChart) { + super(chart); + + // if there is BarData, create a BarHighlighter + barHighlighter = barChart.getBarData() == null ? null : new BarHighlighter(barChart); + } + + @Override + protected List getHighlightsAtXValue(float xVal, float x, float y) { + + mHighlightBuffer.clear(); + + List dataObjects = mChart.getCombinedData().getAllData(); + + for (int i = 0; i < dataObjects.size(); i++) { + + ChartData dataObject = dataObjects.get(i); + + // in case of BarData, let the BarHighlighter take over + if (barHighlighter != null && dataObject instanceof BarData) { + Highlight high = barHighlighter.getHighlight(x, y); + + if (high != null) { + high.setDataIndex(i); + mHighlightBuffer.add(high); + } + } else { + + for (int j = 0, dataSetCount = dataObject.getDataSetCount(); j < dataSetCount; j++) { + + IDataSet dataSet = dataObjects.get(i).getDataSetByIndex(j); + + // don't include datasets that cannot be highlighted + if (!dataSet.isHighlightEnabled()) + continue; + + List highs = buildHighlights(dataSet, j, xVal, DataSet.Rounding.CLOSEST); + for (Highlight high : highs) + { + high.setDataIndex(i); + mHighlightBuffer.add(high); + } + } + } + } + + return mHighlightBuffer; + } + +// protected Highlight getClosest(float x, float y, Highlight... highs) { +// +// Highlight closest = null; +// float minDistance = Float.MAX_VALUE; +// +// for (Highlight high : highs) { +// +// if (high == null) +// continue; +// +// float tempDistance = getDistance(x, y, high.getXPx(), high.getYPx()); +// +// if (tempDistance < minDistance) { +// minDistance = tempDistance; +// closest = high; +// } +// } +// +// return closest; +// } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Highlight.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Highlight.java new file mode 100644 index 0000000..62307cb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Highlight.java @@ -0,0 +1,243 @@ + +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.components.YAxis; + +/** + * Contains information needed to determine the highlighted value. + * + * @author Philipp Jahoda + */ +public class Highlight { + + /** + * the x-value of the highlighted value + */ + private float mX = Float.NaN; + + /** + * the y-value of the highlighted value + */ + private float mY = Float.NaN; + + /** + * the x-pixel of the highlight + */ + private float mXPx; + + /** + * the y-pixel of the highlight + */ + private float mYPx; + + /** + * the index of the data object - in case it refers to more than one + */ + private int mDataIndex = -1; + + /** + * the index of the dataset the highlighted value is in + */ + private int mDataSetIndex; + + /** + * index which value of a stacked bar entry is highlighted, default -1 + */ + private int mStackIndex = -1; + + /** + * the axis the highlighted value belongs to + */ + private YAxis.AxisDependency axis; + + /** + * the x-position (pixels) on which this highlight object was last drawn + */ + private float mDrawX; + + /** + * the y-position (pixels) on which this highlight object was last drawn + */ + private float mDrawY; + + public Highlight(float x, float y, int dataSetIndex, int dataIndex) { + this.mX = x; + this.mY = y; + this.mDataSetIndex = dataSetIndex; + this.mDataIndex = dataIndex; + } + + public Highlight(float x, float y, int dataSetIndex) { + this.mX = x; + this.mY = y; + this.mDataSetIndex = dataSetIndex; + this.mDataIndex = -1; + } + + public Highlight(float x, int dataSetIndex, int stackIndex) { + this(x, Float.NaN, dataSetIndex); + this.mStackIndex = stackIndex; + } + + /** + * constructor + * + * @param x the x-value of the highlighted value + * @param y the y-value of the highlighted value + * @param dataSetIndex the index of the DataSet the highlighted value belongs to + */ + public Highlight(float x, float y, float xPx, float yPx, int dataSetIndex, YAxis.AxisDependency axis) { + this.mX = x; + this.mY = y; + this.mXPx = xPx; + this.mYPx = yPx; + this.mDataSetIndex = dataSetIndex; + this.axis = axis; + } + + /** + * Constructor, only used for stacked-barchart. + * + * @param x the index of the highlighted value on the x-axis + * @param y the y-value of the highlighted value + * @param dataSetIndex the index of the DataSet the highlighted value belongs to + * @param stackIndex references which value of a stacked-bar entry has been + * selected + */ + public Highlight(float x, float y, float xPx, float yPx, int dataSetIndex, int stackIndex, YAxis.AxisDependency axis) { + this(x, y, xPx, yPx, dataSetIndex, axis); + this.mStackIndex = stackIndex; + } + + /** + * returns the x-value of the highlighted value + * + * @return + */ + public float getX() { + return mX; + } + + /** + * returns the y-value of the highlighted value + * + * @return + */ + public float getY() { + return mY; + } + + /** + * returns the x-position of the highlight in pixels + */ + public float getXPx() { + return mXPx; + } + + /** + * returns the y-position of the highlight in pixels + */ + public float getYPx() { + return mYPx; + } + + /** + * the index of the data object - in case it refers to more than one + * + * @return + */ + public int getDataIndex() { + return mDataIndex; + } + + public void setDataIndex(int mDataIndex) { + this.mDataIndex = mDataIndex; + } + + /** + * returns the index of the DataSet the highlighted value is in + * + * @return + */ + public int getDataSetIndex() { + return mDataSetIndex; + } + + /** + * Only needed if a stacked-barchart entry was highlighted. References the + * selected value within the stacked-entry. + * + * @return + */ + public int getStackIndex() { + return mStackIndex; + } + + public boolean isStacked() { + return mStackIndex >= 0; + } + + /** + * Returns the axis the highlighted value belongs to. + * + * @return + */ + public YAxis.AxisDependency getAxis() { + return axis; + } + + /** + * Sets the x- and y-position (pixels) where this highlight was last drawn. + * + * @param x + * @param y + */ + public void setDraw(float x, float y) { + this.mDrawX = x; + this.mDrawY = y; + } + + /** + * Returns the x-position in pixels where this highlight object was last drawn. + * + * @return + */ + public float getDrawX() { + return mDrawX; + } + + /** + * Returns the y-position in pixels where this highlight object was last drawn. + * + * @return + */ + public float getDrawY() { + return mDrawY; + } + + /** + * Returns true if this highlight object is equal to the other (compares + * xIndex and dataSetIndex) + * + * @param h + * @return + */ + public boolean equalTo(Highlight h) { + + if (h == null) + return false; + else { + if (this.mDataSetIndex == h.mDataSetIndex && this.mX == h.mX + && this.mStackIndex == h.mStackIndex && this.mDataIndex == h.mDataIndex) + return true; + else + return false; + } + } + + @Override + public String toString() { + return "Highlight, x: " + mX + ", y: " + mY + ", dataSetIndex: " + mDataSetIndex + + ", stackIndex (only stacked barentry): " + mStackIndex; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java new file mode 100644 index 0000000..d96298e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java @@ -0,0 +1,85 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointD; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Philipp Jahoda on 22/07/15. + */ +public class HorizontalBarHighlighter extends BarHighlighter { + + public HorizontalBarHighlighter(BarDataProvider chart) { + super(chart); + } + + @Override + public Highlight getHighlight(float x, float y) { + + BarData barData = mChart.getBarData(); + + MPPointD pos = getValsForTouch(y, x); + + Highlight high = getHighlightForX((float) pos.y, y, x); + if (high == null) + return null; + + IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); + if (set.isStacked()) { + + return getStackedHighlight(high, + set, + (float) pos.y, + (float) pos.x); + } + + MPPointD.recycleInstance(pos); + + return high; + } + + @Override + protected List buildHighlights(IDataSet set, int dataSetIndex, float xVal, DataSet.Rounding rounding) { + + ArrayList highlights = new ArrayList<>(); + + //noinspection unchecked + List entries = set.getEntriesForXValue(xVal); + if (entries.size() == 0) { + // Try to find closest x-value and take all entries for that x-value + final Entry closest = set.getEntryForXValue(xVal, Float.NaN, rounding); + if (closest != null) + { + //noinspection unchecked + entries = set.getEntriesForXValue(closest.getX()); + } + } + + if (entries.size() == 0) + return highlights; + + for (Entry e : entries) { + MPPointD pixels = mChart.getTransformer( + set.getAxisDependency()).getPixelForValues(e.getY(), e.getX()); + + highlights.add(new Highlight( + e.getX(), e.getY(), + (float) pixels.x, (float) pixels.y, + dataSetIndex, set.getAxisDependency())); + } + + return highlights; + } + + @Override + protected float getDistance(float x1, float y1, float x2, float y2) { + return Math.abs(y1 - y2); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/IHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/IHighlighter.java new file mode 100644 index 0000000..d0ca0cf --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/IHighlighter.java @@ -0,0 +1,17 @@ +package com.github.mikephil.charting.highlight; + +/** + * Created by philipp on 10/06/16. + */ +public interface IHighlighter +{ + + /** + * Returns a Highlight object corresponding to the given x- and y- touch positions in pixels. + * + * @param x + * @param y + * @return + */ + Highlight getHighlight(float x, float y); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieHighlighter.java new file mode 100644 index 0000000..b4c5a1b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieHighlighter.java @@ -0,0 +1,25 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; + +/** + * Created by philipp on 12/06/16. + */ +public class PieHighlighter extends PieRadarHighlighter { + + public PieHighlighter(PieChart chart) { + super(chart); + } + + @Override + protected Highlight getClosestHighlight(int index, float x, float y) { + + IPieDataSet set = mChart.getData().getDataSet(); + + final Entry entry = set.getEntryForIndex(index); + + return new Highlight(index, entry.getY(), x, y, 0, set.getAxisDependency()); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java new file mode 100644 index 0000000..a19906d --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java @@ -0,0 +1,66 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.charts.PieRadarChartBase; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by philipp on 12/06/16. + */ +public abstract class PieRadarHighlighter implements IHighlighter +{ + + protected T mChart; + + /** + * buffer for storing previously highlighted values + */ + protected List mHighlightBuffer = new ArrayList(); + + public PieRadarHighlighter(T chart) { + this.mChart = chart; + } + + @Override + public Highlight getHighlight(float x, float y) { + + float touchDistanceToCenter = mChart.distanceToCenter(x, y); + + // check if a slice was touched + if (touchDistanceToCenter > mChart.getRadius()) { + + // if no slice was touched, highlight nothing + return null; + + } else { + + float angle = mChart.getAngleForPoint(x, y); + + if (mChart instanceof PieChart) { + angle /= mChart.getAnimator().getPhaseY(); + } + + int index = mChart.getIndexForAngle(angle); + + // check if the index could be found + if (index < 0 || index >= mChart.getData().getMaxEntryCountSet().getEntryCount()) { + return null; + + } else { + return getClosestHighlight(index, x, y); + } + } + } + + /** + * Returns the closest Highlight object of the given objects based on the touch position inside the chart. + * + * @param index + * @param x + * @param y + * @return + */ + protected abstract Highlight getClosestHighlight(int index, float x, float y); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/RadarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/RadarHighlighter.java new file mode 100644 index 0000000..3c4f6d0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/RadarHighlighter.java @@ -0,0 +1,79 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.charts.RadarChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +import java.util.List; + +/** + * Created by philipp on 12/06/16. + */ +public class RadarHighlighter extends PieRadarHighlighter { + + public RadarHighlighter(RadarChart chart) { + super(chart); + } + + @Override + protected Highlight getClosestHighlight(int index, float x, float y) { + + List highlights = getHighlightsAtIndex(index); + + float distanceToCenter = mChart.distanceToCenter(x, y) / mChart.getFactor(); + + Highlight closest = null; + float distance = Float.MAX_VALUE; + + for (int i = 0; i < highlights.size(); i++) { + + Highlight high = highlights.get(i); + + float cdistance = Math.abs(high.getY() - distanceToCenter); + if (cdistance < distance) { + closest = high; + distance = cdistance; + } + } + + return closest; + } + /** + * Returns an array of Highlight objects for the given index. The Highlight + * objects give information about the value at the selected index and the + * DataSet it belongs to. INFORMATION: This method does calculations at + * runtime. Do not over-use in performance critical situations. + * + * @param index + * @return + */ + protected List getHighlightsAtIndex(int index) { + + mHighlightBuffer.clear(); + + float phaseX = mChart.getAnimator().getPhaseX(); + float phaseY = mChart.getAnimator().getPhaseY(); + float sliceangle = mChart.getSliceAngle(); + float factor = mChart.getFactor(); + + MPPointF pOut = MPPointF.getInstance(0,0); + for (int i = 0; i < mChart.getData().getDataSetCount(); i++) { + + IDataSet dataSet = mChart.getData().getDataSetByIndex(i); + + final Entry entry = dataSet.getEntryForIndex(index); + + float y = (entry.getY() - mChart.getYChartMin()); + + Utils.getPosition( + mChart.getCenterOffsets(), y * factor * phaseY, + sliceangle * index * phaseX + mChart.getRotationAngle(), pOut); + + mHighlightBuffer.add(new Highlight(index, entry.getY(), pOut.x, pOut.y, i, dataSet.getAxisDependency())); + } + + return mHighlightBuffer; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Range.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Range.java new file mode 100644 index 0000000..96dd592 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Range.java @@ -0,0 +1,38 @@ +package com.github.mikephil.charting.highlight; + +/** + * Created by Philipp Jahoda on 24/07/15. Class that represents the range of one value in a stacked bar entry. e.g. + * stack values are -10, 5, 20 -> then ranges are (-10 - 0, 0 - 5, 5 - 25). + */ +public final class Range { + + public float from; + public float to; + + public Range(float from, float to) { + this.from = from; + this.to = to; + } + + /** + * Returns true if this range contains (if the value is in between) the given value, false if not. + * + * @param value + * @return + */ + public boolean contains(float value) { + + if (value > from && value <= to) + return true; + else + return false; + } + + public boolean isLarger(float value) { + return value > to; + } + + public boolean isSmaller(float value) { + return value < from; + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarDataProvider.java new file mode 100644 index 0000000..9dfee07 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarDataProvider.java @@ -0,0 +1,11 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.BarData; + +public interface BarDataProvider extends BarLineScatterCandleBubbleDataProvider { + + BarData getBarData(); + boolean isDrawBarShadowEnabled(); + boolean isDrawValueAboveBarEnabled(); + boolean isHighlightFullBarEnabled(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.java new file mode 100644 index 0000000..68beeb8 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.java @@ -0,0 +1,16 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.utils.Transformer; + +public interface BarLineScatterCandleBubbleDataProvider extends ChartInterface { + + Transformer getTransformer(AxisDependency axis); + boolean isInverted(AxisDependency axis); + + float getLowestVisibleX(); + float getHighestVisibleX(); + + BarLineScatterCandleBubbleData getData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BubbleDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BubbleDataProvider.java new file mode 100644 index 0000000..82ee30a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BubbleDataProvider.java @@ -0,0 +1,8 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.BubbleData; + +public interface BubbleDataProvider extends BarLineScatterCandleBubbleDataProvider { + + BubbleData getBubbleData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CandleDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CandleDataProvider.java new file mode 100644 index 0000000..357403f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CandleDataProvider.java @@ -0,0 +1,8 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.CandleData; + +public interface CandleDataProvider extends BarLineScatterCandleBubbleDataProvider { + + CandleData getCandleData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ChartInterface.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ChartInterface.java new file mode 100644 index 0000000..219b46b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ChartInterface.java @@ -0,0 +1,69 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import android.graphics.RectF; + +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.utils.MPPointF; + +/** + * Interface that provides everything there is to know about the dimensions, + * bounds, and range of the chart. + * + * @author Philipp Jahoda + */ +public interface ChartInterface { + + /** + * Returns the minimum x value of the chart, regardless of zoom or translation. + * + * @return + */ + float getXChartMin(); + + /** + * Returns the maximum x value of the chart, regardless of zoom or translation. + * + * @return + */ + float getXChartMax(); + + float getXRange(); + + /** + * Returns the minimum y value of the chart, regardless of zoom or translation. + * + * @return + */ + float getYChartMin(); + + /** + * Returns the maximum y value of the chart, regardless of zoom or translation. + * + * @return + */ + float getYChartMax(); + + /** + * Returns the maximum distance in scren dp a touch can be away from an entry to cause it to get highlighted. + * + * @return + */ + float getMaxHighlightDistance(); + + int getWidth(); + + int getHeight(); + + MPPointF getCenterOfView(); + + MPPointF getCenterOffsets(); + + RectF getContentRect(); + + IValueFormatter getDefaultValueFormatter(); + + ChartData getData(); + + int getMaxVisibleCount(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CombinedDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CombinedDataProvider.java new file mode 100644 index 0000000..574d26a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CombinedDataProvider.java @@ -0,0 +1,11 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.CombinedData; + +/** + * Created by philipp on 11/06/16. + */ +public interface CombinedDataProvider extends LineDataProvider, BarDataProvider, BubbleDataProvider, CandleDataProvider, ScatterDataProvider { + + CombinedData getCombinedData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/LineDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/LineDataProvider.java new file mode 100644 index 0000000..5c23ac2 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/LineDataProvider.java @@ -0,0 +1,11 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.LineData; + +public interface LineDataProvider extends BarLineScatterCandleBubbleDataProvider { + + LineData getLineData(); + + YAxis getAxis(YAxis.AxisDependency dependency); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ScatterDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ScatterDataProvider.java new file mode 100644 index 0000000..b58d5af --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ScatterDataProvider.java @@ -0,0 +1,8 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.ScatterData; + +public interface ScatterDataProvider extends BarLineScatterCandleBubbleDataProvider { + + ScatterData getScatterData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarDataSet.java new file mode 100644 index 0000000..5e82a48 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarDataSet.java @@ -0,0 +1,71 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.utils.Fill; + +import java.util.List; + +/** + * Created by philipp on 21/10/15. + */ +public interface IBarDataSet extends IBarLineScatterCandleBubbleDataSet { + + List getFills(); + + Fill getFill(int index); + + /** + * Returns true if this DataSet is stacked (stacksize > 1) or not. + * + * @return + */ + boolean isStacked(); + + /** + * Returns the maximum number of bars that can be stacked upon another in + * this DataSet. This should return 1 for non stacked bars, and > 1 for stacked bars. + * + * @return + */ + int getStackSize(); + + /** + * Returns the color used for drawing the bar-shadows. The bar shadows is a + * surface behind the bar that indicates the maximum value. + * + * @return + */ + int getBarShadowColor(); + + /** + * Returns the width used for drawing borders around the bars. + * If borderWidth == 0, no border will be drawn. + * + * @return + */ + float getBarBorderWidth(); + + /** + * Returns the color drawing borders around the bars. + * + * @return + */ + int getBarBorderColor(); + + /** + * Returns the alpha value (transparency) that is used for drawing the + * highlight indicator. + * + * @return + */ + int getHighLightAlpha(); + + + /** + * Returns the labels used for the different value-stacks in the legend. + * This is only relevant for stacked bar entries. + * + * @return + */ + String[] getStackLabels(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.java new file mode 100644 index 0000000..0f9454b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.java @@ -0,0 +1,16 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.Entry; + +/** + * Created by philipp on 21/10/15. + */ +public interface IBarLineScatterCandleBubbleDataSet extends IDataSet { + + /** + * Returns the color that is used for drawing the highlight indicators. + * + * @return + */ + int getHighLightColor(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBubbleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBubbleDataSet.java new file mode 100644 index 0000000..e284aac --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBubbleDataSet.java @@ -0,0 +1,27 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.BubbleEntry; + +/** + * Created by philipp on 21/10/15. + */ +public interface IBubbleDataSet extends IBarLineScatterCandleBubbleDataSet { + + /** + * Sets the width of the circle that surrounds the bubble when highlighted, + * in dp. + * + * @param width + */ + void setHighlightCircleWidth(float width); + + float getMaxSize(); + + boolean isNormalizeSizeEnabled(); + + /** + * Returns the width of the highlight-circle that surrounds the bubble + * @return + */ + float getHighlightCircleWidth(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ICandleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ICandleDataSet.java new file mode 100644 index 0000000..1d004ed --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ICandleDataSet.java @@ -0,0 +1,85 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.Paint; + +import com.github.mikephil.charting.data.CandleEntry; + +/** + * Created by philipp on 21/10/15. + */ +public interface ICandleDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the space that is left out on the left and right side of each + * candle. + * + * @return + */ + float getBarSpace(); + + /** + * Returns whether the candle bars should show? + * When false, only "ticks" will show + * + * - default: true + * + * @return + */ + boolean getShowCandleBar(); + + /** + * Returns the width of the candle-shadow-line in pixels. + * + * @return + */ + float getShadowWidth(); + + /** + * Returns shadow color for all entries + * + * @return + */ + int getShadowColor(); + + /** + * Returns the neutral color (for open == close) + * + * @return + */ + int getNeutralColor(); + + /** + * Returns the increasing color (for open < close). + * + * @return + */ + int getIncreasingColor(); + + /** + * Returns the decreasing color (for open > close). + * + * @return + */ + int getDecreasingColor(); + + /** + * Returns paint style when open < close + * + * @return + */ + Paint.Style getIncreasingPaintStyle(); + + /** + * Returns paint style when open > close + * + * @return + */ + Paint.Style getDecreasingPaintStyle(); + + /** + * Is the shadow color same as the candle color? + * + * @return + */ + boolean getShadowColorSameAsCandle(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IDataSet.java new file mode 100644 index 0000000..fd8af70 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IDataSet.java @@ -0,0 +1,486 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.DashPathEffect; +import android.graphics.PointF; +import android.graphics.Typeface; + +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.utils.MPPointF; + +import java.util.List; + +/** + * Created by Philipp Jahoda on 21/10/15. + */ +public interface IDataSet { + + /** ###### ###### DATA RELATED METHODS ###### ###### */ + + /** + * returns the minimum y-value this DataSet holds + * + * @return + */ + float getYMin(); + + /** + * returns the maximum y-value this DataSet holds + * + * @return + */ + float getYMax(); + + /** + * returns the minimum x-value this DataSet holds + * + * @return + */ + float getXMin(); + + /** + * returns the maximum x-value this DataSet holds + * + * @return + */ + float getXMax(); + + /** + * Returns the number of y-values this DataSet represents -> the size of the y-values array + * -> yvals.size() + * + * @return + */ + int getEntryCount(); + + /** + * Calculates the minimum and maximum x and y values (mXMin, mXMax, mYMin, mYMax). + */ + void calcMinMax(); + + /** + * Calculates the min and max y-values from the Entry closest to the given fromX to the Entry closest to the given toX value. + * This is only needed for the autoScaleMinMax feature. + * + * @param fromX + * @param toX + */ + void calcMinMaxY(float fromX, float toX); + + /** + * Returns the first Entry object found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value according to the rounding. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @param rounding determine whether to round up/down/closest + * if there is no Entry matching the provided x-value + * @return + * + * + */ + T getEntryForXValue(float xValue, float closestToY, DataSet.Rounding rounding); + + /** + * Returns the first Entry object found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @return + */ + T getEntryForXValue(float xValue, float closestToY); + + /** + * Returns all Entry objects found at the given x-value with binary + * search. An empty array if no Entry object at that x-value. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue + * @return + */ + List getEntriesForXValue(float xValue); + + /** + * Returns the Entry object found at the given index (NOT xIndex) in the values array. + * + * @param index + * @return + */ + T getEntryForIndex(int index); + + /** + * Returns the first Entry index found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value according to the rounding. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @param rounding determine whether to round up/down/closest + * if there is no Entry matching the provided x-value + * @return + */ + int getEntryIndex(float xValue, float closestToY, DataSet.Rounding rounding); + + /** + * Returns the position of the provided entry in the DataSets Entry array. + * Returns -1 if doesn't exist. + * + * @param e + * @return + */ + int getEntryIndex(T e); + + + /** + * This method returns the actual + * index in the Entry array of the DataSet for a given xIndex. IMPORTANT: This method does + * calculations at runtime, do not over-use in performance critical + * situations. + * + * @param xIndex + * @return + */ + int getIndexInEntries(int xIndex); + + /** + * Adds an Entry to the DataSet dynamically. + * Entries are added to the end of the list. + * This will also recalculate the current minimum and maximum + * values of the DataSet and the value-sum. + * + * @param e + */ + boolean addEntry(T e); + + + /** + * Adds an Entry to the DataSet dynamically. + * Entries are added to their appropriate index in the values array respective to their x-position. + * This will also recalculate the current minimum and maximum + * values of the DataSet and the value-sum. + * + * @param e + */ + void addEntryOrdered(T e); + + /** + * Removes the first Entry (at index 0) of this DataSet from the entries array. + * Returns true if successful, false if not. + * + * @return + */ + boolean removeFirst(); + + /** + * Removes the last Entry (at index size-1) of this DataSet from the entries array. + * Returns true if successful, false if not. + * + * @return + */ + boolean removeLast(); + + /** + * Removes an Entry from the DataSets entries array. This will also + * recalculate the current minimum and maximum values of the DataSet and the + * value-sum. Returns true if an Entry was removed, false if no Entry could + * be removed. + * + * @param e + */ + boolean removeEntry(T e); + + /** + * Removes the Entry object closest to the given x-value from the DataSet. + * Returns true if an Entry was removed, false if no Entry could be removed. + * + * @param xValue + */ + boolean removeEntryByXValue(float xValue); + + /** + * Removes the Entry object at the given index in the values array from the DataSet. + * Returns true if an Entry was removed, false if no Entry could be removed. + * + * @param index + * @return + */ + boolean removeEntry(int index); + + /** + * Checks if this DataSet contains the specified Entry. Returns true if so, + * false if not. NOTE: Performance is pretty bad on this one, do not + * over-use in performance critical situations. + * + * @param entry + * @return + */ + boolean contains(T entry); + + /** + * Removes all values from this DataSet and does all necessary recalculations. + */ + void clear(); + + + /** ###### ###### STYLING RELATED (& OTHER) METHODS ###### ###### */ + + /** + * Returns the label string that describes the DataSet. + * + * @return + */ + String getLabel(); + + /** + * Sets the label string that describes the DataSet. + * + * @param label + */ + void setLabel(String label); + + /** + * Returns the axis this DataSet should be plotted against. + * + * @return + */ + YAxis.AxisDependency getAxisDependency(); + + /** + * Set the y-axis this DataSet should be plotted against (either LEFT or + * RIGHT). Default: LEFT + * + * @param dependency + */ + void setAxisDependency(YAxis.AxisDependency dependency); + + /** + * returns all the colors that are set for this DataSet + * + * @return + */ + List getColors(); + + /** + * Returns the first color (index 0) of the colors-array this DataSet + * contains. This is only used for performance reasons when only one color is in the colors array (size == 1) + * + * @return + */ + int getColor(); + + /** + * Returns the color at the given index of the DataSet's color array. + * Performs a IndexOutOfBounds check by modulus. + * + * @param index + * @return + */ + int getColor(int index); + + /** + * returns true if highlighting of values is enabled, false if not + * + * @return + */ + boolean isHighlightEnabled(); + + /** + * If set to true, value highlighting is enabled which means that values can + * be highlighted programmatically or by touch gesture. + * + * @param enabled + */ + void setHighlightEnabled(boolean enabled); + + /** + * Sets the formatter to be used for drawing the values inside the chart. If + * no formatter is set, the chart will automatically determine a reasonable + * formatting (concerning decimals) for all the values that are drawn inside + * the chart. Use chart.getDefaultValueFormatter() to use the formatter + * calculated by the chart. + * + * @param f + */ + void setValueFormatter(IValueFormatter f); + + /** + * Returns the formatter used for drawing the values inside the chart. + * + * @return + */ + IValueFormatter getValueFormatter(); + + /** + * Returns true if the valueFormatter object of this DataSet is null. + * + * @return + */ + boolean needsFormatter(); + + /** + * Sets the color the value-labels of this DataSet should have. + * + * @param color + */ + void setValueTextColor(int color); + + /** + * Sets a list of colors to be used as the colors for the drawn values. + * + * @param colors + */ + void setValueTextColors(List colors); + + /** + * Sets a Typeface for the value-labels of this DataSet. + * + * @param tf + */ + void setValueTypeface(Typeface tf); + + /** + * Sets the text-size of the value-labels of this DataSet in dp. + * + * @param size + */ + void setValueTextSize(float size); + + /** + * Returns only the first color of all colors that are set to be used for the values. + * + * @return + */ + int getValueTextColor(); + + /** + * Returns the color at the specified index that is used for drawing the values inside the chart. + * Uses modulus internally. + * + * @param index + * @return + */ + int getValueTextColor(int index); + + /** + * Returns the typeface that is used for drawing the values inside the chart + * + * @return + */ + Typeface getValueTypeface(); + + /** + * Returns the text size that is used for drawing the values inside the chart + * + * @return + */ + float getValueTextSize(); + + /** + * The form to draw for this dataset in the legend. + *

+ * Return `DEFAULT` to use the default legend form. + */ + Legend.LegendForm getForm(); + + /** + * The form size to draw for this dataset in the legend. + *

+ * Return `Float.NaN` to use the default legend form size. + */ + float getFormSize(); + + /** + * The line width for drawing the form of this dataset in the legend + *

+ * Return `Float.NaN` to use the default legend form line width. + */ + float getFormLineWidth(); + + /** + * The line dash path effect used for shapes that consist of lines. + *

+ * Return `null` to use the default legend form line dash effect. + */ + DashPathEffect getFormLineDashEffect(); + + /** + * set this to true to draw y-values on the chart. + * + * NOTE (for bar and line charts): if `maxVisibleCount` is reached, no values will be drawn even + * if this is enabled + * @param enabled + */ + void setDrawValues(boolean enabled); + + /** + * Returns true if y-value drawing is enabled, false if not + * + * @return + */ + boolean isDrawValuesEnabled(); + + /** + * Set this to true to draw y-icons on the chart. + * + * NOTE (for bar and line charts): if `maxVisibleCount` is reached, no icons will be drawn even + * if this is enabled + * + * @param enabled + */ + void setDrawIcons(boolean enabled); + + /** + * Returns true if y-icon drawing is enabled, false if not + * + * @return + */ + boolean isDrawIconsEnabled(); + + /** + * Offset of icons drawn on the chart. + * + * For all charts except Pie and Radar it will be ordinary (x offset,y offset). + * + * For Pie and Radar chart it will be (y offset, distance from center offset); so if you want icon to be rendered under value, you should increase X component of CGPoint, and if you want icon to be rendered closet to center, you should decrease height component of CGPoint. + * @param offset + */ + void setIconsOffset(MPPointF offset); + + /** + * Get the offset for drawing icons. + */ + MPPointF getIconsOffset(); + + /** + * Set the visibility of this DataSet. If not visible, the DataSet will not + * be drawn to the chart upon refreshing it. + * + * @param visible + */ + void setVisible(boolean visible); + + /** + * Returns true if this DataSet is visible inside the chart, or false if it + * is currently hidden. + * + * @return + */ + boolean isVisible(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineDataSet.java new file mode 100644 index 0000000..3f534fe --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineDataSet.java @@ -0,0 +1,103 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.DashPathEffect; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.formatter.IFillFormatter; + +/** + * Created by Philpp Jahoda on 21/10/15. + */ +public interface ILineDataSet extends ILineRadarDataSet { + + /** + * Returns the drawing mode for this line dataset + * + * @return + */ + LineDataSet.Mode getMode(); + + /** + * Returns the intensity of the cubic lines (the effect intensity). + * Max = 1f = very cubic, Min = 0.05f = low cubic effect, Default: 0.2f + * + * @return + */ + float getCubicIntensity(); + + @Deprecated + boolean isDrawCubicEnabled(); + + @Deprecated + boolean isDrawSteppedEnabled(); + + /** + * Returns the size of the drawn circles. + */ + float getCircleRadius(); + + /** + * Returns the hole radius of the drawn circles. + */ + float getCircleHoleRadius(); + + /** + * Returns the color at the given index of the DataSet's circle-color array. + * Performs a IndexOutOfBounds check by modulus. + * + * @param index + * @return + */ + int getCircleColor(int index); + + /** + * Returns the number of colors in this DataSet's circle-color array. + * + * @return + */ + int getCircleColorCount(); + + /** + * Returns true if drawing circles for this DataSet is enabled, false if not + * + * @return + */ + boolean isDrawCirclesEnabled(); + + /** + * Returns the color of the inner circle (the circle-hole). + * + * @return + */ + int getCircleHoleColor(); + + /** + * Returns true if drawing the circle-holes is enabled, false if not. + * + * @return + */ + boolean isDrawCircleHoleEnabled(); + + /** + * Returns the DashPathEffect that is used for drawing the lines. + * + * @return + */ + DashPathEffect getDashPathEffect(); + + /** + * Returns true if the dashed-line effect is enabled, false if not. + * If the DashPathEffect object is null, also return false here. + * + * @return + */ + boolean isDashedLineEnabled(); + + /** + * Returns the IFillFormatter that is set for this DataSet. + * + * @return + */ + IFillFormatter getFillFormatter(); +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineRadarDataSet.java new file mode 100644 index 0000000..ce89822 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineRadarDataSet.java @@ -0,0 +1,58 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.data.Entry; + +/** + * Created by Philipp Jahoda on 21/10/15. + */ +public interface ILineRadarDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the color that is used for filling the line surface area. + * + * @return + */ + int getFillColor(); + + /** + * Returns the drawable used for filling the area below the line. + * + * @return + */ + Drawable getFillDrawable(); + + /** + * Returns the alpha value that is used for filling the line surface, + * default: 85 + * + * @return + */ + int getFillAlpha(); + + /** + * Returns the stroke-width of the drawn line + * + * @return + */ + float getLineWidth(); + + /** + * Returns true if filled drawing is enabled, false if not + * + * @return + */ + boolean isDrawFilledEnabled(); + + /** + * Set to true if the DataSet should be drawn filled (surface), and not just + * as a line, disabling this will give great performance boost. Please note that this method + * uses the canvas.clipPath(...) method for drawing the filled area. + * For devices with API level < 18 (Android 4.3), hardware acceleration of the chart should + * be turned off. Default: false + * + * @param enabled + */ + void setDrawFilled(boolean enabled); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineScatterCandleRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineScatterCandleRadarDataSet.java new file mode 100644 index 0000000..9ab6802 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineScatterCandleRadarDataSet.java @@ -0,0 +1,35 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.DashPathEffect; + +import com.github.mikephil.charting.data.Entry; + +/** + * Created by Philipp Jahoda on 21/10/15. + */ +public interface ILineScatterCandleRadarDataSet extends IBarLineScatterCandleBubbleDataSet { + + /** + * Returns true if vertical highlight indicator lines are enabled (drawn) + * @return + */ + boolean isVerticalHighlightIndicatorEnabled(); + + /** + * Returns true if vertical highlight indicator lines are enabled (drawn) + * @return + */ + boolean isHorizontalHighlightIndicatorEnabled(); + + /** + * Returns the line-width in which highlight lines are to be drawn. + * @return + */ + float getHighlightLineWidth(); + + /** + * Returns the DashPathEffect that is used for highlighting. + * @return + */ + DashPathEffect getDashPathEffectHighlight(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IPieDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IPieDataSet.java new file mode 100644 index 0000000..b228fca --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IPieDataSet.java @@ -0,0 +1,82 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import androidx.annotation.Nullable; + +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; + +/** + * Created by Philipp Jahoda on 03/11/15. + */ +public interface IPieDataSet extends IDataSet { + + /** + * Returns the space that is set to be between the piechart-slices of this + * DataSet, in pixels. + * + * @return + */ + float getSliceSpace(); + + /** + * When enabled, slice spacing will be 0.0 when the smallest value is going to be + * smaller than the slice spacing itself. + * + * @return + */ + boolean isAutomaticallyDisableSliceSpacingEnabled(); + + /** + * Returns the distance a highlighted piechart slice is "shifted" away from + * the chart-center in dp. + * + * @return + */ + float getSelectionShift(); + + PieDataSet.ValuePosition getXValuePosition(); + PieDataSet.ValuePosition getYValuePosition(); + + /** + * When valuePosition is OutsideSlice, indicates line color + * */ + int getValueLineColor(); + + /** + * When valuePosition is OutsideSlice and enabled, line will have the same color as the slice + * */ + boolean isUseValueColorForLineEnabled(); + + /** + * When valuePosition is OutsideSlice, indicates line width + * */ + float getValueLineWidth(); + + /** + * When valuePosition is OutsideSlice, indicates offset as percentage out of the slice size + * */ + float getValueLinePart1OffsetPercentage(); + + /** + * When valuePosition is OutsideSlice, indicates length of first half of the line + * */ + float getValueLinePart1Length(); + + /** + * When valuePosition is OutsideSlice, indicates length of second half of the line + * */ + float getValueLinePart2Length(); + + /** + * When valuePosition is OutsideSlice, this allows variable line length + * */ + boolean isValueLineVariableLength(); + + /** + * Gets the color for the highlighted sector + * */ + @Nullable + Integer getHighlightColor(); + +} + diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IRadarDataSet.java new file mode 100644 index 0000000..8af00d5 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IRadarDataSet.java @@ -0,0 +1,30 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.RadarEntry; + +/** + * Created by Philipp Jahoda on 03/11/15. + */ +public interface IRadarDataSet extends ILineRadarDataSet { + + /// flag indicating whether highlight circle should be drawn or not + boolean isDrawHighlightCircleEnabled(); + + /// Sets whether highlight circle should be drawn or not + void setDrawHighlightCircleEnabled(boolean enabled); + + int getHighlightCircleFillColor(); + + /// The stroke color for highlight circle. + /// If Utils.COLOR_NONE, the color of the dataset is taken. + int getHighlightCircleStrokeColor(); + + int getHighlightCircleStrokeAlpha(); + + float getHighlightCircleInnerRadius(); + + float getHighlightCircleOuterRadius(); + + float getHighlightCircleStrokeWidth(); + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IScatterDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IScatterDataSet.java new file mode 100644 index 0000000..ac61227 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IScatterDataSet.java @@ -0,0 +1,38 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.renderer.scatter.IShapeRenderer; + +/** + * Created by philipp on 21/10/15. + */ +public interface IScatterDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the currently set scatter shape size + * + * @return + */ + float getScatterShapeSize(); + + /** + * Returns radius of the hole in the shape + * + * @return + */ + float getScatterShapeHoleRadius(); + + /** + * Returns the color for the hole in the shape + * + * @return + */ + int getScatterShapeHoleColor(); + + /** + * Returns the IShapeRenderer responsible for rendering this DataSet. + * + * @return + */ + IShapeRenderer getShapeRenderer(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedMoveViewJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedMoveViewJob.java new file mode 100644 index 0000000..8f953a0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedMoveViewJob.java @@ -0,0 +1,65 @@ +package com.github.mikephil.charting.jobs; + +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.view.View; + +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +@SuppressLint("NewApi") +public class AnimatedMoveViewJob extends AnimatedViewPortJob { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(4, new AnimatedMoveViewJob(null,0,0,null,null,0,0,0)); + pool.setReplenishPercentage(0.5f); + } + + public static AnimatedMoveViewJob getInstance(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v, float xOrigin, float yOrigin, long duration){ + AnimatedMoveViewJob result = pool.get(); + result.mViewPortHandler = viewPortHandler; + result.xValue = xValue; + result.yValue = yValue; + result.mTrans = trans; + result.view = v; + result.xOrigin = xOrigin; + result.yOrigin = yOrigin; + //result.resetAnimator(); + result.animator.setDuration(duration); + return result; + } + + public static void recycleInstance(AnimatedMoveViewJob instance){ + pool.recycle(instance); + } + + + public AnimatedMoveViewJob(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v, float xOrigin, float yOrigin, long duration) { + super(viewPortHandler, xValue, yValue, trans, v, xOrigin, yOrigin, duration); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + + pts[0] = xOrigin + (xValue - xOrigin) * phase; + pts[1] = yOrigin + (yValue - yOrigin) * phase; + + mTrans.pointValuesToPixel(pts); + mViewPortHandler.centerViewPort(pts, view); + } + + public void recycleSelf(){ + recycleInstance(this); + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new AnimatedMoveViewJob(null,0,0,null,null,0,0,0); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedViewPortJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedViewPortJob.java new file mode 100644 index 0000000..f8b520a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedViewPortJob.java @@ -0,0 +1,99 @@ +package com.github.mikephil.charting.jobs; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.view.View; + +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +@SuppressLint("NewApi") +public abstract class AnimatedViewPortJob extends ViewPortJob implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener { + + protected ObjectAnimator animator; + + protected float phase; + + protected float xOrigin; + protected float yOrigin; + + public AnimatedViewPortJob(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v, float xOrigin, float yOrigin, long duration) { + super(viewPortHandler, xValue, yValue, trans, v); + this.xOrigin = xOrigin; + this.yOrigin = yOrigin; + animator = ObjectAnimator.ofFloat(this, "phase", 0f, 1f); + animator.setDuration(duration); + animator.addUpdateListener(this); + animator.addListener(this); + } + + @SuppressLint("NewApi") + @Override + public void run() { + animator.start(); + } + + public float getPhase() { + return phase; + } + + public void setPhase(float phase) { + this.phase = phase; + } + + public float getXOrigin() { + return xOrigin; + } + + public float getYOrigin() { + return yOrigin; + } + + public abstract void recycleSelf(); + + protected void resetAnimator(){ + animator.removeAllListeners(); + animator.removeAllUpdateListeners(); + animator.reverse(); + animator.addUpdateListener(this); + animator.addListener(this); + } + + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationEnd(Animator animation) { + try{ + recycleSelf(); + }catch (IllegalArgumentException e){ + // don't worry about it. + } + } + + @Override + public void onAnimationCancel(Animator animation) { + try{ + recycleSelf(); + }catch (IllegalArgumentException e){ + // don't worry about it. + } + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedZoomJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedZoomJob.java new file mode 100644 index 0000000..e5e4c41 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedZoomJob.java @@ -0,0 +1,119 @@ +package com.github.mikephil.charting.jobs; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.graphics.Matrix; +import android.view.View; + +import com.github.mikephil.charting.charts.BarLineChartBase; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +@SuppressLint("NewApi") +public class AnimatedZoomJob extends AnimatedViewPortJob implements Animator.AnimatorListener { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(8, new AnimatedZoomJob(null,null,null,null,0,0,0,0,0,0,0,0,0,0)); + } + + public static AnimatedZoomJob getInstance(ViewPortHandler viewPortHandler, View v, Transformer trans, YAxis axis, float xAxisRange, float scaleX, float scaleY, float xOrigin, float yOrigin, float zoomCenterX, float zoomCenterY, float zoomOriginX, float zoomOriginY, long duration) { + AnimatedZoomJob result = pool.get(); + result.mViewPortHandler = viewPortHandler; + result.xValue = scaleX; + result.yValue = scaleY; + result.mTrans = trans; + result.view = v; + result.xOrigin = xOrigin; + result.yOrigin = yOrigin; + result.yAxis = axis; + result.xAxisRange = xAxisRange; + result.resetAnimator(); + result.animator.setDuration(duration); + return result; + } + + protected float zoomOriginX; + protected float zoomOriginY; + + protected float zoomCenterX; + protected float zoomCenterY; + + protected YAxis yAxis; + + protected float xAxisRange; + + @SuppressLint("NewApi") + public AnimatedZoomJob(ViewPortHandler viewPortHandler, View v, Transformer trans, YAxis axis, float xAxisRange, float scaleX, float scaleY, float xOrigin, float yOrigin, float zoomCenterX, float zoomCenterY, float zoomOriginX, float zoomOriginY, long duration) { + super(viewPortHandler, scaleX, scaleY, trans, v, xOrigin, yOrigin, duration); + + this.zoomCenterX = zoomCenterX; + this.zoomCenterY = zoomCenterY; + this.zoomOriginX = zoomOriginX; + this.zoomOriginY = zoomOriginY; + this.animator.addListener(this); + this.yAxis = axis; + this.xAxisRange = xAxisRange; + } + + protected Matrix mOnAnimationUpdateMatrixBuffer = new Matrix(); + @Override + public void onAnimationUpdate(ValueAnimator animation) { + + float scaleX = xOrigin + (xValue - xOrigin) * phase; + float scaleY = yOrigin + (yValue - yOrigin) * phase; + + Matrix save = mOnAnimationUpdateMatrixBuffer; + mViewPortHandler.setZoom(scaleX, scaleY, save); + mViewPortHandler.refresh(save, view, false); + + float valsInView = yAxis.mAxisRange / mViewPortHandler.getScaleY(); + float xsInView = xAxisRange / mViewPortHandler.getScaleX(); + + pts[0] = zoomOriginX + ((zoomCenterX - xsInView / 2f) - zoomOriginX) * phase; + pts[1] = zoomOriginY + ((zoomCenterY + valsInView / 2f) - zoomOriginY) * phase; + + mTrans.pointValuesToPixel(pts); + + mViewPortHandler.translate(pts, save); + mViewPortHandler.refresh(save, view, true); + } + + @Override + public void onAnimationEnd(Animator animation) { + ((BarLineChartBase) view).calculateOffsets(); + view.postInvalidate(); + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + + @Override + public void recycleSelf() { + + } + + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new AnimatedZoomJob(null,null,null,null,0,0,0,0,0,0,0,0,0,0); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/MoveViewJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/MoveViewJob.java new file mode 100644 index 0000000..46b56b1 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/MoveViewJob.java @@ -0,0 +1,56 @@ + +package com.github.mikephil.charting.jobs; + +import android.view.View; + +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +public class MoveViewJob extends ViewPortJob { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(2, new MoveViewJob(null,0,0,null,null)); + pool.setReplenishPercentage(0.5f); + } + + public static MoveViewJob getInstance(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v){ + MoveViewJob result = pool.get(); + result.mViewPortHandler = viewPortHandler; + result.xValue = xValue; + result.yValue = yValue; + result.mTrans = trans; + result.view = v; + return result; + } + + public static void recycleInstance(MoveViewJob instance){ + pool.recycle(instance); + } + + public MoveViewJob(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v) { + super(viewPortHandler, xValue, yValue, trans, v); + } + + @Override + public void run() { + + pts[0] = xValue; + pts[1] = yValue; + + mTrans.pointValuesToPixel(pts); + mViewPortHandler.centerViewPort(pts, view); + + this.recycleInstance(this); + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new MoveViewJob(mViewPortHandler, xValue, yValue, mTrans, view); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ViewPortJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ViewPortJob.java new file mode 100644 index 0000000..c424e4b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ViewPortJob.java @@ -0,0 +1,47 @@ + +package com.github.mikephil.charting.jobs; + +import android.view.View; + +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Runnable that is used for viewport modifications since they cannot be + * executed at any time. This can be used to delay the execution of viewport + * modifications until the onSizeChanged(...) method of the chart-view is called. + * This is especially important if viewport modifying methods are called on the chart + * directly after initialization. + * + * @author Philipp Jahoda + */ +public abstract class ViewPortJob extends ObjectPool.Poolable implements Runnable { + + protected float[] pts = new float[2]; + + protected ViewPortHandler mViewPortHandler; + protected float xValue = 0f; + protected float yValue = 0f; + protected Transformer mTrans; + protected View view; + + public ViewPortJob(ViewPortHandler viewPortHandler, float xValue, float yValue, + Transformer trans, View v) { + + this.mViewPortHandler = viewPortHandler; + this.xValue = xValue; + this.yValue = yValue; + this.mTrans = trans; + this.view = v; + + } + + public float getXValue() { + return xValue; + } + + public float getYValue() { + return yValue; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ZoomJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ZoomJob.java new file mode 100644 index 0000000..c39586c --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ZoomJob.java @@ -0,0 +1,87 @@ + +package com.github.mikephil.charting.jobs; + +import android.graphics.Matrix; +import android.view.View; + +import com.github.mikephil.charting.charts.BarLineChartBase; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +public class ZoomJob extends ViewPortJob { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(1, new ZoomJob(null, 0, 0, 0, 0, null, null, null)); + pool.setReplenishPercentage(0.5f); + } + + public static ZoomJob getInstance(ViewPortHandler viewPortHandler, float scaleX, float scaleY, float xValue, float yValue, + Transformer trans, YAxis.AxisDependency axis, View v) { + ZoomJob result = pool.get(); + result.xValue = xValue; + result.yValue = yValue; + result.scaleX = scaleX; + result.scaleY = scaleY; + result.mViewPortHandler = viewPortHandler; + result.mTrans = trans; + result.axisDependency = axis; + result.view = v; + return result; + } + + public static void recycleInstance(ZoomJob instance) { + pool.recycle(instance); + } + + protected float scaleX; + protected float scaleY; + + protected YAxis.AxisDependency axisDependency; + + public ZoomJob(ViewPortHandler viewPortHandler, float scaleX, float scaleY, float xValue, float yValue, Transformer trans, + YAxis.AxisDependency axis, View v) { + super(viewPortHandler, xValue, yValue, trans, v); + + this.scaleX = scaleX; + this.scaleY = scaleY; + this.axisDependency = axis; + } + + protected Matrix mRunMatrixBuffer = new Matrix(); + + @Override + public void run() { + + Matrix save = mRunMatrixBuffer; + mViewPortHandler.zoom(scaleX, scaleY, save); + mViewPortHandler.refresh(save, view, false); + + float yValsInView = ((BarLineChartBase) view).getAxis(axisDependency).mAxisRange / mViewPortHandler.getScaleY(); + float xValsInView = ((BarLineChartBase) view).getXAxis().mAxisRange / mViewPortHandler.getScaleX(); + + pts[0] = xValue - xValsInView / 2f; + pts[1] = yValue + yValsInView / 2f; + + mTrans.pointValuesToPixel(pts); + + mViewPortHandler.translate(pts, save); + mViewPortHandler.refresh(save, view, false); + + ((BarLineChartBase) view).calculateOffsets(); + view.postInvalidate(); + + recycleInstance(this); + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new ZoomJob(null, 0, 0, 0, 0, null, null, null); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/BarLineChartTouchListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/BarLineChartTouchListener.java new file mode 100644 index 0000000..5685d32 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/BarLineChartTouchListener.java @@ -0,0 +1,698 @@ +package com.github.mikephil.charting.listener; + +import android.annotation.SuppressLint; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.animation.AnimationUtils; + +import com.github.mikephil.charting.charts.BarLineChartBase; +import com.github.mikephil.charting.charts.HorizontalBarChart; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * TouchListener for Bar-, Line-, Scatter- and CandleStickChart with handles all + * touch interaction. Longpress == Zoom out. Double-Tap == Zoom in. + * + * @author Philipp Jahoda + */ +public class BarLineChartTouchListener extends ChartTouchListener>>> { + + /** + * the original touch-matrix from the chart + */ + private Matrix mMatrix = new Matrix(); + + /** + * matrix for saving the original matrix state + */ + private Matrix mSavedMatrix = new Matrix(); + + /** + * point where the touch action started + */ + private MPPointF mTouchStartPoint = MPPointF.getInstance(0,0); + + /** + * center between two pointers (fingers on the display) + */ + private MPPointF mTouchPointCenter = MPPointF.getInstance(0,0); + + private float mSavedXDist = 1f; + private float mSavedYDist = 1f; + private float mSavedDist = 1f; + + private IDataSet mClosestDataSetToTouch; + + /** + * used for tracking velocity of dragging + */ + private VelocityTracker mVelocityTracker; + + private long mDecelerationLastTime = 0; + private MPPointF mDecelerationCurrentPoint = MPPointF.getInstance(0,0); + private MPPointF mDecelerationVelocity = MPPointF.getInstance(0,0); + + /** + * the distance of movement that will be counted as a drag + */ + private float mDragTriggerDist; + + /** + * the minimum distance between the pointers that will trigger a zoom gesture + */ + private float mMinScalePointerDistance; + + /** + * Constructor with initialization parameters. + * + * @param chart instance of the chart + * @param touchMatrix the touch-matrix of the chart + * @param dragTriggerDistance the minimum movement distance that will be interpreted as a "drag" gesture in dp (3dp equals + * to about 9 pixels on a 5.5" FHD screen) + */ + public BarLineChartTouchListener(BarLineChartBase>> chart, Matrix touchMatrix, float dragTriggerDistance) { + super(chart); + this.mMatrix = touchMatrix; + + this.mDragTriggerDist = Utils.convertDpToPixel(dragTriggerDistance); + + this.mMinScalePointerDistance = Utils.convertDpToPixel(3.5f); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + + if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + if (mTouchMode == NONE) { + mGestureDetector.onTouchEvent(event); + } + + if (!mChart.isDragEnabled() && (!mChart.isScaleXEnabled() && !mChart.isScaleYEnabled())) + return true; + + // Handle touch events here... + switch (event.getAction() & MotionEvent.ACTION_MASK) { + + case MotionEvent.ACTION_DOWN: + + startAction(event); + + stopDeceleration(); + + saveTouchStart(event); + + break; + + case MotionEvent.ACTION_POINTER_DOWN: + + if (event.getPointerCount() >= 2) { + + mChart.disableScroll(); + + saveTouchStart(event); + + // get the distance between the pointers on the x-axis + mSavedXDist = getXDist(event); + + // get the distance between the pointers on the y-axis + mSavedYDist = getYDist(event); + + // get the total distance between the pointers + mSavedDist = spacing(event); + + if (mSavedDist > 10f) { + + if (mChart.isPinchZoomEnabled()) { + mTouchMode = PINCH_ZOOM; + } else { + if (mChart.isScaleXEnabled() != mChart.isScaleYEnabled()) { + mTouchMode = mChart.isScaleXEnabled() ? X_ZOOM : Y_ZOOM; + } else { + mTouchMode = mSavedXDist > mSavedYDist ? X_ZOOM : Y_ZOOM; + } + } + } + + // determine the touch-pointer center + midPoint(mTouchPointCenter, event); + } + break; + + case MotionEvent.ACTION_MOVE: + + if (mTouchMode == DRAG) { + + mChart.disableScroll(); + + float x = mChart.isDragXEnabled() ? event.getX() - mTouchStartPoint.x : 0.f; + float y = mChart.isDragYEnabled() ? event.getY() - mTouchStartPoint.y : 0.f; + + performDrag(event, x, y); + + } else if (mTouchMode == X_ZOOM || mTouchMode == Y_ZOOM || mTouchMode == PINCH_ZOOM) { + + mChart.disableScroll(); + + if (mChart.isScaleXEnabled() || mChart.isScaleYEnabled()) + performZoom(event); + + } else if (mTouchMode == NONE + && Math.abs(distance(event.getX(), mTouchStartPoint.x, event.getY(), + mTouchStartPoint.y)) > mDragTriggerDist) { + + if (mChart.isDragEnabled()) { + + boolean shouldPan = !mChart.isFullyZoomedOut() || + !mChart.hasNoDragOffset(); + + if (shouldPan) { + + float distanceX = Math.abs(event.getX() - mTouchStartPoint.x); + float distanceY = Math.abs(event.getY() - mTouchStartPoint.y); + + // Disable dragging in a direction that's disallowed + if ((mChart.isDragXEnabled() || distanceY >= distanceX) && + (mChart.isDragYEnabled() || distanceY <= distanceX)) { + + mLastGesture = ChartGesture.DRAG; + mTouchMode = DRAG; + } + + } else { + + if (mChart.isHighlightPerDragEnabled()) { + mLastGesture = ChartGesture.DRAG; + + if (mChart.isHighlightPerDragEnabled()) + performHighlightDrag(event); + } + } + + } + + } + break; + + case MotionEvent.ACTION_UP: + + final VelocityTracker velocityTracker = mVelocityTracker; + final int pointerId = event.getPointerId(0); + velocityTracker.computeCurrentVelocity(1000, Utils.getMaximumFlingVelocity()); + final float velocityY = velocityTracker.getYVelocity(pointerId); + final float velocityX = velocityTracker.getXVelocity(pointerId); + + if (Math.abs(velocityX) > Utils.getMinimumFlingVelocity() || + Math.abs(velocityY) > Utils.getMinimumFlingVelocity()) { + + if (mTouchMode == DRAG && mChart.isDragDecelerationEnabled()) { + + stopDeceleration(); + + mDecelerationLastTime = AnimationUtils.currentAnimationTimeMillis(); + + mDecelerationCurrentPoint.x = event.getX(); + mDecelerationCurrentPoint.y = event.getY(); + + mDecelerationVelocity.x = velocityX; + mDecelerationVelocity.y = velocityY; + + Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by + // Google + } + } + + if (mTouchMode == X_ZOOM || + mTouchMode == Y_ZOOM || + mTouchMode == PINCH_ZOOM || + mTouchMode == POST_ZOOM) { + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + mChart.calculateOffsets(); + mChart.postInvalidate(); + } + + mTouchMode = NONE; + mChart.enableScroll(); + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + endAction(event); + + break; + case MotionEvent.ACTION_POINTER_UP: + Utils.velocityTrackerPointerUpCleanUpIfNecessary(event, mVelocityTracker); + + mTouchMode = POST_ZOOM; + break; + + case MotionEvent.ACTION_CANCEL: + + mTouchMode = NONE; + endAction(event); + break; + } + + // perform the transformation, update the chart + mMatrix = mChart.getViewPortHandler().refresh(mMatrix, mChart, true); + + return true; // indicate event was handled + } + + /** + * ################ ################ ################ ################ + */ + /** BELOW CODE PERFORMS THE ACTUAL TOUCH ACTIONS */ + + /** + * Saves the current Matrix state and the touch-start point. + * + * @param event + */ + private void saveTouchStart(MotionEvent event) { + + mSavedMatrix.set(mMatrix); + mTouchStartPoint.x = event.getX(); + mTouchStartPoint.y = event.getY(); + + mClosestDataSetToTouch = mChart.getDataSetByTouchPoint(event.getX(), event.getY()); + } + + /** + * Performs all necessary operations needed for dragging. + * + * @param event + */ + private void performDrag(MotionEvent event, float distanceX, float distanceY) { + + mLastGesture = ChartGesture.DRAG; + + mMatrix.set(mSavedMatrix); + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + // check if axis is inverted + if (inverted()) { + + // if there is an inverted horizontalbarchart + if (mChart instanceof HorizontalBarChart) { + distanceX = -distanceX; + } else { + distanceY = -distanceY; + } + } + + mMatrix.postTranslate(distanceX, distanceY); + + if (l != null) + l.onChartTranslate(event, distanceX, distanceY); + } + + /** + * Performs the all operations necessary for pinch and axis zoom. + * + * @param event + */ + private void performZoom(MotionEvent event) { + + if (event.getPointerCount() >= 2) { // two finger zoom + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + // get the distance between the pointers of the touch event + float totalDist = spacing(event); + + if (totalDist > mMinScalePointerDistance) { + + // get the translation + MPPointF t = getTrans(mTouchPointCenter.x, mTouchPointCenter.y); + ViewPortHandler h = mChart.getViewPortHandler(); + + // take actions depending on the activated touch mode + if (mTouchMode == PINCH_ZOOM) { + + mLastGesture = ChartGesture.PINCH_ZOOM; + + float scale = totalDist / mSavedDist; // total scale + + boolean isZoomingOut = (scale < 1); + + boolean canZoomMoreX = isZoomingOut ? + h.canZoomOutMoreX() : + h.canZoomInMoreX(); + + boolean canZoomMoreY = isZoomingOut ? + h.canZoomOutMoreY() : + h.canZoomInMoreY(); + + float scaleX = (mChart.isScaleXEnabled()) ? scale : 1f; + float scaleY = (mChart.isScaleYEnabled()) ? scale : 1f; + + if (canZoomMoreY || canZoomMoreX) { + + mMatrix.set(mSavedMatrix); + mMatrix.postScale(scaleX, scaleY, t.x, t.y); + + if (l != null) + l.onChartScale(event, scaleX, scaleY); + } + + } else if (mTouchMode == X_ZOOM && mChart.isScaleXEnabled()) { + + mLastGesture = ChartGesture.X_ZOOM; + + float xDist = getXDist(event); + float scaleX = xDist / mSavedXDist; // x-axis scale + + boolean isZoomingOut = (scaleX < 1); + boolean canZoomMoreX = isZoomingOut ? + h.canZoomOutMoreX() : + h.canZoomInMoreX(); + + if (canZoomMoreX) { + + mMatrix.set(mSavedMatrix); + mMatrix.postScale(scaleX, 1f, t.x, t.y); + + if (l != null) + l.onChartScale(event, scaleX, 1f); + } + + } else if (mTouchMode == Y_ZOOM && mChart.isScaleYEnabled()) { + + mLastGesture = ChartGesture.Y_ZOOM; + + float yDist = getYDist(event); + float scaleY = yDist / mSavedYDist; // y-axis scale + + boolean isZoomingOut = (scaleY < 1); + boolean canZoomMoreY = isZoomingOut ? + h.canZoomOutMoreY() : + h.canZoomInMoreY(); + + if (canZoomMoreY) { + + mMatrix.set(mSavedMatrix); + mMatrix.postScale(1f, scaleY, t.x, t.y); + + if (l != null) + l.onChartScale(event, 1f, scaleY); + } + } + + MPPointF.recycleInstance(t); + } + } + } + + /** + * Highlights upon dragging, generates callbacks for the selection-listener. + * + * @param e + */ + private void performHighlightDrag(MotionEvent e) { + + Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); + + if (h != null && !h.equalTo(mLastHighlighted)) { + mLastHighlighted = h; + mChart.highlightValue(h, true); + } + } + + /** + * ################ ################ ################ ################ + */ + /** DOING THE MATH BELOW ;-) */ + + + /** + * Determines the center point between two pointer touch points. + * + * @param point + * @param event + */ + private static void midPoint(MPPointF point, MotionEvent event) { + float x = event.getX(0) + event.getX(1); + float y = event.getY(0) + event.getY(1); + point.x = (x / 2f); + point.y = (y / 2f); + } + + /** + * returns the distance between two pointer touch points + * + * @param event + * @return + */ + private static float spacing(MotionEvent event) { + float x = event.getX(0) - event.getX(1); + float y = event.getY(0) - event.getY(1); + return (float) Math.sqrt(x * x + y * y); + } + + /** + * calculates the distance on the x-axis between two pointers (fingers on + * the display) + * + * @param e + * @return + */ + private static float getXDist(MotionEvent e) { + float x = Math.abs(e.getX(0) - e.getX(1)); + return x; + } + + /** + * calculates the distance on the y-axis between two pointers (fingers on + * the display) + * + * @param e + * @return + */ + private static float getYDist(MotionEvent e) { + float y = Math.abs(e.getY(0) - e.getY(1)); + return y; + } + + /** + * Returns a recyclable MPPointF instance. + * returns the correct translation depending on the provided x and y touch + * points + * + * @param x + * @param y + * @return + */ + public MPPointF getTrans(float x, float y) { + + ViewPortHandler vph = mChart.getViewPortHandler(); + + float xTrans = x - vph.offsetLeft(); + float yTrans = 0f; + + // check if axis is inverted + if (inverted()) { + yTrans = -(y - vph.offsetTop()); + } else { + yTrans = -(mChart.getMeasuredHeight() - y - vph.offsetBottom()); + } + + return MPPointF.getInstance(xTrans, yTrans); + } + + /** + * Returns true if the current touch situation should be interpreted as inverted, false if not. + * + * @return + */ + private boolean inverted() { + return (mClosestDataSetToTouch == null && mChart.isAnyAxisInverted()) || (mClosestDataSetToTouch != null + && mChart.isInverted(mClosestDataSetToTouch.getAxisDependency())); + } + + /** + * ################ ################ ################ ################ + */ + /** GETTERS AND GESTURE RECOGNITION BELOW */ + + /** + * returns the matrix object the listener holds + * + * @return + */ + public Matrix getMatrix() { + return mMatrix; + } + + /** + * Sets the minimum distance that will be interpreted as a "drag" by the chart in dp. + * Default: 3dp + * + * @param dragTriggerDistance + */ + public void setDragTriggerDist(float dragTriggerDistance) { + this.mDragTriggerDist = Utils.convertDpToPixel(dragTriggerDistance); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + + mLastGesture = ChartGesture.DOUBLE_TAP; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartDoubleTapped(e); + } + + // check if double-tap zooming is enabled + if (mChart.isDoubleTapToZoomEnabled() && mChart.getData().getEntryCount() > 0) { + + MPPointF trans = getTrans(e.getX(), e.getY()); + + float scaleX = mChart.isScaleXEnabled() ? 1.4f : 1f; + float scaleY = mChart.isScaleYEnabled() ? 1.4f : 1f; + + mChart.zoom(scaleX, scaleY, trans.x, trans.y); + + if (mChart.isLogEnabled()) + Log.i("BarlineChartTouch", "Double-Tap, Zooming In, x: " + trans.x + ", y: " + + trans.y); + + if (l != null) { + l.onChartScale(e, scaleX, scaleY); + } + + MPPointF.recycleInstance(trans); + } + + return super.onDoubleTap(e); + } + + @Override + public void onLongPress(MotionEvent e) { + + mLastGesture = ChartGesture.LONG_PRESS; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + + l.onChartLongPressed(e); + } + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + + mLastGesture = ChartGesture.SINGLE_TAP; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartSingleTapped(e); + } + + if (!mChart.isHighlightPerTapEnabled()) { + return false; + } + + Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); + performHighlight(h, e); + + return super.onSingleTapUp(e); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + + mLastGesture = ChartGesture.FLING; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartFling(e1, e2, velocityX, velocityY); + } + + return super.onFling(e1, e2, velocityX, velocityY); + } + + public void stopDeceleration() { + mDecelerationVelocity.x = 0; + mDecelerationVelocity.y = 0; + } + + public void computeScroll() { + + if (mDecelerationVelocity.x == 0.f && mDecelerationVelocity.y == 0.f) + return; // There's no deceleration in progress + + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + mDecelerationVelocity.x *= mChart.getDragDecelerationFrictionCoef(); + mDecelerationVelocity.y *= mChart.getDragDecelerationFrictionCoef(); + + final float timeInterval = (float) (currentTime - mDecelerationLastTime) / 1000.f; + + float distanceX = mDecelerationVelocity.x * timeInterval; + float distanceY = mDecelerationVelocity.y * timeInterval; + + mDecelerationCurrentPoint.x += distanceX; + mDecelerationCurrentPoint.y += distanceY; + + MotionEvent event = MotionEvent.obtain(currentTime, currentTime, MotionEvent.ACTION_MOVE, mDecelerationCurrentPoint.x, + mDecelerationCurrentPoint.y, 0); + + float dragDistanceX = mChart.isDragXEnabled() ? mDecelerationCurrentPoint.x - mTouchStartPoint.x : 0.f; + float dragDistanceY = mChart.isDragYEnabled() ? mDecelerationCurrentPoint.y - mTouchStartPoint.y : 0.f; + + performDrag(event, dragDistanceX, dragDistanceY); + + event.recycle(); + mMatrix = mChart.getViewPortHandler().refresh(mMatrix, mChart, false); + + mDecelerationLastTime = currentTime; + + if (Math.abs(mDecelerationVelocity.x) >= 0.01 || Math.abs(mDecelerationVelocity.y) >= 0.01) + Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google + else { + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + mChart.calculateOffsets(); + mChart.postInvalidate(); + + stopDeceleration(); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/ChartTouchListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/ChartTouchListener.java new file mode 100644 index 0000000..75c8e86 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/ChartTouchListener.java @@ -0,0 +1,143 @@ +package com.github.mikephil.charting.listener; + +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.highlight.Highlight; + +/** + * Created by philipp on 12/06/15. + */ +public abstract class ChartTouchListener> extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + + public enum ChartGesture { + NONE, DRAG, X_ZOOM, Y_ZOOM, PINCH_ZOOM, ROTATE, SINGLE_TAP, DOUBLE_TAP, LONG_PRESS, FLING + } + + /** + * the last touch gesture that has been performed + **/ + protected ChartGesture mLastGesture = ChartGesture.NONE; + + // states + protected static final int NONE = 0; + protected static final int DRAG = 1; + protected static final int X_ZOOM = 2; + protected static final int Y_ZOOM = 3; + protected static final int PINCH_ZOOM = 4; + protected static final int POST_ZOOM = 5; + protected static final int ROTATE = 6; + + /** + * integer field that holds the current touch-state + */ + protected int mTouchMode = NONE; + + /** + * the last highlighted object (via touch) + */ + protected Highlight mLastHighlighted; + + /** + * the gesturedetector used for detecting taps and longpresses, ... + */ + protected GestureDetector mGestureDetector; + + /** + * the chart the listener represents + */ + protected T mChart; + + public ChartTouchListener(T chart) { + this.mChart = chart; + + mGestureDetector = new GestureDetector(chart.getContext(), this); + } + + /** + * Calls the OnChartGestureListener to do the start callback + * + * @param me + */ + public void startAction(MotionEvent me) { + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) + l.onChartGestureStart(me, mLastGesture); + } + + /** + * Calls the OnChartGestureListener to do the end callback + * + * @param me + */ + public void endAction(MotionEvent me) { + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) + l.onChartGestureEnd(me, mLastGesture); + } + + /** + * Sets the last value that was highlighted via touch. + * + * @param high + */ + public void setLastHighlighted(Highlight high) { + mLastHighlighted = high; + } + + /** + * returns the touch mode the listener is currently in + * + * @return + */ + public int getTouchMode() { + return mTouchMode; + } + + /** + * Returns the last gesture that has been performed on the chart. + * + * @return + */ + public ChartGesture getLastGesture() { + return mLastGesture; + } + + + /** + * Perform a highlight operation. + * + * @param e + */ + protected void performHighlight(Highlight h, MotionEvent e) { + + if (h == null || h.equalTo(mLastHighlighted)) { + mChart.highlightValue(null, true); + mLastHighlighted = null; + } else { + mChart.highlightValue(h, true); + mLastHighlighted = h; + } + } + + /** + * returns the distance between two points + * + * @param eventX + * @param startX + * @param eventY + * @param startY + * @return + */ + protected static float distance(float eventX, float startX, float eventY, float startY) { + float dx = eventX - startX; + float dy = eventY - startY; + return (float) Math.sqrt(dx * dx + dy * dy); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartGestureListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartGestureListener.java new file mode 100644 index 0000000..da0c5ed --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartGestureListener.java @@ -0,0 +1,76 @@ +package com.github.mikephil.charting.listener; + +import android.view.MotionEvent; + +/** + * Listener for callbacks when doing gestures on the chart. + * + * @author Philipp Jahoda + */ +public interface OnChartGestureListener { + + /** + * Callbacks when a touch-gesture has started on the chart (ACTION_DOWN) + * + * @param me + * @param lastPerformedGesture + */ + void onChartGestureStart(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture); + + /** + * Callbacks when a touch-gesture has ended on the chart (ACTION_UP, ACTION_CANCEL) + * + * @param me + * @param lastPerformedGesture + */ + void onChartGestureEnd(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture); + + /** + * Callbacks when the chart is longpressed. + * + * @param me + */ + void onChartLongPressed(MotionEvent me); + + /** + * Callbacks when the chart is double-tapped. + * + * @param me + */ + void onChartDoubleTapped(MotionEvent me); + + /** + * Callbacks when the chart is single-tapped. + * + * @param me + */ + void onChartSingleTapped(MotionEvent me); + + /** + * Callbacks then a fling gesture is made on the chart. + * + * @param me1 + * @param me2 + * @param velocityX + * @param velocityY + */ + void onChartFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY); + + /** + * Callbacks when the chart is scaled / zoomed via pinch zoom / double-tap gesture. + * + * @param me + * @param scaleX scalefactor on the x-axis + * @param scaleY scalefactor on the y-axis + */ + void onChartScale(MotionEvent me, float scaleX, float scaleY); + + /** + * Callbacks when the chart is moved / translated via drag gesture. + * + * @param me + * @param dX translation distance on the x-axis + * @param dY translation distance on the y-axis + */ + void onChartTranslate(MotionEvent me, float dX, float dY); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartValueSelectedListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartValueSelectedListener.java new file mode 100644 index 0000000..7f50232 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartValueSelectedListener.java @@ -0,0 +1,27 @@ +package com.github.mikephil.charting.listener; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; + +/** + * Listener for callbacks when selecting values inside the chart by + * touch-gesture. + * + * @author Philipp Jahoda + */ +public interface OnChartValueSelectedListener { + + /** + * Called when a value has been selected inside the chart. + * + * @param e The selected Entry + * @param h The corresponding highlight object that contains information + * about the highlighted position such as dataSetIndex, ... + */ + void onValueSelected(Entry e, Highlight h); + + /** + * Called when nothing has been selected or an "un-select" has been made. + */ + void onNothingSelected(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawLineChartTouchListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawLineChartTouchListener.java new file mode 100644 index 0000000..ca3a67f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawLineChartTouchListener.java @@ -0,0 +1,15 @@ +package com.github.mikephil.charting.listener; + +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; + +public class OnDrawLineChartTouchListener extends SimpleOnGestureListener implements OnTouchListener { + + @Override + public boolean onTouch(View v, MotionEvent event) { + return false; + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawListener.java new file mode 100644 index 0000000..5890350 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawListener.java @@ -0,0 +1,38 @@ +package com.github.mikephil.charting.listener; + +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; + +/** + * Listener for callbacks when drawing on the chart. + * + * @author Philipp + * + */ +public interface OnDrawListener { + + /** + * Called whenever an entry is added with the finger. Note this is also called for entries that are generated by the + * library, when the touch gesture is too fast and skips points. + * + * @param entry + * the last drawn entry + */ + void onEntryAdded(Entry entry); + + /** + * Called whenever an entry is moved by the user after beeing highlighted + * + * @param entry + */ + void onEntryMoved(Entry entry); + + /** + * Called when drawing finger is lifted and the draw is finished. + * + * @param dataSet + * the last drawn DataSet + */ + void onDrawFinished(DataSet dataSet); + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/PieRadarChartTouchListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/PieRadarChartTouchListener.java new file mode 100644 index 0000000..d3527f9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/PieRadarChartTouchListener.java @@ -0,0 +1,288 @@ + +package com.github.mikephil.charting.listener; + +import android.annotation.SuppressLint; +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AnimationUtils; + +import com.github.mikephil.charting.charts.PieRadarChartBase; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; + +/** + * Touchlistener for the PieChart. + * + * @author Philipp Jahoda + */ +public class PieRadarChartTouchListener extends ChartTouchListener> { + + private MPPointF mTouchStartPoint = MPPointF.getInstance(0,0); + + /** + * the angle where the dragging started + */ + private float mStartAngle = 0f; + + private ArrayList _velocitySamples = new ArrayList(); + + private long mDecelerationLastTime = 0; + private float mDecelerationAngularVelocity = 0.f; + + public PieRadarChartTouchListener(PieRadarChartBase chart) { + super(chart); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + + if (mGestureDetector.onTouchEvent(event)) + return true; + + // if rotation by touch is enabled + // TODO: Also check if the pie itself is being touched, rather than the entire chart area + if (mChart.isRotationEnabled()) { + + float x = event.getX(); + float y = event.getY(); + + switch (event.getAction()) { + + case MotionEvent.ACTION_DOWN: + + startAction(event); + + stopDeceleration(); + + resetVelocity(); + + if (mChart.isDragDecelerationEnabled()) + sampleVelocity(x, y); + + setGestureStartAngle(x, y); + mTouchStartPoint.x = x; + mTouchStartPoint.y = y; + + break; + case MotionEvent.ACTION_MOVE: + + if (mChart.isDragDecelerationEnabled()) + sampleVelocity(x, y); + + if (mTouchMode == NONE + && distance(x, mTouchStartPoint.x, y, mTouchStartPoint.y) + > Utils.convertDpToPixel(8f)) { + mLastGesture = ChartGesture.ROTATE; + mTouchMode = ROTATE; + mChart.disableScroll(); + } else if (mTouchMode == ROTATE) { + updateGestureRotation(x, y); + mChart.invalidate(); + } + + endAction(event); + + break; + case MotionEvent.ACTION_UP: + + if (mChart.isDragDecelerationEnabled()) { + + stopDeceleration(); + + sampleVelocity(x, y); + + mDecelerationAngularVelocity = calculateVelocity(); + + if (mDecelerationAngularVelocity != 0.f) { + mDecelerationLastTime = AnimationUtils.currentAnimationTimeMillis(); + + Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google + } + } + + mChart.enableScroll(); + mTouchMode = NONE; + + endAction(event); + + break; + } + } + + return true; + } + + @Override + public void onLongPress(MotionEvent me) { + + mLastGesture = ChartGesture.LONG_PRESS; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartLongPressed(me); + } + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + + mLastGesture = ChartGesture.SINGLE_TAP; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartSingleTapped(e); + } + + if(!mChart.isHighlightPerTapEnabled()) { + return false; + } + + Highlight high = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); + performHighlight(high, e); + + return true; + } + + private void resetVelocity() { + _velocitySamples.clear(); + } + + private void sampleVelocity(float touchLocationX, float touchLocationY) { + + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + _velocitySamples.add(new AngularVelocitySample(currentTime, mChart.getAngleForPoint(touchLocationX, touchLocationY))); + + // Remove samples older than our sample time - 1 seconds + for (int i = 0, count = _velocitySamples.size(); i < count - 2; i++) { + if (currentTime - _velocitySamples.get(i).time > 1000) { + _velocitySamples.remove(0); + i--; + count--; + } else { + break; + } + } + } + + private float calculateVelocity() { + + if (_velocitySamples.isEmpty()) + return 0.f; + + AngularVelocitySample firstSample = _velocitySamples.get(0); + AngularVelocitySample lastSample = _velocitySamples.get(_velocitySamples.size() - 1); + + // Look for a sample that's closest to the latest sample, but not the same, so we can deduce the direction + AngularVelocitySample beforeLastSample = firstSample; + for (int i = _velocitySamples.size() - 1; i >= 0; i--) { + beforeLastSample = _velocitySamples.get(i); + if (beforeLastSample.angle != lastSample.angle) { + break; + } + } + + // Calculate the sampling time + float timeDelta = (lastSample.time - firstSample.time) / 1000.f; + if (timeDelta == 0.f) { + timeDelta = 0.1f; + } + + // Calculate clockwise/ccw by choosing two values that should be closest to each other, + // so if the angles are two far from each other we know they are inverted "for sure" + boolean clockwise = lastSample.angle >= beforeLastSample.angle; + if (Math.abs(lastSample.angle - beforeLastSample.angle) > 270.0) { + clockwise = !clockwise; + } + + // Now if the "gesture" is over a too big of an angle - then we know the angles are inverted, and we need to move them closer to each other from both sides of the 360.0 wrapping point + if (lastSample.angle - firstSample.angle > 180.0) { + firstSample.angle += 360.0; + } else if (firstSample.angle - lastSample.angle > 180.0) { + lastSample.angle += 360.0; + } + + // The velocity + float velocity = Math.abs((lastSample.angle - firstSample.angle) / timeDelta); + + // Direction? + if (!clockwise) { + velocity = -velocity; + } + + return velocity; + } + + /** + * sets the starting angle of the rotation, this is only used by the touch + * listener, x and y is the touch position + * + * @param x + * @param y + */ + public void setGestureStartAngle(float x, float y) { + mStartAngle = mChart.getAngleForPoint(x, y) - mChart.getRawRotationAngle(); + } + + /** + * updates the view rotation depending on the given touch position, also + * takes the starting angle into consideration + * + * @param x + * @param y + */ + public void updateGestureRotation(float x, float y) { + mChart.setRotationAngle(mChart.getAngleForPoint(x, y) - mStartAngle); + } + + /** + * Sets the deceleration-angular-velocity to 0f + */ + public void stopDeceleration() { + mDecelerationAngularVelocity = 0.f; + } + + public void computeScroll() { + + if (mDecelerationAngularVelocity == 0.f) + return; // There's no deceleration in progress + + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + mDecelerationAngularVelocity *= mChart.getDragDecelerationFrictionCoef(); + + final float timeInterval = (float) (currentTime - mDecelerationLastTime) / 1000.f; + + mChart.setRotationAngle(mChart.getRotationAngle() + mDecelerationAngularVelocity * timeInterval); + + mDecelerationLastTime = currentTime; + + if (Math.abs(mDecelerationAngularVelocity) >= 0.001) + Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google + else + stopDeceleration(); + } + + private class AngularVelocitySample { + + public long time; + public float angle; + + public AngularVelocitySample(long time, float angle) { + this.time = time; + this.angle = angle; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/matrix/Vector3.java b/MPChartLib/src/main/java/com/github/mikephil/charting/matrix/Vector3.java new file mode 100644 index 0000000..6ff9a62 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/matrix/Vector3.java @@ -0,0 +1,136 @@ + +package com.github.mikephil.charting.matrix; + +/** + * Simple 3D vector class. Handles basic vector math for 3D vectors. + */ +public final class Vector3 { + public float x; + public float y; + public float z; + + public static final Vector3 ZERO = new Vector3(0, 0, 0); + public static final Vector3 UNIT_X = new Vector3(1, 0, 0); + public static final Vector3 UNIT_Y = new Vector3(0, 1, 0); + public static final Vector3 UNIT_Z = new Vector3(0, 0, 1); + + public Vector3() { + } + + public Vector3(float[] array) + { + set(array[0], array[1], array[2]); + } + + public Vector3(float xValue, float yValue, float zValue) { + set(xValue, yValue, zValue); + } + + public Vector3(Vector3 other) { + set(other); + } + + public final void add(Vector3 other) { + x += other.x; + y += other.y; + z += other.z; + } + + public final void add(float otherX, float otherY, float otherZ) { + x += otherX; + y += otherY; + z += otherZ; + } + + public final void subtract(Vector3 other) { + x -= other.x; + y -= other.y; + z -= other.z; + } + + public final void subtractMultiple(Vector3 other, float multiplicator) + { + x -= other.x * multiplicator; + y -= other.y * multiplicator; + z -= other.z * multiplicator; + } + + public final void multiply(float magnitude) { + x *= magnitude; + y *= magnitude; + z *= magnitude; + } + + public final void multiply(Vector3 other) { + x *= other.x; + y *= other.y; + z *= other.z; + } + + public final void divide(float magnitude) { + if (magnitude != 0.0f) { + x /= magnitude; + y /= magnitude; + z /= magnitude; + } + } + + public final void set(Vector3 other) { + x = other.x; + y = other.y; + z = other.z; + } + + public final void set(float xValue, float yValue, float zValue) { + x = xValue; + y = yValue; + z = zValue; + } + + public final float dot(Vector3 other) { + return (x * other.x) + (y * other.y) + (z * other.z); + } + + public final Vector3 cross(Vector3 other) { + return new Vector3(y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x); + } + + public final float length() { + return (float) Math.sqrt(length2()); + } + + public final float length2() { + return (x * x) + (y * y) + (z * z); + } + + public final float distance2(Vector3 other) { + float dx = x - other.x; + float dy = y - other.y; + float dz = z - other.z; + return (dx * dx) + (dy * dy) + (dz * dz); + } + + public final float normalize() { + final float magnitude = length(); + + // TODO: I'm choosing safety over speed here. + if (magnitude != 0.0f) { + x /= magnitude; + y /= magnitude; + z /= magnitude; + } + + return magnitude; + } + + public final void zero() { + set(0.0f, 0.0f, 0.0f); + } + + public final boolean pointsInSameDirection(Vector3 other) { + return this.dot(other) > 0; + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/model/GradientColor.java b/MPChartLib/src/main/java/com/github/mikephil/charting/model/GradientColor.java new file mode 100644 index 0000000..b5c8715 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/model/GradientColor.java @@ -0,0 +1,69 @@ +package com.github.mikephil.charting.model; + +import com.github.mikephil.charting.utils.Fill; + +/** + * Deprecated. Use `Fill` + */ +@Deprecated +public class GradientColor extends Fill +{ + /** + * Deprecated. Use `Fill.getGradientColors()` + */ + @Deprecated + public int getStartColor() + { + return getGradientColors()[0]; + } + + /** + * Deprecated. Use `Fill.setGradientColors(...)` + */ + @Deprecated + public void setStartColor(int startColor) + { + if (getGradientColors() == null || getGradientColors().length != 2) + { + setGradientColors(new int[]{ + startColor, + getGradientColors() != null && getGradientColors().length > 1 + ? getGradientColors()[1] + : 0 + }); + } else + { + getGradientColors()[0] = startColor; + } + } + + /** + * Deprecated. Use `Fill.getGradientColors()` + */ + @Deprecated + public int getEndColor() + { + return getGradientColors()[1]; + } + + /** + * Deprecated. Use `Fill.setGradientColors(...)` + */ + @Deprecated + public void setEndColor(int endColor) + { + if (getGradientColors() == null || getGradientColors().length != 2) + { + setGradientColors(new int[]{ + getGradientColors() != null && getGradientColors().length > 0 + ? getGradientColors()[0] + : 0, + endColor + }); + } else + { + getGradientColors()[1] = endColor; + } + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/AxisRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/AxisRenderer.java new file mode 100644 index 0000000..72ea2d1 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/AxisRenderer.java @@ -0,0 +1,293 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Baseclass of all axis renderers. + * + * @author Philipp Jahoda + */ +public abstract class AxisRenderer extends Renderer { + + /** base axis this axis renderer works with */ + protected AxisBase mAxis; + + /** transformer to transform values to screen pixels and return */ + protected Transformer mTrans; + + /** + * paint object for the grid lines + */ + protected Paint mGridPaint; + + /** + * paint for the x-label values + */ + protected Paint mAxisLabelPaint; + + /** + * paint for the line surrounding the chart + */ + protected Paint mAxisLinePaint; + + /** + * paint used for the limit lines + */ + protected Paint mLimitLinePaint; + + public AxisRenderer(ViewPortHandler viewPortHandler, Transformer trans, AxisBase axis) { + super(viewPortHandler); + + this.mTrans = trans; + this.mAxis = axis; + + if(mViewPortHandler != null) { + + mAxisLabelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mGridPaint = new Paint(); + mGridPaint.setColor(Color.GRAY); + mGridPaint.setStrokeWidth(1f); + mGridPaint.setStyle(Style.STROKE); + mGridPaint.setAlpha(90); + + mAxisLinePaint = new Paint(); + mAxisLinePaint.setColor(Color.BLACK); + mAxisLinePaint.setStrokeWidth(1f); + mAxisLinePaint.setStyle(Style.STROKE); + + mLimitLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mLimitLinePaint.setStyle(Paint.Style.STROKE); + } + } + + /** + * Returns the Paint object used for drawing the axis (labels). + * + * @return + */ + public Paint getPaintAxisLabels() { + return mAxisLabelPaint; + } + + /** + * Returns the Paint object that is used for drawing the grid-lines of the + * axis. + * + * @return + */ + public Paint getPaintGrid() { + return mGridPaint; + } + + /** + * Returns the Paint object that is used for drawing the axis-line that goes + * alongside the axis. + * + * @return + */ + public Paint getPaintAxisLine() { + return mAxisLinePaint; + } + + /** + * Returns the Transformer object used for transforming the axis values. + * + * @return + */ + public Transformer getTransformer() { + return mTrans; + } + + /** + * Computes the axis values. + * + * @param min - the minimum value in the data object for this axis + * @param max - the maximum value in the data object for this axis + */ + public void computeAxis(float min, float max, boolean inverted) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (mViewPortHandler != null && mViewPortHandler.contentWidth() > 10 && !mViewPortHandler.isFullyZoomedOutY()) { + + MPPointD p1 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop()); + MPPointD p2 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentBottom()); + + if (!inverted) { + + min = (float) p2.y; + max = (float) p1.y; + } else { + + min = (float) p1.y; + max = (float) p2.y; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + computeAxisValues(min, max); + } + + /** + * Sets up the axis values. Computes the desired number of labels between the two given extremes. + * + * @return + */ + protected void computeAxisValues(float min, float max) { + + float yMin = min; + float yMax = max; + + int labelCount = mAxis.getLabelCount(); + double range = Math.abs(yMax - yMin); + + if (labelCount == 0 || range <= 0 || Double.isInfinite(range)) { + mAxis.mEntries = new float[]{}; + mAxis.mCenteredEntries = new float[]{}; + mAxis.mEntryCount = 0; + return; + } + + // Find out how much spacing (in y value space) between axis values + double rawInterval = range / labelCount; + double interval = Utils.roundToNextSignificant(rawInterval); + + // If granularity is enabled, then do not allow the interval to go below specified granularity. + // This is used to avoid repeated values when rounding values for display. + if (mAxis.isGranularityEnabled()) + interval = interval < mAxis.getGranularity() ? mAxis.getGranularity() : interval; + + // Normalize interval + double intervalMagnitude = Utils.roundToNextSignificant(Math.pow(10, (int) Math.log10(interval))); + int intervalSigDigit = (int) (interval / intervalMagnitude); + if (intervalSigDigit > 5) { + // Use one order of magnitude higher, to avoid intervals like 0.9 or 90 + // if it's 0.0 after floor(), we use the old value + interval = Math.floor(10.0 * intervalMagnitude) == 0.0 + ? interval + : Math.floor(10.0 * intervalMagnitude); + + } + + int n = mAxis.isCenterAxisLabelsEnabled() ? 1 : 0; + + // force label count + if (mAxis.isForceLabelsEnabled()) { + + interval = (float) range / (float) (labelCount - 1); + mAxis.mEntryCount = labelCount; + + if (mAxis.mEntries.length < labelCount) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = new float[labelCount]; + } + + float v = min; + + for (int i = 0; i < labelCount; i++) { + mAxis.mEntries[i] = v; + v += interval; + } + + n = labelCount; + + // no forced count + } else { + + double first = interval == 0.0 ? 0.0 : Math.ceil(yMin / interval) * interval; + if(mAxis.isCenterAxisLabelsEnabled()) { + first -= interval; + } + + double last = interval == 0.0 ? 0.0 : Utils.nextUp(Math.floor(yMax / interval) * interval); + + double f; + int i; + + if (interval != 0.0 && last != first) { + for (f = first; f <= last; f += interval) { + ++n; + } + } + else if (last == first && n == 0) { + n = 1; + } + + mAxis.mEntryCount = n; + + if (mAxis.mEntries.length < n) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = new float[n]; + } + + for (f = first, i = 0; i < n; f += interval, ++i) { + + if (f == 0.0) // Fix for negative zero case (Where value == -0.0, and 0.0 == -0.0) + f = 0.0; + + mAxis.mEntries[i] = (float) f; + } + } + + // set decimals + if (interval < 1) { + mAxis.mDecimals = (int) Math.ceil(-Math.log10(interval)); + } else { + mAxis.mDecimals = 0; + } + + if (mAxis.isCenterAxisLabelsEnabled()) { + + if (mAxis.mCenteredEntries.length < n) { + mAxis.mCenteredEntries = new float[n]; + } + + float offset = (float)interval / 2f; + + for (int i = 0; i < n; i++) { + mAxis.mCenteredEntries[i] = mAxis.mEntries[i] + offset; + } + } + } + + /** + * Draws the axis labels to the screen. + * + * @param c + */ + public abstract void renderAxisLabels(Canvas c); + + /** + * Draws the grid lines belonging to the axis. + * + * @param c + */ + public abstract void renderGridLines(Canvas c); + + /** + * Draws the line that goes alongside the axis. + * + * @param c + */ + public abstract void renderAxisLine(Canvas c); + + /** + * Draws the LimitLines associated with this axis to the screen. + * + * @param c + */ + public abstract void renderLimitLines(Canvas c); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java new file mode 100644 index 0000000..1656a3a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java @@ -0,0 +1,498 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.buffer.BarBuffer; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.highlight.Range; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.Fill; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class BarChartRenderer extends BarLineScatterCandleBubbleRenderer { + + protected BarDataProvider mChart; + + /** + * the rect object that is used for drawing the bars + */ + protected RectF mBarRect = new RectF(); + + protected BarBuffer[] mBarBuffers; + + protected Paint mShadowPaint; + protected Paint mBarBorderPaint; + + public BarChartRenderer(BarDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + this.mChart = chart; + + mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mHighlightPaint.setStyle(Paint.Style.FILL); + mHighlightPaint.setColor(Color.rgb(0, 0, 0)); + // set alpha after color + mHighlightPaint.setAlpha(120); + + mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mShadowPaint.setStyle(Paint.Style.FILL); + + mBarBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBarBorderPaint.setStyle(Paint.Style.STROKE); + } + + @Override + public void initBuffers() { + + BarData barData = mChart.getBarData(); + mBarBuffers = new BarBuffer[barData.getDataSetCount()]; + + for (int i = 0; i < mBarBuffers.length; i++) { + IBarDataSet set = barData.getDataSetByIndex(i); + mBarBuffers[i] = new BarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1), + barData.getDataSetCount(), set.isStacked()); + } + } + + @Override + public void drawData(Canvas c) { + + BarData barData = mChart.getBarData(); + + for (int i = 0; i < barData.getDataSetCount(); i++) { + + IBarDataSet set = barData.getDataSetByIndex(i); + + if (set.isVisible()) { + drawDataSet(c, set, i); + } + } + } + + private RectF mBarShadowRectBuffer = new RectF(); + + protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mBarBorderPaint.setColor(dataSet.getBarBorderColor()); + mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); + + final boolean drawBorder = dataSet.getBarBorderWidth() > 0.f; + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + // draw the bar shadow before the values + if (mChart.isDrawBarShadowEnabled()) { + mShadowPaint.setColor(dataSet.getBarShadowColor()); + + BarData barData = mChart.getBarData(); + + final float barWidth = barData.getBarWidth(); + final float barWidthHalf = barWidth / 2.0f; + float x; + + for (int i = 0, count = Math.min((int)(Math.ceil((float)(dataSet.getEntryCount()) * phaseX)), dataSet.getEntryCount()); + i < count; + i++) { + + BarEntry e = dataSet.getEntryForIndex(i); + + x = e.getX(); + + mBarShadowRectBuffer.left = x - barWidthHalf; + mBarShadowRectBuffer.right = x + barWidthHalf; + + trans.rectValueToPixel(mBarShadowRectBuffer); + + if (!mViewPortHandler.isInBoundsLeft(mBarShadowRectBuffer.right)) + continue; + + if (!mViewPortHandler.isInBoundsRight(mBarShadowRectBuffer.left)) + break; + + mBarShadowRectBuffer.top = mViewPortHandler.contentTop(); + mBarShadowRectBuffer.bottom = mViewPortHandler.contentBottom(); + + c.drawRect(mBarShadowRectBuffer, mShadowPaint); + } + } + + // initialize the buffer + BarBuffer buffer = mBarBuffers[index]; + buffer.setPhases(phaseX, phaseY); + buffer.setDataSet(index); + buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency())); + buffer.setBarWidth(mChart.getBarData().getBarWidth()); + + buffer.feed(dataSet); + + trans.pointValuesToPixel(buffer.buffer); + + final boolean isCustomFill = dataSet.getFills() != null && !dataSet.getFills().isEmpty(); + final boolean isSingleColor = dataSet.getColors().size() == 1; + final boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + + if (isSingleColor) { + mRenderPaint.setColor(dataSet.getColor()); + } + + for (int j = 0, pos = 0; j < buffer.size(); j += 4, pos++) { + + if (!mViewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) + continue; + + if (!mViewPortHandler.isInBoundsRight(buffer.buffer[j])) + break; + + if (!isSingleColor) { + // Set the color for the currently drawn value. If the index + // is out of bounds, reuse colors. + mRenderPaint.setColor(dataSet.getColor(pos)); + } + + if (isCustomFill) { + dataSet.getFill(pos) + .fillRect( + c, mRenderPaint, + buffer.buffer[j], + buffer.buffer[j + 1], + buffer.buffer[j + 2], + buffer.buffer[j + 3], + isInverted ? Fill.Direction.DOWN : Fill.Direction.UP); + } + else { + c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mRenderPaint); + } + + if (drawBorder) { + c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mBarBorderPaint); + } + } + } + + protected void prepareBarHighlight(float x, float y1, float y2, float barWidthHalf, Transformer trans) { + + float left = x - barWidthHalf; + float right = x + barWidthHalf; + float top = y1; + float bottom = y2; + + mBarRect.set(left, top, right, bottom); + + trans.rectToPixelPhase(mBarRect, mAnimator.getPhaseY()); + } + + @Override + public void drawValues(Canvas c) { + + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getBarData().getDataSets(); + + final float valueOffsetPlus = Utils.convertDpToPixel(4.5f); + float posOffset = 0f; + float negOffset = 0f; + boolean drawValueAboveBar = mChart.isDrawValueAboveBarEnabled(); + + for (int i = 0; i < mChart.getBarData().getDataSetCount(); i++) { + + IBarDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet)) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + + // calculate the correct offset depending on the draw position of + // the value + float valueTextHeight = Utils.calcTextHeight(mValuePaint, "8"); + posOffset = (drawValueAboveBar ? -valueOffsetPlus : valueTextHeight + valueOffsetPlus); + negOffset = (drawValueAboveBar ? valueTextHeight + valueOffsetPlus : -valueOffsetPlus); + + if (isInverted) { + posOffset = -posOffset - valueTextHeight; + negOffset = -negOffset - valueTextHeight; + } + + // get the buffer + BarBuffer buffer = mBarBuffers[i]; + + final float phaseY = mAnimator.getPhaseY(); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + // if only single values are drawn (sum) + if (!dataSet.isStacked()) { + + for (int j = 0; j < buffer.buffer.length * mAnimator.getPhaseX(); j += 4) { + + float x = (buffer.buffer[j] + buffer.buffer[j + 2]) / 2f; + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsY(buffer.buffer[j + 1]) + || !mViewPortHandler.isInBoundsLeft(x)) + continue; + + BarEntry entry = dataSet.getEntryForIndex(j / 4); + float val = entry.getY(); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, dataSet.getValueFormatter(), val, entry, i, x, + val >= 0 ? + (buffer.buffer[j + 1] + posOffset) : + (buffer.buffer[j + 3] + negOffset), + dataSet.getValueTextColor(j / 4)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float px = x; + float py = val >= 0 ? + (buffer.buffer[j + 1] + posOffset) : + (buffer.buffer[j + 3] + negOffset); + + px += iconsOffset.x; + py += iconsOffset.y; + + Utils.drawImage( + c, + icon, + (int)px, + (int)py, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + // if we have stacks + } else { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + int bufferIndex = 0; + int index = 0; + + while (index < dataSet.getEntryCount() * mAnimator.getPhaseX()) { + + BarEntry entry = dataSet.getEntryForIndex(index); + + float[] vals = entry.getYVals(); + float x = (buffer.buffer[bufferIndex] + buffer.buffer[bufferIndex + 2]) / 2f; + + int color = dataSet.getValueTextColor(index); + + // we still draw stacked bars, but there is one + // non-stacked + // in between + if (vals == null) { + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsY(buffer.buffer[bufferIndex + 1]) + || !mViewPortHandler.isInBoundsLeft(x)) + continue; + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, dataSet.getValueFormatter(), entry.getY(), entry, i, x, + buffer.buffer[bufferIndex + 1] + + (entry.getY() >= 0 ? posOffset : negOffset), + color); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float px = x; + float py = buffer.buffer[bufferIndex + 1] + + (entry.getY() >= 0 ? posOffset : negOffset); + + px += iconsOffset.x; + py += iconsOffset.y; + + Utils.drawImage( + c, + icon, + (int)px, + (int)py, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + + // draw stack values + } else { + + float[] transformed = new float[vals.length * 2]; + + float posY = 0f; + float negY = -entry.getNegativeSum(); + + for (int k = 0, idx = 0; k < transformed.length; k += 2, idx++) { + + float value = vals[idx]; + float y; + + if (value == 0.0f && (posY == 0.0f || negY == 0.0f)) { + // Take care of the situation of a 0.0 value, which overlaps a non-zero bar + y = value; + } else if (value >= 0.0f) { + posY += value; + y = posY; + } else { + y = negY; + negY -= value; + } + + transformed[k + 1] = y * phaseY; + } + + trans.pointValuesToPixel(transformed); + + for (int k = 0; k < transformed.length; k += 2) { + + final float val = vals[k / 2]; + final boolean drawBelow = + (val == 0.0f && negY == 0.0f && posY > 0.0f) || + val < 0.0f; + float y = transformed[k + 1] + + (drawBelow ? negOffset : posOffset); + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsY(y) + || !mViewPortHandler.isInBoundsLeft(x)) + continue; + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + dataSet.getValueFormatter(), + vals[k / 2], + entry, + i, + x, + y, + color); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + } + + bufferIndex = vals == null ? bufferIndex + 4 : bufferIndex + 4 * vals.length; + index++; + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + BarData barData = mChart.getBarData(); + + for (Highlight high : indices) { + + IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + BarEntry e = set.getEntryForXValue(high.getX(), high.getY()); + + if (!isInBoundsX(e, set)) + continue; + + Transformer trans = mChart.getTransformer(set.getAxisDependency()); + + mHighlightPaint.setColor(set.getHighLightColor()); + mHighlightPaint.setAlpha(set.getHighLightAlpha()); + + boolean isStack = (high.getStackIndex() >= 0 && e.isStacked()) ? true : false; + + final float y1; + final float y2; + + if (isStack) { + + if(mChart.isHighlightFullBarEnabled()) { + + y1 = e.getPositiveSum(); + y2 = -e.getNegativeSum(); + + } else { + + Range range = e.getRanges()[high.getStackIndex()]; + + y1 = range.from; + y2 = range.to; + } + + } else { + y1 = e.getY(); + y2 = 0.f; + } + + prepareBarHighlight(e.getX(), y1, y2, barData.getBarWidth() / 2f, trans); + + setHighlightDrawPos(high, mBarRect); + + c.drawRect(mBarRect, mHighlightPaint); + } + } + + /** + * Sets the drawing position of the highlight object based on the riven bar-rect. + * @param high + */ + protected void setHighlightDrawPos(Highlight high, RectF bar) { + high.setDraw(bar.centerX(), bar.top); + } + + @Override + public void drawExtras(Canvas c) { + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarLineScatterCandleBubbleRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarLineScatterCandleBubbleRenderer.java new file mode 100644 index 0000000..0659918 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarLineScatterCandleBubbleRenderer.java @@ -0,0 +1,96 @@ +package com.github.mikephil.charting.renderer; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 09/06/16. + */ +public abstract class BarLineScatterCandleBubbleRenderer extends DataRenderer { + + /** + * buffer for storing the current minimum and maximum visible x + */ + protected XBounds mXBounds = new XBounds(); + + public BarLineScatterCandleBubbleRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + } + + /** + * Returns true if the DataSet values should be drawn, false if not. + * + * @param set + * @return + */ + protected boolean shouldDrawValues(IDataSet set) { + return set.isVisible() && (set.isDrawValuesEnabled() || set.isDrawIconsEnabled()); + } + + /** + * Checks if the provided entry object is in bounds for drawing considering the current animation phase. + * + * @param e + * @param set + * @return + */ + protected boolean isInBoundsX(Entry e, IBarLineScatterCandleBubbleDataSet set) { + + if (e == null) + return false; + + float entryIndex = set.getEntryIndex(e); + + if (e == null || entryIndex >= set.getEntryCount() * mAnimator.getPhaseX()) { + return false; + } else { + return true; + } + } + + /** + * Class representing the bounds of the current viewport in terms of indices in the values array of a DataSet. + */ + protected class XBounds { + + /** + * minimum visible entry index + */ + public int min; + + /** + * maximum visible entry index + */ + public int max; + + /** + * range of visible entry indices + */ + public int range; + + /** + * Calculates the minimum and maximum x values as well as the range between them. + * + * @param chart + * @param dataSet + */ + public void set(BarLineScatterCandleBubbleDataProvider chart, IBarLineScatterCandleBubbleDataSet dataSet) { + float phaseX = Math.max(0.f, Math.min(1.f, mAnimator.getPhaseX())); + + float low = chart.getLowestVisibleX(); + float high = chart.getHighestVisibleX(); + + Entry entryFrom = dataSet.getEntryForXValue(low, Float.NaN, DataSet.Rounding.DOWN); + Entry entryTo = dataSet.getEntryForXValue(high, Float.NaN, DataSet.Rounding.UP); + + min = entryFrom == null ? 0 : dataSet.getEntryIndex(entryFrom); + max = entryTo == null ? 0 : dataSet.getEntryIndex(entryTo); + range = (int) ((max - min) * phaseX); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BubbleChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BubbleChartRenderer.java new file mode 100644 index 0000000..5ce19c2 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BubbleChartRenderer.java @@ -0,0 +1,274 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.BubbleData; +import com.github.mikephil.charting.data.BubbleEntry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BubbleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +/** + * Bubble chart implementation: Copyright 2015 Pierre-Marc Airoldi Licensed + * under Apache License 2.0 Ported by Daniel Cohen Gindi + */ +public class BubbleChartRenderer extends BarLineScatterCandleBubbleRenderer { + + protected BubbleDataProvider mChart; + + public BubbleChartRenderer(BubbleDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + + mRenderPaint.setStyle(Style.FILL); + + mHighlightPaint.setStyle(Style.STROKE); + mHighlightPaint.setStrokeWidth(Utils.convertDpToPixel(1.5f)); + } + + @Override + public void initBuffers() { + + } + + @Override + public void drawData(Canvas c) { + + BubbleData bubbleData = mChart.getBubbleData(); + + for (IBubbleDataSet set : bubbleData.getDataSets()) { + + if (set.isVisible()) + drawDataSet(c, set); + } + } + + private float[] sizeBuffer = new float[4]; + private float[] pointBuffer = new float[2]; + + protected float getShapeSize(float entrySize, float maxSize, float reference, boolean normalizeSize) { + final float factor = normalizeSize ? ((maxSize == 0f) ? 1f : (float) Math.sqrt(entrySize / maxSize)) : + entrySize; + final float shapeSize = reference * factor; + return shapeSize; + } + + protected void drawDataSet(Canvas c, IBubbleDataSet dataSet) { + + if (dataSet.getEntryCount() < 1) + return; + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + float phaseY = mAnimator.getPhaseY(); + + mXBounds.set(mChart, dataSet); + + sizeBuffer[0] = 0f; + sizeBuffer[2] = 1f; + + trans.pointValuesToPixel(sizeBuffer); + + boolean normalizeSize = dataSet.isNormalizeSizeEnabled(); + + // calcualte the full width of 1 step on the x-axis + final float maxBubbleWidth = Math.abs(sizeBuffer[2] - sizeBuffer[0]); + final float maxBubbleHeight = Math.abs(mViewPortHandler.contentBottom() - mViewPortHandler.contentTop()); + final float referenceSize = Math.min(maxBubbleHeight, maxBubbleWidth); + + for (int j = mXBounds.min; j <= mXBounds.range + mXBounds.min; j++) { + + final BubbleEntry entry = dataSet.getEntryForIndex(j); + + pointBuffer[0] = entry.getX(); + pointBuffer[1] = (entry.getY()) * phaseY; + trans.pointValuesToPixel(pointBuffer); + + float shapeHalf = getShapeSize(entry.getSize(), dataSet.getMaxSize(), referenceSize, normalizeSize) / 2f; + + if (!mViewPortHandler.isInBoundsTop(pointBuffer[1] + shapeHalf) + || !mViewPortHandler.isInBoundsBottom(pointBuffer[1] - shapeHalf)) + continue; + + if (!mViewPortHandler.isInBoundsLeft(pointBuffer[0] + shapeHalf)) + continue; + + if (!mViewPortHandler.isInBoundsRight(pointBuffer[0] - shapeHalf)) + break; + + final int color = dataSet.getColor(j); + + mRenderPaint.setColor(color); + c.drawCircle(pointBuffer[0], pointBuffer[1], shapeHalf, mRenderPaint); + } + } + + @Override + public void drawValues(Canvas c) { + + BubbleData bubbleData = mChart.getBubbleData(); + + if (bubbleData == null) + return; + + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + final List dataSets = bubbleData.getDataSets(); + + float lineHeight = Utils.calcTextHeight(mValuePaint, "1"); + + for (int i = 0; i < dataSets.size(); i++) { + + IBubbleDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + final float phaseX = Math.max(0.f, Math.min(1.f, mAnimator.getPhaseX())); + final float phaseY = mAnimator.getPhaseY(); + + mXBounds.set(mChart, dataSet); + + final float[] positions = mChart.getTransformer(dataSet.getAxisDependency()) + .generateTransformedValuesBubble(dataSet, phaseY, mXBounds.min, mXBounds.max); + + final float alpha = phaseX == 1 ? phaseY : phaseX; + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < positions.length; j += 2) { + + int valueTextColor = dataSet.getValueTextColor(j / 2 + mXBounds.min); + valueTextColor = Color.argb(Math.round(255.f * alpha), Color.red(valueTextColor), + Color.green(valueTextColor), Color.blue(valueTextColor)); + + float x = positions[j]; + float y = positions[j + 1]; + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if ((!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y))) + continue; + + BubbleEntry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, dataSet.getValueFormatter(), entry.getSize(), entry, i, x, + y + (0.5f * lineHeight), valueTextColor); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawExtras(Canvas c) { + } + + private float[] _hsvBuffer = new float[3]; + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + BubbleData bubbleData = mChart.getBubbleData(); + + float phaseY = mAnimator.getPhaseY(); + + for (Highlight high : indices) { + + IBubbleDataSet set = bubbleData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + final BubbleEntry entry = set.getEntryForXValue(high.getX(), high.getY()); + + if (entry.getY() != high.getY()) + continue; + + if (!isInBoundsX(entry, set)) + continue; + + Transformer trans = mChart.getTransformer(set.getAxisDependency()); + + sizeBuffer[0] = 0f; + sizeBuffer[2] = 1f; + + trans.pointValuesToPixel(sizeBuffer); + + boolean normalizeSize = set.isNormalizeSizeEnabled(); + + // calcualte the full width of 1 step on the x-axis + final float maxBubbleWidth = Math.abs(sizeBuffer[2] - sizeBuffer[0]); + final float maxBubbleHeight = Math.abs( + mViewPortHandler.contentBottom() - mViewPortHandler.contentTop()); + final float referenceSize = Math.min(maxBubbleHeight, maxBubbleWidth); + + pointBuffer[0] = entry.getX(); + pointBuffer[1] = (entry.getY()) * phaseY; + trans.pointValuesToPixel(pointBuffer); + + high.setDraw(pointBuffer[0], pointBuffer[1]); + + float shapeHalf = getShapeSize(entry.getSize(), + set.getMaxSize(), + referenceSize, + normalizeSize) / 2f; + + if (!mViewPortHandler.isInBoundsTop(pointBuffer[1] + shapeHalf) + || !mViewPortHandler.isInBoundsBottom(pointBuffer[1] - shapeHalf)) + continue; + + if (!mViewPortHandler.isInBoundsLeft(pointBuffer[0] + shapeHalf)) + continue; + + if (!mViewPortHandler.isInBoundsRight(pointBuffer[0] - shapeHalf)) + break; + + final int originalColor = set.getColor((int) entry.getX()); + + Color.RGBToHSV(Color.red(originalColor), Color.green(originalColor), + Color.blue(originalColor), _hsvBuffer); + _hsvBuffer[2] *= 0.5f; + final int color = Color.HSVToColor(Color.alpha(originalColor), _hsvBuffer); + + mHighlightPaint.setColor(color); + mHighlightPaint.setStrokeWidth(set.getHighlightCircleWidth()); + c.drawCircle(pointBuffer[0], pointBuffer[1], shapeHalf, mHighlightPaint); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CandleStickChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CandleStickChartRenderer.java new file mode 100644 index 0000000..991b702 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CandleStickChartRenderer.java @@ -0,0 +1,363 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.CandleData; +import com.github.mikephil.charting.data.CandleEntry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.CandleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class CandleStickChartRenderer extends LineScatterCandleRadarRenderer { + + protected CandleDataProvider mChart; + + private float[] mShadowBuffers = new float[8]; + private float[] mBodyBuffers = new float[4]; + private float[] mRangeBuffers = new float[4]; + private float[] mOpenBuffers = new float[4]; + private float[] mCloseBuffers = new float[4]; + + public CandleStickChartRenderer(CandleDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + } + + @Override + public void initBuffers() { + + } + + @Override + public void drawData(Canvas c) { + + CandleData candleData = mChart.getCandleData(); + + for (ICandleDataSet set : candleData.getDataSets()) { + + if (set.isVisible()) + drawDataSet(c, set); + } + } + + @SuppressWarnings("ResourceAsColor") + protected void drawDataSet(Canvas c, ICandleDataSet dataSet) { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + float phaseY = mAnimator.getPhaseY(); + float barSpace = dataSet.getBarSpace(); + boolean showCandleBar = dataSet.getShowCandleBar(); + + mXBounds.set(mChart, dataSet); + + mRenderPaint.setStrokeWidth(dataSet.getShadowWidth()); + + // draw the body + for (int j = mXBounds.min; j <= mXBounds.range + mXBounds.min; j++) { + + // get the entry + CandleEntry e = dataSet.getEntryForIndex(j); + + if (e == null) + continue; + + final float xPos = e.getX(); + + final float open = e.getOpen(); + final float close = e.getClose(); + final float high = e.getHigh(); + final float low = e.getLow(); + + if (showCandleBar) { + // calculate the shadow + + mShadowBuffers[0] = xPos; + mShadowBuffers[2] = xPos; + mShadowBuffers[4] = xPos; + mShadowBuffers[6] = xPos; + + if (open > close) { + mShadowBuffers[1] = high * phaseY; + mShadowBuffers[3] = open * phaseY; + mShadowBuffers[5] = low * phaseY; + mShadowBuffers[7] = close * phaseY; + } else if (open < close) { + mShadowBuffers[1] = high * phaseY; + mShadowBuffers[3] = close * phaseY; + mShadowBuffers[5] = low * phaseY; + mShadowBuffers[7] = open * phaseY; + } else { + mShadowBuffers[1] = high * phaseY; + mShadowBuffers[3] = open * phaseY; + mShadowBuffers[5] = low * phaseY; + mShadowBuffers[7] = mShadowBuffers[3]; + } + + trans.pointValuesToPixel(mShadowBuffers); + + // draw the shadows + + if (dataSet.getShadowColorSameAsCandle()) { + + if (open > close) + mRenderPaint.setColor( + dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE ? + dataSet.getColor(j) : + dataSet.getDecreasingColor() + ); + + else if (open < close) + mRenderPaint.setColor( + dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE ? + dataSet.getColor(j) : + dataSet.getIncreasingColor() + ); + + else + mRenderPaint.setColor( + dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE ? + dataSet.getColor(j) : + dataSet.getNeutralColor() + ); + + } else { + mRenderPaint.setColor( + dataSet.getShadowColor() == ColorTemplate.COLOR_NONE ? + dataSet.getColor(j) : + dataSet.getShadowColor() + ); + } + + mRenderPaint.setStyle(Paint.Style.STROKE); + + c.drawLines(mShadowBuffers, mRenderPaint); + + // calculate the body + + mBodyBuffers[0] = xPos - 0.5f + barSpace; + mBodyBuffers[1] = close * phaseY; + mBodyBuffers[2] = (xPos + 0.5f - barSpace); + mBodyBuffers[3] = open * phaseY; + + trans.pointValuesToPixel(mBodyBuffers); + + // draw body differently for increasing and decreasing entry + if (open > close) { // decreasing + + if (dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE) { + mRenderPaint.setColor(dataSet.getColor(j)); + } else { + mRenderPaint.setColor(dataSet.getDecreasingColor()); + } + + mRenderPaint.setStyle(dataSet.getDecreasingPaintStyle()); + + c.drawRect( + mBodyBuffers[0], mBodyBuffers[3], + mBodyBuffers[2], mBodyBuffers[1], + mRenderPaint); + + } else if (open < close) { + + if (dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE) { + mRenderPaint.setColor(dataSet.getColor(j)); + } else { + mRenderPaint.setColor(dataSet.getIncreasingColor()); + } + + mRenderPaint.setStyle(dataSet.getIncreasingPaintStyle()); + + c.drawRect( + mBodyBuffers[0], mBodyBuffers[1], + mBodyBuffers[2], mBodyBuffers[3], + mRenderPaint); + } else { // equal values + + if (dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE) { + mRenderPaint.setColor(dataSet.getColor(j)); + } else { + mRenderPaint.setColor(dataSet.getNeutralColor()); + } + + c.drawLine( + mBodyBuffers[0], mBodyBuffers[1], + mBodyBuffers[2], mBodyBuffers[3], + mRenderPaint); + } + } else { + + mRangeBuffers[0] = xPos; + mRangeBuffers[1] = high * phaseY; + mRangeBuffers[2] = xPos; + mRangeBuffers[3] = low * phaseY; + + mOpenBuffers[0] = xPos - 0.5f + barSpace; + mOpenBuffers[1] = open * phaseY; + mOpenBuffers[2] = xPos; + mOpenBuffers[3] = open * phaseY; + + mCloseBuffers[0] = xPos + 0.5f - barSpace; + mCloseBuffers[1] = close * phaseY; + mCloseBuffers[2] = xPos; + mCloseBuffers[3] = close * phaseY; + + trans.pointValuesToPixel(mRangeBuffers); + trans.pointValuesToPixel(mOpenBuffers); + trans.pointValuesToPixel(mCloseBuffers); + + // draw the ranges + int barColor; + + if (open > close) + barColor = dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE + ? dataSet.getColor(j) + : dataSet.getDecreasingColor(); + else if (open < close) + barColor = dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE + ? dataSet.getColor(j) + : dataSet.getIncreasingColor(); + else + barColor = dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE + ? dataSet.getColor(j) + : dataSet.getNeutralColor(); + + mRenderPaint.setColor(barColor); + c.drawLine( + mRangeBuffers[0], mRangeBuffers[1], + mRangeBuffers[2], mRangeBuffers[3], + mRenderPaint); + c.drawLine( + mOpenBuffers[0], mOpenBuffers[1], + mOpenBuffers[2], mOpenBuffers[3], + mRenderPaint); + c.drawLine( + mCloseBuffers[0], mCloseBuffers[1], + mCloseBuffers[2], mCloseBuffers[3], + mRenderPaint); + } + } + } + + @Override + public void drawValues(Canvas c) { + + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getCandleData().getDataSets(); + + for (int i = 0; i < dataSets.size(); i++) { + + ICandleDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mXBounds.set(mChart, dataSet); + + float[] positions = trans.generateTransformedValuesCandle( + dataSet, mAnimator.getPhaseX(), mAnimator.getPhaseY(), mXBounds.min, mXBounds.max); + + float yOffset = Utils.convertDpToPixel(5f); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < positions.length; j += 2) { + + float x = positions[j]; + float y = positions[j + 1]; + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y)) + continue; + + CandleEntry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + dataSet.getValueFormatter(), + entry.getHigh(), + entry, + i, + x, + y - yOffset, + dataSet + .getValueTextColor(j / 2)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawExtras(Canvas c) { + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + CandleData candleData = mChart.getCandleData(); + + for (Highlight high : indices) { + + ICandleDataSet set = candleData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + CandleEntry e = set.getEntryForXValue(high.getX(), high.getY()); + + if (!isInBoundsX(e, set)) + continue; + + float lowValue = e.getLow() * mAnimator.getPhaseY(); + float highValue = e.getHigh() * mAnimator.getPhaseY(); + float y = (lowValue + highValue) / 2f; + + MPPointD pix = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(e.getX(), y); + + high.setDraw((float) pix.x, (float) pix.y); + + // draw the lines + drawHighlightLines(c, (float) pix.x, (float) pix.y, set); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java new file mode 100644 index 0000000..6d0d4d3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java @@ -0,0 +1,167 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.charts.CombinedChart; +import com.github.mikephil.charting.charts.CombinedChart.DrawOrder; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.data.CombinedData; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * Renderer class that is responsible for rendering multiple different data-types. + */ +public class CombinedChartRenderer extends DataRenderer { + + /** + * all rederers for the different kinds of data this combined-renderer can draw + */ + protected List mRenderers = new ArrayList(5); + + protected WeakReference mChart; + + public CombinedChartRenderer(CombinedChart chart, ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = new WeakReference(chart); + createRenderers(); + } + + /** + * Creates the renderers needed for this combined-renderer in the required order. Also takes the DrawOrder into + * consideration. + */ + public void createRenderers() { + + mRenderers.clear(); + + CombinedChart chart = (CombinedChart)mChart.get(); + if (chart == null) + return; + + DrawOrder[] orders = chart.getDrawOrder(); + + for (DrawOrder order : orders) { + + switch (order) { + case BAR: + if (chart.getBarData() != null) + mRenderers.add(new BarChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + case BUBBLE: + if (chart.getBubbleData() != null) + mRenderers.add(new BubbleChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + case LINE: + if (chart.getLineData() != null) + mRenderers.add(new LineChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + case CANDLE: + if (chart.getCandleData() != null) + mRenderers.add(new CandleStickChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + case SCATTER: + if (chart.getScatterData() != null) + mRenderers.add(new ScatterChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + } + } + } + + @Override + public void initBuffers() { + + for (DataRenderer renderer : mRenderers) + renderer.initBuffers(); + } + + @Override + public void drawData(Canvas c) { + + for (DataRenderer renderer : mRenderers) + renderer.drawData(c); + } + + @Override + public void drawValues(Canvas c) { + + for (DataRenderer renderer : mRenderers) + renderer.drawValues(c); + } + + @Override + public void drawExtras(Canvas c) { + + for (DataRenderer renderer : mRenderers) + renderer.drawExtras(c); + } + + protected List mHighlightBuffer = new ArrayList(); + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + Chart chart = mChart.get(); + if (chart == null) return; + + for (DataRenderer renderer : mRenderers) { + ChartData data = null; + + if (renderer instanceof BarChartRenderer) + data = ((BarChartRenderer)renderer).mChart.getBarData(); + else if (renderer instanceof LineChartRenderer) + data = ((LineChartRenderer)renderer).mChart.getLineData(); + else if (renderer instanceof CandleStickChartRenderer) + data = ((CandleStickChartRenderer)renderer).mChart.getCandleData(); + else if (renderer instanceof ScatterChartRenderer) + data = ((ScatterChartRenderer)renderer).mChart.getScatterData(); + else if (renderer instanceof BubbleChartRenderer) + data = ((BubbleChartRenderer)renderer).mChart.getBubbleData(); + + int dataIndex = data == null ? -1 + : ((CombinedData)chart.getData()).getAllData().indexOf(data); + + mHighlightBuffer.clear(); + + for (Highlight h : indices) { + if (h.getDataIndex() == dataIndex || h.getDataIndex() == -1) + mHighlightBuffer.add(h); + } + + renderer.drawHighlighted(c, mHighlightBuffer.toArray(new Highlight[mHighlightBuffer.size()])); + } + } + + /** + * Returns the sub-renderer object at the specified index. + * + * @param index + * @return + */ + public DataRenderer getSubRenderer(int index) { + if (index >= mRenderers.size() || index < 0) + return null; + else + return mRenderers.get(index); + } + + /** + * Returns all sub-renderers. + * + * @return + */ + public List getSubRenderers() { + return mRenderers; + } + + public void setSubRenderers(List renderers) { + this.mRenderers = renderers; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/DataRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/DataRenderer.java new file mode 100644 index 0000000..e8e5446 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/DataRenderer.java @@ -0,0 +1,169 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.ChartInterface; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Superclass of all render classes for the different data types (line, bar, ...). + * + * @author Philipp Jahoda + */ +public abstract class DataRenderer extends Renderer { + + /** + * the animator object used to perform animations on the chart data + */ + protected ChartAnimator mAnimator; + + /** + * main paint object used for rendering + */ + protected Paint mRenderPaint; + + /** + * paint used for highlighting values + */ + protected Paint mHighlightPaint; + + protected Paint mDrawPaint; + + /** + * paint object for drawing values (text representing values of chart + * entries) + */ + protected Paint mValuePaint; + + public DataRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(viewPortHandler); + this.mAnimator = animator; + + mRenderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mRenderPaint.setStyle(Style.FILL); + + mDrawPaint = new Paint(Paint.DITHER_FLAG); + + mValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mValuePaint.setColor(Color.rgb(63, 63, 63)); + mValuePaint.setTextAlign(Align.CENTER); + mValuePaint.setTextSize(Utils.convertDpToPixel(9f)); + + mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mHighlightPaint.setStyle(Paint.Style.STROKE); + mHighlightPaint.setStrokeWidth(2f); + mHighlightPaint.setColor(Color.rgb(255, 187, 115)); + } + + protected boolean isDrawingValuesAllowed(ChartInterface chart) { + return chart.getData().getEntryCount() < chart.getMaxVisibleCount() + * mViewPortHandler.getScaleX(); + } + + /** + * Returns the Paint object this renderer uses for drawing the values + * (value-text). + * + * @return + */ + public Paint getPaintValues() { + return mValuePaint; + } + + /** + * Returns the Paint object this renderer uses for drawing highlight + * indicators. + * + * @return + */ + public Paint getPaintHighlight() { + return mHighlightPaint; + } + + /** + * Returns the Paint object used for rendering. + * + * @return + */ + public Paint getPaintRender() { + return mRenderPaint; + } + + /** + * Applies the required styling (provided by the DataSet) to the value-paint + * object. + * + * @param set + */ + protected void applyValueTextStyle(IDataSet set) { + + mValuePaint.setTypeface(set.getValueTypeface()); + mValuePaint.setTextSize(set.getValueTextSize()); + } + + /** + * Initializes the buffers used for rendering with a new size. Since this + * method performs memory allocations, it should only be called if + * necessary. + */ + public abstract void initBuffers(); + + /** + * Draws the actual data in form of lines, bars, ... depending on Renderer subclass. + * + * @param c + */ + public abstract void drawData(Canvas c); + + /** + * Loops over all Entrys and draws their values. + * + * @param c + */ + public abstract void drawValues(Canvas c); + + /** + * Draws the value of the given entry by using the provided IValueFormatter. + * + * @param c canvas + * @param formatter formatter for custom value-formatting + * @param value the value to be drawn + * @param entry the entry the value belongs to + * @param dataSetIndex the index of the DataSet the drawn Entry belongs to + * @param x position + * @param y position + * @param color + */ + public void drawValue(Canvas c, IValueFormatter formatter, float value, Entry entry, int dataSetIndex, float x, float y, int color) { + mValuePaint.setColor(color); + c.drawText(formatter.getFormattedValue(value, entry, dataSetIndex, mViewPortHandler), x, y, mValuePaint); + } + + /** + * Draws any kind of additional information (e.g. line-circles). + * + * @param c + */ + public abstract void drawExtras(Canvas c); + + /** + * Draws all highlight indicators for the values that are currently highlighted. + * + * @param c + * @param indices the highlighted values + */ + public abstract void drawHighlighted(Canvas c, Highlight[] indices); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java new file mode 100644 index 0000000..0cd7234 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java @@ -0,0 +1,443 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint.Align; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.buffer.BarBuffer; +import com.github.mikephil.charting.buffer.HorizontalBarBuffer; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.dataprovider.ChartInterface; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.Fill; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +/** + * Renderer for the HorizontalBarChart. + * + * @author Philipp Jahoda + */ +public class HorizontalBarChartRenderer extends BarChartRenderer { + + public HorizontalBarChartRenderer(BarDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(chart, animator, viewPortHandler); + + mValuePaint.setTextAlign(Align.LEFT); + } + + @Override + public void initBuffers() { + + BarData barData = mChart.getBarData(); + mBarBuffers = new HorizontalBarBuffer[barData.getDataSetCount()]; + + for (int i = 0; i < mBarBuffers.length; i++) { + IBarDataSet set = barData.getDataSetByIndex(i); + mBarBuffers[i] = new HorizontalBarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1), + barData.getDataSetCount(), set.isStacked()); + } + } + + private RectF mBarShadowRectBuffer = new RectF(); + + @Override + protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mBarBorderPaint.setColor(dataSet.getBarBorderColor()); + mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); + + final boolean drawBorder = dataSet.getBarBorderWidth() > 0.f; + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + // draw the bar shadow before the values + if (mChart.isDrawBarShadowEnabled()) { + mShadowPaint.setColor(dataSet.getBarShadowColor()); + + BarData barData = mChart.getBarData(); + + final float barWidth = barData.getBarWidth(); + final float barWidthHalf = barWidth / 2.0f; + float x; + + for (int i = 0, count = Math.min((int)(Math.ceil((float)(dataSet.getEntryCount()) * phaseX)), dataSet.getEntryCount()); + i < count; + i++) { + + BarEntry e = dataSet.getEntryForIndex(i); + + x = e.getX(); + + mBarShadowRectBuffer.top = x - barWidthHalf; + mBarShadowRectBuffer.bottom = x + barWidthHalf; + + trans.rectValueToPixel(mBarShadowRectBuffer); + + if (!mViewPortHandler.isInBoundsTop(mBarShadowRectBuffer.bottom)) + continue; + + if (!mViewPortHandler.isInBoundsBottom(mBarShadowRectBuffer.top)) + break; + + mBarShadowRectBuffer.left = mViewPortHandler.contentLeft(); + mBarShadowRectBuffer.right = mViewPortHandler.contentRight(); + + c.drawRect(mBarShadowRectBuffer, mShadowPaint); + } + } + + // initialize the buffer + BarBuffer buffer = mBarBuffers[index]; + buffer.setPhases(phaseX, phaseY); + buffer.setDataSet(index); + buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency())); + buffer.setBarWidth(mChart.getBarData().getBarWidth()); + + buffer.feed(dataSet); + + trans.pointValuesToPixel(buffer.buffer); + + final boolean isCustomFill = dataSet.getFills() != null && !dataSet.getFills().isEmpty(); + final boolean isSingleColor = dataSet.getColors().size() == 1; + final boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + + if (isSingleColor) { + mRenderPaint.setColor(dataSet.getColor()); + } + + for (int j = 0, pos = 0; j < buffer.size(); j += 4, pos++) { + + if (!mViewPortHandler.isInBoundsTop(buffer.buffer[j + 3])) + break; + + if (!mViewPortHandler.isInBoundsBottom(buffer.buffer[j + 1])) + continue; + + if (!isSingleColor) { + // Set the color for the currently drawn value. If the index + // is out of bounds, reuse colors. + mRenderPaint.setColor(dataSet.getColor(j / 4)); + } + + if (isCustomFill) { + dataSet.getFill(pos) + .fillRect( + c, mRenderPaint, + buffer.buffer[j], + buffer.buffer[j + 1], + buffer.buffer[j + 2], + buffer.buffer[j + 3], + isInverted ? Fill.Direction.LEFT : Fill.Direction.RIGHT); + } + else { + c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mRenderPaint); + } + + if (drawBorder) { + c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mBarBorderPaint); + } + } + } + + @Override + public void drawValues(Canvas c) { + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getBarData().getDataSets(); + + final float valueOffsetPlus = Utils.convertDpToPixel(5f); + float posOffset = 0f; + float negOffset = 0f; + final boolean drawValueAboveBar = mChart.isDrawValueAboveBarEnabled(); + + for (int i = 0; i < mChart.getBarData().getDataSetCount(); i++) { + + IBarDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet)) + continue; + + boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + final float halfTextHeight = Utils.calcTextHeight(mValuePaint, "10") / 2f; + + IValueFormatter formatter = dataSet.getValueFormatter(); + + // get the buffer + BarBuffer buffer = mBarBuffers[i]; + + final float phaseY = mAnimator.getPhaseY(); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + // if only single values are drawn (sum) + if (!dataSet.isStacked()) { + + for (int j = 0; j < buffer.buffer.length * mAnimator.getPhaseX(); j += 4) { + + float y = (buffer.buffer[j + 1] + buffer.buffer[j + 3]) / 2f; + + if (!mViewPortHandler.isInBoundsTop(buffer.buffer[j + 1])) + break; + + if (!mViewPortHandler.isInBoundsX(buffer.buffer[j])) + continue; + + if (!mViewPortHandler.isInBoundsBottom(buffer.buffer[j + 1])) + continue; + + BarEntry entry = dataSet.getEntryForIndex(j / 4); + float val = entry.getY(); + String formattedValue = formatter.getFormattedValue(val, entry, i, mViewPortHandler); + + // calculate the correct offset depending on the draw position of the value + float valueTextWidth = Utils.calcTextWidth(mValuePaint, formattedValue); + posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)); + negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus) + - (buffer.buffer[j + 2] - buffer.buffer[j]); + + if (isInverted) { + posOffset = -posOffset - valueTextWidth; + negOffset = -negOffset - valueTextWidth; + } + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + formattedValue, + buffer.buffer[j + 2] + (val >= 0 ? posOffset : negOffset), + y + halfTextHeight, + dataSet.getValueTextColor(j / 2)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float px = buffer.buffer[j + 2] + (val >= 0 ? posOffset : negOffset); + float py = y; + + px += iconsOffset.x; + py += iconsOffset.y; + + Utils.drawImage( + c, + icon, + (int)px, + (int)py, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + // if each value of a potential stack should be drawn + } else { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + int bufferIndex = 0; + int index = 0; + + while (index < dataSet.getEntryCount() * mAnimator.getPhaseX()) { + + BarEntry entry = dataSet.getEntryForIndex(index); + + int color = dataSet.getValueTextColor(index); + float[] vals = entry.getYVals(); + + // we still draw stacked bars, but there is one + // non-stacked + // in between + if (vals == null) { + + if (!mViewPortHandler.isInBoundsTop(buffer.buffer[bufferIndex + 1])) + break; + + if (!mViewPortHandler.isInBoundsX(buffer.buffer[bufferIndex])) + continue; + + if (!mViewPortHandler.isInBoundsBottom(buffer.buffer[bufferIndex + 1])) + continue; + + float val = entry.getY(); + String formattedValue = formatter.getFormattedValue(val, + entry, i, mViewPortHandler); + + // calculate the correct offset depending on the draw position of the value + float valueTextWidth = Utils.calcTextWidth(mValuePaint, formattedValue); + posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)); + negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus); + + if (isInverted) { + posOffset = -posOffset - valueTextWidth; + negOffset = -negOffset - valueTextWidth; + } + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, formattedValue, + buffer.buffer[bufferIndex + 2] + + (entry.getY() >= 0 ? posOffset : negOffset), + buffer.buffer[bufferIndex + 1] + halfTextHeight, color); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float px = buffer.buffer[bufferIndex + 2] + + (entry.getY() >= 0 ? posOffset : negOffset); + float py = buffer.buffer[bufferIndex + 1]; + + px += iconsOffset.x; + py += iconsOffset.y; + + Utils.drawImage( + c, + icon, + (int)px, + (int)py, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + + } else { + + float[] transformed = new float[vals.length * 2]; + + float posY = 0f; + float negY = -entry.getNegativeSum(); + + for (int k = 0, idx = 0; k < transformed.length; k += 2, idx++) { + + float value = vals[idx]; + float y; + + if (value == 0.0f && (posY == 0.0f || negY == 0.0f)) { + // Take care of the situation of a 0.0 value, which overlaps a non-zero bar + y = value; + } else if (value >= 0.0f) { + posY += value; + y = posY; + } else { + y = negY; + negY -= value; + } + + transformed[k] = y * phaseY; + } + + trans.pointValuesToPixel(transformed); + + for (int k = 0; k < transformed.length; k += 2) { + + final float val = vals[k / 2]; + String formattedValue = formatter.getFormattedValue(val, + entry, i, mViewPortHandler); + + // calculate the correct offset depending on the draw position of the value + float valueTextWidth = Utils.calcTextWidth(mValuePaint, formattedValue); + posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)); + negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus); + + if (isInverted) { + posOffset = -posOffset - valueTextWidth; + negOffset = -negOffset - valueTextWidth; + } + + final boolean drawBelow = + (val == 0.0f && negY == 0.0f && posY > 0.0f) || + val < 0.0f; + + float x = transformed[k] + + (drawBelow ? negOffset : posOffset); + float y = (buffer.buffer[bufferIndex + 1] + buffer.buffer[bufferIndex + 3]) / 2f; + + if (!mViewPortHandler.isInBoundsTop(y)) + break; + + if (!mViewPortHandler.isInBoundsX(x)) + continue; + + if (!mViewPortHandler.isInBoundsBottom(y)) + continue; + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, formattedValue, x, y + halfTextHeight, color); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + } + + bufferIndex = vals == null ? bufferIndex + 4 : bufferIndex + 4 * vals.length; + index++; + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + protected void drawValue(Canvas c, String valueText, float x, float y, int color) { + mValuePaint.setColor(color); + c.drawText(valueText, x, y, mValuePaint); + } + + @Override + protected void prepareBarHighlight(float x, float y1, float y2, float barWidthHalf, Transformer trans) { + + float top = x - barWidthHalf; + float bottom = x + barWidthHalf; + float left = y1; + float right = y2; + + mBarRect.set(left, top, right, bottom); + + trans.rectToPixelPhaseHorizontal(mBarRect, mAnimator.getPhaseY()); + } + + @Override + protected void setHighlightDrawPos(Highlight high, RectF bar) { + high.setDraw(bar.centerY(), bar.right); + } + + @Override + protected boolean isDrawingValuesAllowed(ChartInterface chart) { + return chart.getData().getEntryCount() < chart.getMaxVisibleCount() + * mViewPortHandler.getScaleY(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LegendRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LegendRenderer.java new file mode 100644 index 0000000..5d49580 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LegendRenderer.java @@ -0,0 +1,570 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.Typeface; + +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.LegendEntry; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LegendRenderer extends Renderer { + + /** + * paint for the legend labels + */ + protected Paint mLegendLabelPaint; + + /** + * paint used for the legend forms + */ + protected Paint mLegendFormPaint; + + /** + * the legend object this renderer renders + */ + protected Legend mLegend; + + public LegendRenderer(ViewPortHandler viewPortHandler, Legend legend) { + super(viewPortHandler); + + this.mLegend = legend; + + mLegendLabelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mLegendLabelPaint.setTextSize(Utils.convertDpToPixel(9f)); + mLegendLabelPaint.setTextAlign(Align.LEFT); + + mLegendFormPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mLegendFormPaint.setStyle(Paint.Style.FILL); + } + + /** + * Returns the Paint object used for drawing the Legend labels. + * + * @return + */ + public Paint getLabelPaint() { + return mLegendLabelPaint; + } + + /** + * Returns the Paint object used for drawing the Legend forms. + * + * @return + */ + public Paint getFormPaint() { + return mLegendFormPaint; + } + + + protected List computedEntries = new ArrayList<>(16); + + /** + * Prepares the legend and calculates all needed forms, labels and colors. + * + * @param data + */ + public void computeLegend(ChartData data) { + + if (!mLegend.isLegendCustom()) { + + computedEntries.clear(); + + // loop for building up the colors and labels used in the legend + for (int i = 0; i < data.getDataSetCount(); i++) { + + IDataSet dataSet = data.getDataSetByIndex(i); + if (dataSet == null) continue; + + List clrs = dataSet.getColors(); + int entryCount = dataSet.getEntryCount(); + + // if we have a barchart with stacked bars + if (dataSet instanceof IBarDataSet && ((IBarDataSet) dataSet).isStacked()) { + + IBarDataSet bds = (IBarDataSet) dataSet; + String[] sLabels = bds.getStackLabels(); + + int minEntries = Math.min(clrs.size(), bds.getStackSize()); + + for (int j = 0; j < minEntries; j++) { + String label; + if (sLabels.length > 0) { + int labelIndex = j % minEntries; + label = labelIndex < sLabels.length ? sLabels[labelIndex] : null; + } else { + label = null; + } + + computedEntries.add(new LegendEntry( + label, + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + clrs.get(j) + )); + } + + if (bds.getLabel() != null) { + // add the legend description label + computedEntries.add(new LegendEntry( + dataSet.getLabel(), + Legend.LegendForm.NONE, + Float.NaN, + Float.NaN, + null, + ColorTemplate.COLOR_NONE + )); + } + + } else if (dataSet instanceof IPieDataSet) { + + IPieDataSet pds = (IPieDataSet) dataSet; + + for (int j = 0; j < clrs.size() && j < entryCount; j++) { + + computedEntries.add(new LegendEntry( + pds.getEntryForIndex(j).getLabel(), + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + clrs.get(j) + )); + } + + if (pds.getLabel() != null) { + // add the legend description label + computedEntries.add(new LegendEntry( + dataSet.getLabel(), + Legend.LegendForm.NONE, + Float.NaN, + Float.NaN, + null, + ColorTemplate.COLOR_NONE + )); + } + + } else if (dataSet instanceof ICandleDataSet && ((ICandleDataSet) dataSet).getDecreasingColor() != + ColorTemplate.COLOR_NONE) { + + int decreasingColor = ((ICandleDataSet) dataSet).getDecreasingColor(); + int increasingColor = ((ICandleDataSet) dataSet).getIncreasingColor(); + + computedEntries.add(new LegendEntry( + null, + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + decreasingColor + )); + + computedEntries.add(new LegendEntry( + dataSet.getLabel(), + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + increasingColor + )); + + } else { // all others + + for (int j = 0; j < clrs.size() && j < entryCount; j++) { + + String label; + + // if multiple colors are set for a DataSet, group them + if (j < clrs.size() - 1 && j < entryCount - 1) { + label = null; + } else { // add label to the last entry + label = data.getDataSetByIndex(i).getLabel(); + } + + computedEntries.add(new LegendEntry( + label, + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + clrs.get(j) + )); + } + } + } + + if (mLegend.getExtraEntries() != null) { + Collections.addAll(computedEntries, mLegend.getExtraEntries()); + } + + mLegend.setEntries(computedEntries); + } + + Typeface tf = mLegend.getTypeface(); + + if (tf != null) + mLegendLabelPaint.setTypeface(tf); + + mLegendLabelPaint.setTextSize(mLegend.getTextSize()); + mLegendLabelPaint.setColor(mLegend.getTextColor()); + + // calculate all dimensions of the mLegend + mLegend.calculateDimensions(mLegendLabelPaint, mViewPortHandler); + } + + protected Paint.FontMetrics legendFontMetrics = new Paint.FontMetrics(); + + public void renderLegend(Canvas c) { + + if (!mLegend.isEnabled()) + return; + + Typeface tf = mLegend.getTypeface(); + + if (tf != null) + mLegendLabelPaint.setTypeface(tf); + + mLegendLabelPaint.setTextSize(mLegend.getTextSize()); + mLegendLabelPaint.setColor(mLegend.getTextColor()); + + float labelLineHeight = Utils.getLineHeight(mLegendLabelPaint, legendFontMetrics); + float labelLineSpacing = Utils.getLineSpacing(mLegendLabelPaint, legendFontMetrics) + + Utils.convertDpToPixel(mLegend.getYEntrySpace()); + float formYOffset = labelLineHeight - Utils.calcTextHeight(mLegendLabelPaint, "ABC") / 2.f; + + LegendEntry[] entries = mLegend.getEntries(); + + float formToTextSpace = Utils.convertDpToPixel(mLegend.getFormToTextSpace()); + float xEntrySpace = Utils.convertDpToPixel(mLegend.getXEntrySpace()); + Legend.LegendOrientation orientation = mLegend.getOrientation(); + Legend.LegendHorizontalAlignment horizontalAlignment = mLegend.getHorizontalAlignment(); + Legend.LegendVerticalAlignment verticalAlignment = mLegend.getVerticalAlignment(); + Legend.LegendDirection direction = mLegend.getDirection(); + float defaultFormSize = Utils.convertDpToPixel(mLegend.getFormSize()); + + // space between the entries + float stackSpace = Utils.convertDpToPixel(mLegend.getStackSpace()); + + float yoffset = mLegend.getYOffset(); + float xoffset = mLegend.getXOffset(); + float originPosX = 0.f; + + switch (horizontalAlignment) { + case LEFT: + + if (orientation == Legend.LegendOrientation.VERTICAL) + originPosX = xoffset; + else + originPosX = mViewPortHandler.contentLeft() + xoffset; + + if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) + originPosX += mLegend.mNeededWidth; + + break; + + case RIGHT: + + if (orientation == Legend.LegendOrientation.VERTICAL) + originPosX = mViewPortHandler.getChartWidth() - xoffset; + else + originPosX = mViewPortHandler.contentRight() - xoffset; + + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + originPosX -= mLegend.mNeededWidth; + + break; + + case CENTER: + + if (orientation == Legend.LegendOrientation.VERTICAL) + originPosX = mViewPortHandler.getChartWidth() / 2.f; + else + originPosX = mViewPortHandler.contentLeft() + + mViewPortHandler.contentWidth() / 2.f; + + originPosX += (direction == Legend.LegendDirection.LEFT_TO_RIGHT + ? +xoffset + : -xoffset); + + // Horizontally layed out legends do the center offset on a line basis, + // So here we offset the vertical ones only. + if (orientation == Legend.LegendOrientation.VERTICAL) { + originPosX += (direction == Legend.LegendDirection.LEFT_TO_RIGHT + ? -mLegend.mNeededWidth / 2.0 + xoffset + : mLegend.mNeededWidth / 2.0 - xoffset); + } + + break; + } + + switch (orientation) { + case HORIZONTAL: { + + List calculatedLineSizes = mLegend.getCalculatedLineSizes(); + List calculatedLabelSizes = mLegend.getCalculatedLabelSizes(); + List calculatedLabelBreakPoints = mLegend.getCalculatedLabelBreakPoints(); + + float posX = originPosX; + float posY = 0.f; + + switch (verticalAlignment) { + case TOP: + posY = yoffset; + break; + + case BOTTOM: + posY = mViewPortHandler.getChartHeight() - yoffset - mLegend.mNeededHeight; + break; + + case CENTER: + posY = (mViewPortHandler.getChartHeight() - mLegend.mNeededHeight) / 2.f + yoffset; + break; + } + + int lineIndex = 0; + + for (int i = 0, count = entries.length; i < count; i++) { + + LegendEntry e = entries[i]; + boolean drawingForm = e.form != Legend.LegendForm.NONE; + float formSize = Float.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); + + if (i < calculatedLabelBreakPoints.size() && calculatedLabelBreakPoints.get(i)) { + posX = originPosX; + posY += labelLineHeight + labelLineSpacing; + } + + if (posX == originPosX && + horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER && + lineIndex < calculatedLineSizes.size()) { + posX += (direction == Legend.LegendDirection.RIGHT_TO_LEFT + ? calculatedLineSizes.get(lineIndex).width + : -calculatedLineSizes.get(lineIndex).width) / 2.f; + lineIndex++; + } + + boolean isStacked = e.label == null; // grouped forms have null labels + + if (drawingForm) { + if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) + posX -= formSize; + + drawForm(c, posX, posY + formYOffset, e, mLegend); + + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + posX += formSize; + } + + if (!isStacked) { + if (drawingForm) + posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -formToTextSpace : + formToTextSpace; + + if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) + posX -= calculatedLabelSizes.get(i).width; + + drawLabel(c, posX, posY + labelLineHeight, e.label); + + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + posX += calculatedLabelSizes.get(i).width; + + posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -xEntrySpace : xEntrySpace; + } else + posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -stackSpace : stackSpace; + } + + break; + } + + case VERTICAL: { + // contains the stacked legend size in pixels + float stack = 0f; + boolean wasStacked = false; + float posY = 0.f; + + switch (verticalAlignment) { + case TOP: + posY = (horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER + ? 0.f + : mViewPortHandler.contentTop()); + posY += yoffset; + break; + + case BOTTOM: + posY = (horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER + ? mViewPortHandler.getChartHeight() + : mViewPortHandler.contentBottom()); + posY -= mLegend.mNeededHeight + yoffset; + break; + + case CENTER: + posY = mViewPortHandler.getChartHeight() / 2.f + - mLegend.mNeededHeight / 2.f + + mLegend.getYOffset(); + break; + } + + for (int i = 0; i < entries.length; i++) { + + LegendEntry e = entries[i]; + boolean drawingForm = e.form != Legend.LegendForm.NONE; + float formSize = Float.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); + + float posX = originPosX; + + if (drawingForm) { + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + posX += stack; + else + posX -= formSize - stack; + + drawForm(c, posX, posY + formYOffset, e, mLegend); + + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + posX += formSize; + } + + if (e.label != null) { + + if (drawingForm && !wasStacked) + posX += direction == Legend.LegendDirection.LEFT_TO_RIGHT ? formToTextSpace + : -formToTextSpace; + else if (wasStacked) + posX = originPosX; + + if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) + posX -= Utils.calcTextWidth(mLegendLabelPaint, e.label); + + if (!wasStacked) { + drawLabel(c, posX, posY + labelLineHeight, e.label); + } else { + posY += labelLineHeight + labelLineSpacing; + drawLabel(c, posX, posY + labelLineHeight, e.label); + } + + // make a step down + posY += labelLineHeight + labelLineSpacing; + stack = 0f; + } else { + stack += formSize + stackSpace; + wasStacked = true; + } + } + + break; + + } + } + } + + private Path mLineFormPath = new Path(); + + /** + * Draws the Legend-form at the given position with the color at the given + * index. + * + * @param c canvas to draw with + * @param x position + * @param y position + * @param entry the entry to render + * @param legend the legend context + */ + protected void drawForm( + Canvas c, + float x, float y, + LegendEntry entry, + Legend legend) { + + if (entry.formColor == ColorTemplate.COLOR_SKIP || + entry.formColor == ColorTemplate.COLOR_NONE || + entry.formColor == 0) + return; + + int restoreCount = c.save(); + + Legend.LegendForm form = entry.form; + if (form == Legend.LegendForm.DEFAULT) + form = legend.getForm(); + + mLegendFormPaint.setColor(entry.formColor); + + final float formSize = Utils.convertDpToPixel( + Float.isNaN(entry.formSize) + ? legend.getFormSize() + : entry.formSize); + final float half = formSize / 2f; + + switch (form) { + case NONE: + // Do nothing + break; + + case EMPTY: + // Do not draw, but keep space for the form + break; + + case DEFAULT: + case CIRCLE: + mLegendFormPaint.setStyle(Paint.Style.FILL); + c.drawCircle(x + half, y, half, mLegendFormPaint); + break; + + case SQUARE: + mLegendFormPaint.setStyle(Paint.Style.FILL); + c.drawRect(x, y - half, x + formSize, y + half, mLegendFormPaint); + break; + + case LINE: + { + final float formLineWidth = Utils.convertDpToPixel( + Float.isNaN(entry.formLineWidth) + ? legend.getFormLineWidth() + : entry.formLineWidth); + final DashPathEffect formLineDashEffect = entry.formLineDashEffect == null + ? legend.getFormLineDashEffect() + : entry.formLineDashEffect; + mLegendFormPaint.setStyle(Paint.Style.STROKE); + mLegendFormPaint.setStrokeWidth(formLineWidth); + mLegendFormPaint.setPathEffect(formLineDashEffect); + + mLineFormPath.reset(); + mLineFormPath.moveTo(x, y); + mLineFormPath.lineTo(x + formSize, y); + c.drawPath(mLineFormPath, mLegendFormPaint); + } + break; + } + + c.restoreToCount(restoreCount); + } + + /** + * Draws the provided label at the given position. + * + * @param c canvas to draw with + * @param x + * @param y + * @param label the label to draw + */ + protected void drawLabel(Canvas c, float x, float y, String label) { + c.drawText(label, x, y, mLegendLabelPaint); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java new file mode 100644 index 0000000..a86c16f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java @@ -0,0 +1,863 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.List; + +public class LineChartRenderer extends LineRadarRenderer { + + protected LineDataProvider mChart; + + /** + * paint for the inner circle of the value indicators + */ + protected Paint mCirclePaintInner; + + /** + * Bitmap object used for drawing the paths (otherwise they are too long if + * rendered directly on the canvas) + */ + protected WeakReference mDrawBitmap; + + /** + * on this canvas, the paths are rendered, it is initialized with the + * pathBitmap + */ + protected Canvas mBitmapCanvas; + + /** + * the bitmap configuration to be used + */ + protected Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888; + + protected Path cubicPath = new Path(); + protected Path cubicFillPath = new Path(); + + public LineChartRenderer(LineDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + + mCirclePaintInner = new Paint(Paint.ANTI_ALIAS_FLAG); + mCirclePaintInner.setStyle(Paint.Style.FILL); + mCirclePaintInner.setColor(Color.WHITE); + } + + @Override + public void initBuffers() { + } + + @Override + public void drawData(Canvas c) { + + int width = (int) mViewPortHandler.getChartWidth(); + int height = (int) mViewPortHandler.getChartHeight(); + + Bitmap drawBitmap = mDrawBitmap == null ? null : mDrawBitmap.get(); + + if (drawBitmap == null + || (drawBitmap.getWidth() != width) + || (drawBitmap.getHeight() != height)) { + + if (width > 0 && height > 0) { + drawBitmap = Bitmap.createBitmap(width, height, mBitmapConfig); + mDrawBitmap = new WeakReference<>(drawBitmap); + mBitmapCanvas = new Canvas(drawBitmap); + } else + return; + } + + drawBitmap.eraseColor(Color.TRANSPARENT); + + LineData lineData = mChart.getLineData(); + + for (ILineDataSet set : lineData.getDataSets()) { + + if (set.isVisible()) + drawDataSet(c, set); + } + + c.drawBitmap(drawBitmap, 0, 0, mRenderPaint); + } + + protected void drawDataSet(Canvas c, ILineDataSet dataSet) { + + if (dataSet.getEntryCount() < 1) + return; + + mRenderPaint.setStrokeWidth(dataSet.getLineWidth()); + mRenderPaint.setPathEffect(dataSet.getDashPathEffect()); + + switch (dataSet.getMode()) { + default: + case LINEAR: + case STEPPED: + drawLinear(c, dataSet); + break; + + case CUBIC_BEZIER: + drawCubicBezier(dataSet); + break; + + case HORIZONTAL_BEZIER: + drawHorizontalBezier(dataSet); + break; + } + + mRenderPaint.setPathEffect(null); + } + + protected void drawHorizontalBezier(ILineDataSet dataSet) { + + float phaseY = mAnimator.getPhaseY(); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mXBounds.set(mChart, dataSet); + + cubicPath.reset(); + + if (mXBounds.range >= 1) { + + Entry prev = dataSet.getEntryForIndex(mXBounds.min); + Entry cur = prev; + + // let the spline start + cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); + + for (int j = mXBounds.min + 1; j <= mXBounds.range + mXBounds.min; j++) { + + prev = cur; + cur = dataSet.getEntryForIndex(j); + + final float cpx = (prev.getX()) + + (cur.getX() - prev.getX()) / 2.0f; + + cubicPath.cubicTo( + cpx, prev.getY() * phaseY, + cpx, cur.getY() * phaseY, + cur.getX(), cur.getY() * phaseY); + } + } + + // if filled is enabled, close the path + if (dataSet.isDrawFilledEnabled()) { + + cubicFillPath.reset(); + cubicFillPath.addPath(cubicPath); + // create a new path, this is bad for performance + drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds); + } + + mRenderPaint.setColor(dataSet.getColor()); + + mRenderPaint.setStyle(Paint.Style.STROKE); + + trans.pathValueToPixel(cubicPath); + + mBitmapCanvas.drawPath(cubicPath, mRenderPaint); + + mRenderPaint.setPathEffect(null); + } + + protected void drawCubicBezier(ILineDataSet dataSet) { + + float phaseY = mAnimator.getPhaseY(); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mXBounds.set(mChart, dataSet); + + float intensity = dataSet.getCubicIntensity(); + + cubicPath.reset(); + + if (mXBounds.range >= 1) { + + float prevDx = 0f; + float prevDy = 0f; + float curDx = 0f; + float curDy = 0f; + + // Take an extra point from the left, and an extra from the right. + // That's because we need 4 points for a cubic bezier (cubic=4), otherwise we get lines moving and doing weird stuff on the edges of the chart. + // So in the starting `prev` and `cur`, go -2, -1 + // And in the `lastIndex`, add +1 + + final int firstIndex = mXBounds.min + 1; + final int lastIndex = mXBounds.min + mXBounds.range; + + Entry prevPrev; + Entry prev = dataSet.getEntryForIndex(Math.max(firstIndex - 2, 0)); + Entry cur = dataSet.getEntryForIndex(Math.max(firstIndex - 1, 0)); + Entry next = cur; + int nextIndex = -1; + + if (cur == null) return; + + // let the spline start + cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); + + for (int j = mXBounds.min + 1; j <= mXBounds.range + mXBounds.min; j++) { + + prevPrev = prev; + prev = cur; + cur = nextIndex == j ? next : dataSet.getEntryForIndex(j); + + nextIndex = j + 1 < dataSet.getEntryCount() ? j + 1 : j; + next = dataSet.getEntryForIndex(nextIndex); + + prevDx = (cur.getX() - prevPrev.getX()) * intensity; + prevDy = (cur.getY() - prevPrev.getY()) * intensity; + curDx = (next.getX() - prev.getX()) * intensity; + curDy = (next.getY() - prev.getY()) * intensity; + + cubicPath.cubicTo(prev.getX() + prevDx, (prev.getY() + prevDy) * phaseY, + cur.getX() - curDx, + (cur.getY() - curDy) * phaseY, cur.getX(), cur.getY() * phaseY); + } + } + + // if filled is enabled, close the path + if (dataSet.isDrawFilledEnabled()) { + + cubicFillPath.reset(); + cubicFillPath.addPath(cubicPath); + + drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds); + } + + mRenderPaint.setColor(dataSet.getColor()); + + mRenderPaint.setStyle(Paint.Style.STROKE); + + trans.pathValueToPixel(cubicPath); + + mBitmapCanvas.drawPath(cubicPath, mRenderPaint); + + mRenderPaint.setPathEffect(null); + } + + protected void drawCubicFill(Canvas c, ILineDataSet dataSet, Path spline, Transformer trans, XBounds bounds) { + + float fillMin = dataSet.getFillFormatter() + .getFillLinePosition(dataSet, mChart); + + spline.lineTo(dataSet.getEntryForIndex(bounds.min + bounds.range).getX(), fillMin); + spline.lineTo(dataSet.getEntryForIndex(bounds.min).getX(), fillMin); + spline.close(); + + trans.pathValueToPixel(spline); + + final Drawable drawable = dataSet.getFillDrawable(); + if (drawable != null) { + + drawFilledPath(c, spline, drawable); + } else { + + drawFilledPath(c, spline, dataSet.getFillColor(), dataSet.getFillAlpha()); + } + } + + private float[] mLineBuffer = new float[4]; + + /** + * Draws a normal line. + * + * @param c + * @param dataSet + */ + protected void drawLinear(Canvas c, ILineDataSet dataSet) { + + int entryCount = dataSet.getEntryCount(); + + final boolean isDrawSteppedEnabled = dataSet.isDrawSteppedEnabled(); + final int pointsPerEntryPair = isDrawSteppedEnabled ? 4 : 2; + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + float phaseY = mAnimator.getPhaseY(); + + mRenderPaint.setStyle(Paint.Style.STROKE); + + Canvas canvas = null; + + // if the data-set is dashed, draw on bitmap-canvas + if (dataSet.isDashedLineEnabled()) { + canvas = mBitmapCanvas; + } else { + canvas = c; + } + + mXBounds.set(mChart, dataSet); + + // if drawing filled is enabled + if (dataSet.isDrawFilledEnabled() && entryCount > 0) { + drawLinearFill(c, dataSet, trans, mXBounds); + } + + // more than 1 color + if (dataSet.getColors().size() > 1) { + + int numberOfFloats = pointsPerEntryPair * 2; + + if (mLineBuffer.length <= numberOfFloats) + mLineBuffer = new float[numberOfFloats * 2]; + + int max = mXBounds.min + mXBounds.range; + + for (int j = mXBounds.min; j < max; j++) { + + Entry e = dataSet.getEntryForIndex(j); + if (e == null) continue; + + mLineBuffer[0] = e.getX(); + mLineBuffer[1] = e.getY() * phaseY; + + if (j < mXBounds.max) { + + e = dataSet.getEntryForIndex(j + 1); + + if (e == null) break; + + if (isDrawSteppedEnabled) { + mLineBuffer[2] = e.getX(); + mLineBuffer[3] = mLineBuffer[1]; + mLineBuffer[4] = mLineBuffer[2]; + mLineBuffer[5] = mLineBuffer[3]; + mLineBuffer[6] = e.getX(); + mLineBuffer[7] = e.getY() * phaseY; + } else { + mLineBuffer[2] = e.getX(); + mLineBuffer[3] = e.getY() * phaseY; + } + + } else { + mLineBuffer[2] = mLineBuffer[0]; + mLineBuffer[3] = mLineBuffer[1]; + } + + // Determine the start and end coordinates of the line, and make sure they differ. + float firstCoordinateX = mLineBuffer[0]; + float firstCoordinateY = mLineBuffer[1]; + float lastCoordinateX = mLineBuffer[numberOfFloats - 2]; + float lastCoordinateY = mLineBuffer[numberOfFloats - 1]; + + if (firstCoordinateX == lastCoordinateX && + firstCoordinateY == lastCoordinateY) + continue; + + trans.pointValuesToPixel(mLineBuffer); + + if (!mViewPortHandler.isInBoundsRight(firstCoordinateX)) + break; + + // make sure the lines don't do shitty things outside + // bounds + if (!mViewPortHandler.isInBoundsLeft(lastCoordinateX) || + !mViewPortHandler.isInBoundsTop(Math.max(firstCoordinateY, lastCoordinateY)) || + !mViewPortHandler.isInBoundsBottom(Math.min(firstCoordinateY, lastCoordinateY))) + continue; + + // get the color that is set for this line-segment + mRenderPaint.setColor(dataSet.getColor(j)); + + canvas.drawLines(mLineBuffer, 0, pointsPerEntryPair * 2, mRenderPaint); + } + + } else { // only one color per dataset + + if (mLineBuffer.length < Math.max((entryCount) * pointsPerEntryPair, pointsPerEntryPair) * 2) + mLineBuffer = new float[Math.max((entryCount) * pointsPerEntryPair, pointsPerEntryPair) * 4]; + + Entry e1, e2; + + e1 = dataSet.getEntryForIndex(mXBounds.min); + + if (e1 != null) { + + int j = 0; + for (int x = mXBounds.min; x <= mXBounds.range + mXBounds.min; x++) { + + e1 = dataSet.getEntryForIndex(x == 0 ? 0 : (x - 1)); + e2 = dataSet.getEntryForIndex(x); + + if (e1 == null || e2 == null) continue; + + mLineBuffer[j++] = e1.getX(); + mLineBuffer[j++] = e1.getY() * phaseY; + + if (isDrawSteppedEnabled) { + mLineBuffer[j++] = e2.getX(); + mLineBuffer[j++] = e1.getY() * phaseY; + mLineBuffer[j++] = e2.getX(); + mLineBuffer[j++] = e1.getY() * phaseY; + } + + mLineBuffer[j++] = e2.getX(); + mLineBuffer[j++] = e2.getY() * phaseY; + } + + if (j > 0) { + trans.pointValuesToPixel(mLineBuffer); + + final int size = Math.max((mXBounds.range + 1) * pointsPerEntryPair, pointsPerEntryPair) * 2; + + mRenderPaint.setColor(dataSet.getColor()); + + canvas.drawLines(mLineBuffer, 0, size, mRenderPaint); + } + } + } + + mRenderPaint.setPathEffect(null); + } + + protected Path mGenerateFilledPathBuffer = new Path(); + + /** + * Draws a filled linear path on the canvas. + * + * @param c + * @param dataSet + * @param trans + * @param bounds + */ + protected void drawLinearFill(Canvas c, ILineDataSet dataSet, Transformer trans, XBounds bounds) { + + final Path filled = mGenerateFilledPathBuffer; + + final int startingIndex = bounds.min; + final int endingIndex = bounds.range + bounds.min; + final int indexInterval = 128; + + int currentStartIndex = 0; + int currentEndIndex = indexInterval; + int iterations = 0; + + // Doing this iteratively in order to avoid OutOfMemory errors that can happen on large bounds sets. + do { + currentStartIndex = startingIndex + (iterations * indexInterval); + currentEndIndex = currentStartIndex + indexInterval; + currentEndIndex = currentEndIndex > endingIndex ? endingIndex : currentEndIndex; + + if (currentStartIndex <= currentEndIndex) { + generateFilledPath(dataSet, currentStartIndex, currentEndIndex, filled); + + trans.pathValueToPixel(filled); + + final Drawable drawable = dataSet.getFillDrawable(); + if (drawable != null) { + + drawFilledPath(c, filled, drawable); + } else { + + drawFilledPath(c, filled, dataSet.getFillColor(), dataSet.getFillAlpha()); + } + } + + iterations++; + + } while (currentStartIndex <= currentEndIndex); + + } + + /** + * Generates a path that is used for filled drawing. + * + * @param dataSet The dataset from which to read the entries. + * @param startIndex The index from which to start reading the dataset + * @param endIndex The index from which to stop reading the dataset + * @param outputPath The path object that will be assigned the chart data. + * @return + */ + private void generateFilledPath(final ILineDataSet dataSet, final int startIndex, final int endIndex, final Path outputPath) { + + final float fillMin = dataSet.getFillFormatter().getFillLinePosition(dataSet, mChart); + final float phaseY = mAnimator.getPhaseY(); + final boolean isDrawSteppedEnabled = dataSet.getMode() == LineDataSet.Mode.STEPPED; + + final Path filled = outputPath; + filled.reset(); + + final Entry entry = dataSet.getEntryForIndex(startIndex); + + filled.moveTo(entry.getX(), fillMin); + filled.lineTo(entry.getX(), entry.getY() * phaseY); + + // create a new path + Entry currentEntry = null; + Entry previousEntry = entry; + for (int x = startIndex + 1; x <= endIndex; x++) { + + currentEntry = dataSet.getEntryForIndex(x); + + if (isDrawSteppedEnabled) { + filled.lineTo(currentEntry.getX(), previousEntry.getY() * phaseY); + } + + filled.lineTo(currentEntry.getX(), currentEntry.getY() * phaseY); + + previousEntry = currentEntry; + } + + // close up + if (currentEntry != null) { + filled.lineTo(currentEntry.getX(), fillMin); + } + + filled.close(); + } + + @Override + public void drawValues(Canvas c) { + + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getLineData().getDataSets(); + + for (int i = 0; i < dataSets.size(); i++) { + + ILineDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + // make sure the values do not interfear with the circles + int valOffset = (int) (dataSet.getCircleRadius() * 1.75f); + + if (!dataSet.isDrawCirclesEnabled()) + valOffset = valOffset / 2; + + mXBounds.set(mChart, dataSet); + + float[] positions = trans.generateTransformedValuesLine(dataSet, mAnimator.getPhaseX(), mAnimator + .getPhaseY(), mXBounds.min, mXBounds.max); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < positions.length; j += 2) { + + float x = positions[j]; + float y = positions[j + 1]; + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y)) + continue; + + Entry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, dataSet.getValueFormatter(), entry.getY(), entry, i, x, + y - valOffset, dataSet.getValueTextColor(j / 2)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawExtras(Canvas c) { + drawCircles(c); + } + + /** + * cache for the circle bitmaps of all datasets + */ + private HashMap mImageCaches = new HashMap<>(); + + /** + * buffer for drawing the circles + */ + private float[] mCirclesBuffer = new float[2]; + + protected void drawCircles(Canvas c) { + + mRenderPaint.setStyle(Paint.Style.FILL); + + float phaseY = mAnimator.getPhaseY(); + + mCirclesBuffer[0] = 0; + mCirclesBuffer[1] = 0; + + List dataSets = mChart.getLineData().getDataSets(); + + for (int i = 0; i < dataSets.size(); i++) { + + ILineDataSet dataSet = dataSets.get(i); + + if (!dataSet.isVisible() || !dataSet.isDrawCirclesEnabled() || + dataSet.getEntryCount() == 0) + continue; + + mCirclePaintInner.setColor(dataSet.getCircleHoleColor()); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mXBounds.set(mChart, dataSet); + + float circleRadius = dataSet.getCircleRadius(); + float circleHoleRadius = dataSet.getCircleHoleRadius(); + boolean drawCircleHole = dataSet.isDrawCircleHoleEnabled() && + circleHoleRadius < circleRadius && + circleHoleRadius > 0.f; + boolean drawTransparentCircleHole = drawCircleHole && + dataSet.getCircleHoleColor() == ColorTemplate.COLOR_NONE; + + DataSetImageCache imageCache; + + if (mImageCaches.containsKey(dataSet)) { + imageCache = mImageCaches.get(dataSet); + } else { + imageCache = new DataSetImageCache(); + mImageCaches.put(dataSet, imageCache); + } + + boolean changeRequired = imageCache.init(dataSet); + + // only fill the cache with new bitmaps if a change is required + if (changeRequired) { + imageCache.fill(dataSet, drawCircleHole, drawTransparentCircleHole); + } + + int boundsRangeCount = mXBounds.range + mXBounds.min; + + for (int j = mXBounds.min; j <= boundsRangeCount; j++) { + + Entry e = dataSet.getEntryForIndex(j); + + if (e == null) break; + + mCirclesBuffer[0] = e.getX(); + mCirclesBuffer[1] = e.getY() * phaseY; + + trans.pointValuesToPixel(mCirclesBuffer); + + if (!mViewPortHandler.isInBoundsRight(mCirclesBuffer[0])) + break; + + if (!mViewPortHandler.isInBoundsLeft(mCirclesBuffer[0]) || + !mViewPortHandler.isInBoundsY(mCirclesBuffer[1])) + continue; + + Bitmap circleBitmap = imageCache.getBitmap(j); + + if (circleBitmap != null) { + c.drawBitmap(circleBitmap, mCirclesBuffer[0] - circleRadius, mCirclesBuffer[1] - circleRadius, null); + } + } + } + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + LineData lineData = mChart.getLineData(); + + for (Highlight high : indices) { + + ILineDataSet set = lineData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + Entry e = set.getEntryForXValue(high.getX(), high.getY()); + + if (!isInBoundsX(e, set)) + continue; + + MPPointD pix = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(e.getX(), e.getY() * mAnimator + .getPhaseY()); + + high.setDraw((float) pix.x, (float) pix.y); + + // draw the lines + drawHighlightLines(c, (float) pix.x, (float) pix.y, set); + } + } + + /** + * Sets the Bitmap.Config to be used by this renderer. + * Default: Bitmap.Config.ARGB_8888 + * Use Bitmap.Config.ARGB_4444 to consume less memory. + * + * @param config + */ + public void setBitmapConfig(Bitmap.Config config) { + mBitmapConfig = config; + releaseBitmap(); + } + + /** + * Returns the Bitmap.Config that is used by this renderer. + * + * @return + */ + public Bitmap.Config getBitmapConfig() { + return mBitmapConfig; + } + + /** + * Releases the drawing bitmap. This should be called when {@link LineChart#onDetachedFromWindow()}. + */ + public void releaseBitmap() { + if (mBitmapCanvas != null) { + mBitmapCanvas.setBitmap(null); + mBitmapCanvas = null; + } + if (mDrawBitmap != null) { + Bitmap drawBitmap = mDrawBitmap.get(); + if (drawBitmap != null) { + drawBitmap.recycle(); + } + mDrawBitmap.clear(); + mDrawBitmap = null; + } + } + + private class DataSetImageCache { + + private Path mCirclePathBuffer = new Path(); + + private Bitmap[] circleBitmaps; + + /** + * Sets up the cache, returns true if a change of cache was required. + * + * @param set + * @return + */ + protected boolean init(ILineDataSet set) { + + int size = set.getCircleColorCount(); + boolean changeRequired = false; + + if (circleBitmaps == null) { + circleBitmaps = new Bitmap[size]; + changeRequired = true; + } else if (circleBitmaps.length != size) { + circleBitmaps = new Bitmap[size]; + changeRequired = true; + } + + return changeRequired; + } + + /** + * Fills the cache with bitmaps for the given dataset. + * + * @param set + * @param drawCircleHole + * @param drawTransparentCircleHole + */ + protected void fill(ILineDataSet set, boolean drawCircleHole, boolean drawTransparentCircleHole) { + + int colorCount = set.getCircleColorCount(); + float circleRadius = set.getCircleRadius(); + float circleHoleRadius = set.getCircleHoleRadius(); + + for (int i = 0; i < colorCount; i++) { + + Bitmap.Config conf = Bitmap.Config.ARGB_4444; + Bitmap circleBitmap = Bitmap.createBitmap((int) (circleRadius * 2.1), (int) (circleRadius * 2.1), conf); + + Canvas canvas = new Canvas(circleBitmap); + circleBitmaps[i] = circleBitmap; + mRenderPaint.setColor(set.getCircleColor(i)); + + if (drawTransparentCircleHole) { + // Begin path for circle with hole + mCirclePathBuffer.reset(); + + mCirclePathBuffer.addCircle( + circleRadius, + circleRadius, + circleRadius, + Path.Direction.CW); + + // Cut hole in path + mCirclePathBuffer.addCircle( + circleRadius, + circleRadius, + circleHoleRadius, + Path.Direction.CCW); + + // Fill in-between + canvas.drawPath(mCirclePathBuffer, mRenderPaint); + } else { + + canvas.drawCircle( + circleRadius, + circleRadius, + circleRadius, + mRenderPaint); + + if (drawCircleHole) { + canvas.drawCircle( + circleRadius, + circleRadius, + circleHoleRadius, + mCirclePaintInner); + } + } + } + } + + /** + * Returns the cached Bitmap at the given index. + * + * @param index + * @return + */ + protected Bitmap getBitmap(int index) { + return circleBitmaps[index % circleBitmaps.length]; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineRadarRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineRadarRenderer.java new file mode 100644 index 0000000..288eff6 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineRadarRenderer.java @@ -0,0 +1,95 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 25/01/16. + */ +public abstract class LineRadarRenderer extends LineScatterCandleRadarRenderer { + + public LineRadarRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + } + + /** + * Draws the provided path in filled mode with the provided drawable. + * + * @param c + * @param filledPath + * @param drawable + */ + protected void drawFilledPath(Canvas c, Path filledPath, Drawable drawable) { + + if (clipPathSupported()) { + + int save = c.save(); + c.clipPath(filledPath); + + drawable.setBounds((int) mViewPortHandler.contentLeft(), + (int) mViewPortHandler.contentTop(), + (int) mViewPortHandler.contentRight(), + (int) mViewPortHandler.contentBottom()); + drawable.draw(c); + + c.restoreToCount(save); + } else { + throw new RuntimeException("Fill-drawables not (yet) supported below API level 18, " + + "this code was run on API level " + Utils.getSDKInt() + "."); + } + } + + /** + * Draws the provided path in filled mode with the provided color and alpha. + * Special thanks to Angelo Suzuki (https://github.com/tinsukE) for this. + * + * @param c + * @param filledPath + * @param fillColor + * @param fillAlpha + */ + protected void drawFilledPath(Canvas c, Path filledPath, int fillColor, int fillAlpha) { + + int color = (fillAlpha << 24) | (fillColor & 0xffffff); + + if (clipPathSupported()) { + + int save = c.save(); + + c.clipPath(filledPath); + + c.drawColor(color); + c.restoreToCount(save); + } else { + + // save + Paint.Style previous = mRenderPaint.getStyle(); + int previousColor = mRenderPaint.getColor(); + + // set + mRenderPaint.setStyle(Paint.Style.FILL); + mRenderPaint.setColor(color); + + c.drawPath(filledPath, mRenderPaint); + + // restore + mRenderPaint.setColor(previousColor); + mRenderPaint.setStyle(previous); + } + } + + /** + * Clip path with hardware acceleration only working properly on API level 18 and above. + * + * @return + */ + private boolean clipPathSupported() { + return Utils.getSDKInt() >= 18; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineScatterCandleRadarRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineScatterCandleRadarRenderer.java new file mode 100644 index 0000000..53f54cb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineScatterCandleRadarRenderer.java @@ -0,0 +1,63 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Path; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.interfaces.datasets.ILineScatterCandleRadarDataSet; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 11/07/15. + */ +public abstract class LineScatterCandleRadarRenderer extends BarLineScatterCandleBubbleRenderer { + + /** + * path that is used for drawing highlight-lines (drawLines(...) cannot be used because of dashes) + */ + private Path mHighlightLinePath = new Path(); + + public LineScatterCandleRadarRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + } + + /** + * Draws vertical & horizontal highlight-lines if enabled. + * + * @param c + * @param x x-position of the highlight line intersection + * @param y y-position of the highlight line intersection + * @param set the currently drawn dataset + */ + protected void drawHighlightLines(Canvas c, float x, float y, ILineScatterCandleRadarDataSet set) { + + // set color and stroke-width + mHighlightPaint.setColor(set.getHighLightColor()); + mHighlightPaint.setStrokeWidth(set.getHighlightLineWidth()); + + // draw highlighted lines (if enabled) + mHighlightPaint.setPathEffect(set.getDashPathEffectHighlight()); + + // draw vertical highlight lines + if (set.isVerticalHighlightIndicatorEnabled()) { + + // create vertical path + mHighlightLinePath.reset(); + mHighlightLinePath.moveTo(x, mViewPortHandler.contentTop()); + mHighlightLinePath.lineTo(x, mViewPortHandler.contentBottom()); + + c.drawPath(mHighlightLinePath, mHighlightPaint); + } + + // draw horizontal highlight lines + if (set.isHorizontalHighlightIndicatorEnabled()) { + + // create horizontal path + mHighlightLinePath.reset(); + mHighlightLinePath.moveTo(mViewPortHandler.contentLeft(), y); + mHighlightLinePath.lineTo(mViewPortHandler.contentRight(), y); + + c.drawPath(mHighlightLinePath, mHighlightPaint); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/PieChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/PieChartRenderer.java new file mode 100644 index 0000000..f35c775 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/PieChartRenderer.java @@ -0,0 +1,1070 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.lang.ref.WeakReference; +import java.util.List; + +public class PieChartRenderer extends DataRenderer { + + protected PieChart mChart; + + /** + * paint for the hole in the center of the pie chart and the transparent + * circle + */ + protected Paint mHolePaint; + protected Paint mTransparentCirclePaint; + protected Paint mValueLinePaint; + + /** + * paint object for the text that can be displayed in the center of the + * chart + */ + private TextPaint mCenterTextPaint; + + /** + * paint object used for drwing the slice-text + */ + private Paint mEntryLabelsPaint; + + private StaticLayout mCenterTextLayout; + private CharSequence mCenterTextLastValue; + private RectF mCenterTextLastBounds = new RectF(); + private RectF[] mRectBuffer = {new RectF(), new RectF(), new RectF()}; + + /** + * Bitmap for drawing the center hole + */ + protected WeakReference mDrawBitmap; + + protected Canvas mBitmapCanvas; + + public PieChartRenderer(PieChart chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + + mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mHolePaint.setColor(Color.WHITE); + mHolePaint.setStyle(Style.FILL); + + mTransparentCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTransparentCirclePaint.setColor(Color.WHITE); + mTransparentCirclePaint.setStyle(Style.FILL); + mTransparentCirclePaint.setAlpha(105); + + mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + mCenterTextPaint.setColor(Color.BLACK); + mCenterTextPaint.setTextSize(Utils.convertDpToPixel(12f)); + + mValuePaint.setTextSize(Utils.convertDpToPixel(13f)); + mValuePaint.setColor(Color.WHITE); + mValuePaint.setTextAlign(Align.CENTER); + + mEntryLabelsPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mEntryLabelsPaint.setColor(Color.WHITE); + mEntryLabelsPaint.setTextAlign(Align.CENTER); + mEntryLabelsPaint.setTextSize(Utils.convertDpToPixel(13f)); + + mValueLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mValueLinePaint.setStyle(Style.STROKE); + } + + public Paint getPaintHole() { + return mHolePaint; + } + + public Paint getPaintTransparentCircle() { + return mTransparentCirclePaint; + } + + public TextPaint getPaintCenterText() { + return mCenterTextPaint; + } + + public Paint getPaintEntryLabels() { + return mEntryLabelsPaint; + } + + @Override + public void initBuffers() { + // TODO Auto-generated method stub + } + + @Override + public void drawData(Canvas c) { + + int width = (int) mViewPortHandler.getChartWidth(); + int height = (int) mViewPortHandler.getChartHeight(); + + Bitmap drawBitmap = mDrawBitmap == null ? null : mDrawBitmap.get(); + + if (drawBitmap == null + || (drawBitmap.getWidth() != width) + || (drawBitmap.getHeight() != height)) { + + if (width > 0 && height > 0) { + drawBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); + mDrawBitmap = new WeakReference<>(drawBitmap); + mBitmapCanvas = new Canvas(drawBitmap); + } else + return; + } + + drawBitmap.eraseColor(Color.TRANSPARENT); + + PieData pieData = mChart.getData(); + + for (IPieDataSet set : pieData.getDataSets()) { + + if (set.isVisible() && set.getEntryCount() > 0) + drawDataSet(c, set); + } + } + + private Path mPathBuffer = new Path(); + private RectF mInnerRectBuffer = new RectF(); + + protected float calculateMinimumRadiusForSpacedSlice( + MPPointF center, + float radius, + float angle, + float arcStartPointX, + float arcStartPointY, + float startAngle, + float sweepAngle) { + final float angleMiddle = startAngle + sweepAngle / 2.f; + + // Other point of the arc + float arcEndPointX = center.x + radius * (float) Math.cos((startAngle + sweepAngle) * Utils.FDEG2RAD); + float arcEndPointY = center.y + radius * (float) Math.sin((startAngle + sweepAngle) * Utils.FDEG2RAD); + + // Middle point on the arc + float arcMidPointX = center.x + radius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); + float arcMidPointY = center.y + radius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); + + // This is the base of the contained triangle + double basePointsDistance = Math.sqrt( + Math.pow(arcEndPointX - arcStartPointX, 2) + + Math.pow(arcEndPointY - arcStartPointY, 2)); + + // After reducing space from both sides of the "slice", + // the angle of the contained triangle should stay the same. + // So let's find out the height of that triangle. + float containedTriangleHeight = (float) (basePointsDistance / 2.0 * + Math.tan((180.0 - angle) / 2.0 * Utils.DEG2RAD)); + + // Now we subtract that from the radius + float spacedRadius = radius - containedTriangleHeight; + + // And now subtract the height of the arc that's between the triangle and the outer circle + spacedRadius -= Math.sqrt( + Math.pow(arcMidPointX - (arcEndPointX + arcStartPointX) / 2.f, 2) + + Math.pow(arcMidPointY - (arcEndPointY + arcStartPointY) / 2.f, 2)); + + return spacedRadius; + } + + /** + * Calculates the sliceSpace to use based on visible values and their size compared to the set sliceSpace. + * + * @param dataSet + * @return + */ + protected float getSliceSpace(IPieDataSet dataSet) { + + if (!dataSet.isAutomaticallyDisableSliceSpacingEnabled()) + return dataSet.getSliceSpace(); + + float spaceSizeRatio = dataSet.getSliceSpace() / mViewPortHandler.getSmallestContentExtension(); + float minValueRatio = dataSet.getYMin() / mChart.getData().getYValueSum() * 2; + + float sliceSpace = spaceSizeRatio > minValueRatio ? 0f : dataSet.getSliceSpace(); + + return sliceSpace; + } + + protected void drawDataSet(Canvas c, IPieDataSet dataSet) { + + float angle = 0; + float rotationAngle = mChart.getRotationAngle(); + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + final RectF circleBox = mChart.getCircleBox(); + + final int entryCount = dataSet.getEntryCount(); + final float[] drawAngles = mChart.getDrawAngles(); + final MPPointF center = mChart.getCenterCircleBox(); + final float radius = mChart.getRadius(); + final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); + final float userInnerRadius = drawInnerArc + ? radius * (mChart.getHoleRadius() / 100.f) + : 0.f; + final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f; + final RectF roundedCircleBox = new RectF(); + final boolean drawRoundedSlices = drawInnerArc && mChart.isDrawRoundedSlicesEnabled(); + + int visibleAngleCount = 0; + for (int j = 0; j < entryCount; j++) { + // draw only if the value is greater than zero + if ((Math.abs(dataSet.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { + visibleAngleCount++; + } + } + + final float sliceSpace = visibleAngleCount <= 1 ? 0.f : getSliceSpace(dataSet); + + for (int j = 0; j < entryCount; j++) { + + float sliceAngle = drawAngles[j]; + float innerRadius = userInnerRadius; + + Entry e = dataSet.getEntryForIndex(j); + + // draw only if the value is greater than zero + if (!(Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { + angle += sliceAngle * phaseX; + continue; + } + + // Don't draw if it's highlighted, unless the chart uses rounded slices + if (dataSet.isHighlightEnabled() && mChart.needsHighlight(j) && !drawRoundedSlices) { + angle += sliceAngle * phaseX; + continue; + } + + final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; + + mRenderPaint.setColor(dataSet.getColor(j)); + + final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * radius); + final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; + float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; + if (sweepAngleOuter < 0.f) { + sweepAngleOuter = 0.f; + } + + mPathBuffer.reset(); + + if (drawRoundedSlices) { + float x = center.x + (radius - roundedRadius) * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); + float y = center.y + (radius - roundedRadius) * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); + roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); + } + + float arcStartPointX = center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); + float arcStartPointY = center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); + + if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { + // Android is doing "mod 360" + mPathBuffer.addCircle(center.x, center.y, radius, Path.Direction.CW); + } else { + + if (drawRoundedSlices) { + mPathBuffer.arcTo(roundedCircleBox, startAngleOuter + 180, -180); + } + + mPathBuffer.arcTo( + circleBox, + startAngleOuter, + sweepAngleOuter + ); + } + + // API < 21 does not receive floats in addArc, but a RectF + mInnerRectBuffer.set( + center.x - innerRadius, + center.y - innerRadius, + center.x + innerRadius, + center.y + innerRadius); + + if (drawInnerArc && (innerRadius > 0.f || accountForSliceSpacing)) { + + if (accountForSliceSpacing) { + float minSpacedRadius = + calculateMinimumRadiusForSpacedSlice( + center, radius, + sliceAngle * phaseY, + arcStartPointX, arcStartPointY, + startAngleOuter, + sweepAngleOuter); + + if (minSpacedRadius < 0.f) + minSpacedRadius = -minSpacedRadius; + + innerRadius = Math.max(innerRadius, minSpacedRadius); + } + + final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * innerRadius); + final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; + float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; + if (sweepAngleInner < 0.f) { + sweepAngleInner = 0.f; + } + final float endAngleInner = startAngleInner + sweepAngleInner; + + if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { + // Android is doing "mod 360" + mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); + } else { + + if (drawRoundedSlices) { + float x = center.x + (radius - roundedRadius) * (float) Math.cos(endAngleInner * Utils.FDEG2RAD); + float y = center.y + (radius - roundedRadius) * (float) Math.sin(endAngleInner * Utils.FDEG2RAD); + roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); + mPathBuffer.arcTo(roundedCircleBox, endAngleInner, 180); + } else + mPathBuffer.lineTo( + center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), + center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); + + mPathBuffer.arcTo( + mInnerRectBuffer, + endAngleInner, + -sweepAngleInner + ); + } + } else { + + if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) { + if (accountForSliceSpacing) { + + float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; + + float sliceSpaceOffset = + calculateMinimumRadiusForSpacedSlice( + center, + radius, + sliceAngle * phaseY, + arcStartPointX, + arcStartPointY, + startAngleOuter, + sweepAngleOuter); + + float arcEndPointX = center.x + + sliceSpaceOffset * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); + float arcEndPointY = center.y + + sliceSpaceOffset * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); + + mPathBuffer.lineTo( + arcEndPointX, + arcEndPointY); + + } else { + mPathBuffer.lineTo( + center.x, + center.y); + } + } + + } + + mPathBuffer.close(); + + mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); + + angle += sliceAngle * phaseX; + } + + MPPointF.recycleInstance(center); + } + + @Override + public void drawValues(Canvas c) { + + MPPointF center = mChart.getCenterCircleBox(); + + // get whole the radius + float radius = mChart.getRadius(); + float rotationAngle = mChart.getRotationAngle(); + float[] drawAngles = mChart.getDrawAngles(); + float[] absoluteAngles = mChart.getAbsoluteAngles(); + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f; + final float holeRadiusPercent = mChart.getHoleRadius() / 100.f; + float labelRadiusOffset = radius / 10f * 3.6f; + + if (mChart.isDrawHoleEnabled()) { + labelRadiusOffset = (radius - (radius * holeRadiusPercent)) / 2f; + + if (!mChart.isDrawSlicesUnderHoleEnabled() && mChart.isDrawRoundedSlicesEnabled()) { + // Add curved circle slice and spacing to rotation angle, so that it sits nicely inside + rotationAngle += roundedRadius * 360 / (Math.PI * 2 * radius); + } + } + + final float labelRadius = radius - labelRadiusOffset; + + PieData data = mChart.getData(); + List dataSets = data.getDataSets(); + + float yValueSum = data.getYValueSum(); + + boolean drawEntryLabels = mChart.isDrawEntryLabelsEnabled(); + + float angle; + int xIndex = 0; + + c.save(); + + float offset = Utils.convertDpToPixel(5.f); + + for (int i = 0; i < dataSets.size(); i++) { + + IPieDataSet dataSet = dataSets.get(i); + + final boolean drawValues = dataSet.isDrawValuesEnabled(); + + if (!drawValues && !drawEntryLabels) + continue; + + final PieDataSet.ValuePosition xValuePosition = dataSet.getXValuePosition(); + final PieDataSet.ValuePosition yValuePosition = dataSet.getYValuePosition(); + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + float lineHeight = Utils.calcTextHeight(mValuePaint, "Q") + + Utils.convertDpToPixel(4f); + + IValueFormatter formatter = dataSet.getValueFormatter(); + + int entryCount = dataSet.getEntryCount(); + + boolean isUseValueColorForLineEnabled = dataSet.isUseValueColorForLineEnabled(); + int valueLineColor = dataSet.getValueLineColor(); + + mValueLinePaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getValueLineWidth())); + + final float sliceSpace = getSliceSpace(dataSet); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < entryCount; j++) { + + PieEntry entry = dataSet.getEntryForIndex(j); + + if (xIndex == 0) + angle = 0.f; + else + angle = absoluteAngles[xIndex - 1] * phaseX; + + final float sliceAngle = drawAngles[xIndex]; + final float sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius); + + // offset needed to center the drawn text in the slice + final float angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2.f) / 2.f; + + angle = angle + angleOffset; + + final float transformedAngle = rotationAngle + angle * phaseY; + + float value = mChart.isUsePercentValuesEnabled() ? entry.getY() + / yValueSum * 100f : entry.getY(); + String entryLabel = entry.getLabel(); + + final float sliceXBase = (float) Math.cos(transformedAngle * Utils.FDEG2RAD); + final float sliceYBase = (float) Math.sin(transformedAngle * Utils.FDEG2RAD); + + final boolean drawXOutside = drawEntryLabels && + xValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; + final boolean drawYOutside = drawValues && + yValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; + final boolean drawXInside = drawEntryLabels && + xValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; + final boolean drawYInside = drawValues && + yValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; + + if (drawXOutside || drawYOutside) { + + final float valueLineLength1 = dataSet.getValueLinePart1Length(); + final float valueLineLength2 = dataSet.getValueLinePart2Length(); + final float valueLinePart1OffsetPercentage = dataSet.getValueLinePart1OffsetPercentage() / 100.f; + + float pt2x, pt2y; + float labelPtx, labelPty; + + float line1Radius; + + if (mChart.isDrawHoleEnabled()) + line1Radius = (radius - (radius * holeRadiusPercent)) + * valueLinePart1OffsetPercentage + + (radius * holeRadiusPercent); + else + line1Radius = radius * valueLinePart1OffsetPercentage; + + final float polyline2Width = dataSet.isValueLineVariableLength() + ? labelRadius * valueLineLength2 * (float) Math.abs(Math.sin( + transformedAngle * Utils.FDEG2RAD)) + : labelRadius * valueLineLength2; + + final float pt0x = line1Radius * sliceXBase + center.x; + final float pt0y = line1Radius * sliceYBase + center.y; + + final float pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x; + final float pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y; + + if (transformedAngle % 360.0 >= 90.0 && transformedAngle % 360.0 <= 270.0) { + pt2x = pt1x - polyline2Width; + pt2y = pt1y; + + mValuePaint.setTextAlign(Align.RIGHT); + + if(drawXOutside) + mEntryLabelsPaint.setTextAlign(Align.RIGHT); + + labelPtx = pt2x - offset; + labelPty = pt2y; + } else { + pt2x = pt1x + polyline2Width; + pt2y = pt1y; + mValuePaint.setTextAlign(Align.LEFT); + + if(drawXOutside) + mEntryLabelsPaint.setTextAlign(Align.LEFT); + + labelPtx = pt2x + offset; + labelPty = pt2y; + } + + int lineColor = ColorTemplate.COLOR_NONE; + + if (isUseValueColorForLineEnabled) + lineColor = dataSet.getColor(j); + else if (valueLineColor != ColorTemplate.COLOR_NONE) + lineColor = valueLineColor; + + if (lineColor != ColorTemplate.COLOR_NONE) { + mValueLinePaint.setColor(lineColor); + c.drawLine(pt0x, pt0y, pt1x, pt1y, mValueLinePaint); + c.drawLine(pt1x, pt1y, pt2x, pt2y, mValueLinePaint); + } + + // draw everything, depending on settings + if (drawXOutside && drawYOutside) { + + drawValue(c, + formatter, + value, + entry, + 0, + labelPtx, + labelPty, + dataSet.getValueTextColor(j)); + + if (j < data.getEntryCount() && entryLabel != null) { + drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight); + } + + } else if (drawXOutside) { + if (j < data.getEntryCount() && entryLabel != null) { + drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight / 2.f); + } + } else if (drawYOutside) { + + drawValue(c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2.f, dataSet + .getValueTextColor(j)); + } + } + + if (drawXInside || drawYInside) { + // calculate the text position + float x = labelRadius * sliceXBase + center.x; + float y = labelRadius * sliceYBase + center.y; + + mValuePaint.setTextAlign(Align.CENTER); + + // draw everything, depending on settings + if (drawXInside && drawYInside) { + + drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j)); + + if (j < data.getEntryCount() && entryLabel != null) { + drawEntryLabel(c, entryLabel, x, y + lineHeight); + } + + } else if (drawXInside) { + if (j < data.getEntryCount() && entryLabel != null) { + drawEntryLabel(c, entryLabel, x, y + lineHeight / 2f); + } + } else if (drawYInside) { + + drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)); + } + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float x = (labelRadius + iconsOffset.y) * sliceXBase + center.x; + float y = (labelRadius + iconsOffset.y) * sliceYBase + center.y; + y += iconsOffset.x; + + Utils.drawImage( + c, + icon, + (int)x, + (int)y, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + + xIndex++; + } + + MPPointF.recycleInstance(iconsOffset); + } + MPPointF.recycleInstance(center); + c.restore(); + } + + /** + * Draws an entry label at the specified position. + * + * @param c + * @param label + * @param x + * @param y + */ + protected void drawEntryLabel(Canvas c, String label, float x, float y) { + c.drawText(label, x, y, mEntryLabelsPaint); + } + + @Override + public void drawExtras(Canvas c) { + drawHole(c); + c.drawBitmap(mDrawBitmap.get(), 0, 0, null); + drawCenterText(c); + } + + private Path mHoleCirclePath = new Path(); + + /** + * draws the hole in the center of the chart and the transparent circle / + * hole + */ + protected void drawHole(Canvas c) { + + if (mChart.isDrawHoleEnabled() && mBitmapCanvas != null) { + + float radius = mChart.getRadius(); + float holeRadius = radius * (mChart.getHoleRadius() / 100); + MPPointF center = mChart.getCenterCircleBox(); + + if (Color.alpha(mHolePaint.getColor()) > 0) { + // draw the hole-circle + mBitmapCanvas.drawCircle( + center.x, center.y, + holeRadius, mHolePaint); + } + + // only draw the circle if it can be seen (not covered by the hole) + if (Color.alpha(mTransparentCirclePaint.getColor()) > 0 && + mChart.getTransparentCircleRadius() > mChart.getHoleRadius()) { + + int alpha = mTransparentCirclePaint.getAlpha(); + float secondHoleRadius = radius * (mChart.getTransparentCircleRadius() / 100); + + mTransparentCirclePaint.setAlpha((int) ((float) alpha * mAnimator.getPhaseX() * mAnimator.getPhaseY())); + + // draw the transparent-circle + mHoleCirclePath.reset(); + mHoleCirclePath.addCircle(center.x, center.y, secondHoleRadius, Path.Direction.CW); + mHoleCirclePath.addCircle(center.x, center.y, holeRadius, Path.Direction.CCW); + mBitmapCanvas.drawPath(mHoleCirclePath, mTransparentCirclePaint); + + // reset alpha + mTransparentCirclePaint.setAlpha(alpha); + } + MPPointF.recycleInstance(center); + } + } + + protected Path mDrawCenterTextPathBuffer = new Path(); + /** + * draws the description text in the center of the pie chart makes most + * sense when center-hole is enabled + */ + protected void drawCenterText(Canvas c) { + + CharSequence centerText = mChart.getCenterText(); + + if (mChart.isDrawCenterTextEnabled() && centerText != null) { + + MPPointF center = mChart.getCenterCircleBox(); + MPPointF offset = mChart.getCenterTextOffset(); + + float x = center.x + offset.x; + float y = center.y + offset.y; + + float innerRadius = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled() + ? mChart.getRadius() * (mChart.getHoleRadius() / 100f) + : mChart.getRadius(); + + RectF holeRect = mRectBuffer[0]; + holeRect.left = x - innerRadius; + holeRect.top = y - innerRadius; + holeRect.right = x + innerRadius; + holeRect.bottom = y + innerRadius; + RectF boundingRect = mRectBuffer[1]; + boundingRect.set(holeRect); + + float radiusPercent = mChart.getCenterTextRadiusPercent() / 100f; + if (radiusPercent > 0.0) { + boundingRect.inset( + (boundingRect.width() - boundingRect.width() * radiusPercent) / 2.f, + (boundingRect.height() - boundingRect.height() * radiusPercent) / 2.f + ); + } + + if (!centerText.equals(mCenterTextLastValue) || !boundingRect.equals(mCenterTextLastBounds)) { + + // Next time we won't recalculate StaticLayout... + mCenterTextLastBounds.set(boundingRect); + mCenterTextLastValue = centerText; + + float width = mCenterTextLastBounds.width(); + + // If width is 0, it will crash. Always have a minimum of 1 + mCenterTextLayout = new StaticLayout(centerText, 0, centerText.length(), + mCenterTextPaint, + (int) Math.max(Math.ceil(width), 1.f), + Layout.Alignment.ALIGN_CENTER, 1.f, 0.f, false); + } + + //float layoutWidth = Utils.getStaticLayoutMaxWidth(mCenterTextLayout); + float layoutHeight = mCenterTextLayout.getHeight(); + + c.save(); + if (Build.VERSION.SDK_INT >= 18) { + Path path = mDrawCenterTextPathBuffer; + path.reset(); + path.addOval(holeRect, Path.Direction.CW); + c.clipPath(path); + } + + c.translate(boundingRect.left, boundingRect.top + (boundingRect.height() - layoutHeight) / 2.f); + mCenterTextLayout.draw(c); + + c.restore(); + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(offset); + } + } + + protected RectF mDrawHighlightedRectF = new RectF(); + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + /* Skip entirely if using rounded circle slices, because it doesn't make sense to highlight + * in this way. + * TODO: add support for changing slice color with highlighting rather than only shifting the slice + */ + + final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); + if (drawInnerArc && mChart.isDrawRoundedSlicesEnabled()) + return; + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + float angle; + float rotationAngle = mChart.getRotationAngle(); + + float[] drawAngles = mChart.getDrawAngles(); + float[] absoluteAngles = mChart.getAbsoluteAngles(); + final MPPointF center = mChart.getCenterCircleBox(); + final float radius = mChart.getRadius(); + final float userInnerRadius = drawInnerArc + ? radius * (mChart.getHoleRadius() / 100.f) + : 0.f; + + final RectF highlightedCircleBox = mDrawHighlightedRectF; + highlightedCircleBox.set(0,0,0,0); + + for (int i = 0; i < indices.length; i++) { + + // get the index to highlight + int index = (int) indices[i].getX(); + + if (index >= drawAngles.length) + continue; + + IPieDataSet set = mChart.getData() + .getDataSetByIndex(indices[i].getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + final int entryCount = set.getEntryCount(); + int visibleAngleCount = 0; + for (int j = 0; j < entryCount; j++) { + // draw only if the value is greater than zero + if ((Math.abs(set.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { + visibleAngleCount++; + } + } + + if (index == 0) + angle = 0.f; + else + angle = absoluteAngles[index - 1] * phaseX; + + final float sliceSpace = visibleAngleCount <= 1 ? 0.f : set.getSliceSpace(); + + float sliceAngle = drawAngles[index]; + float innerRadius = userInnerRadius; + + float shift = set.getSelectionShift(); + final float highlightedRadius = radius + shift; + highlightedCircleBox.set(mChart.getCircleBox()); + highlightedCircleBox.inset(-shift, -shift); + + final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; + + Integer highlightColor = set.getHighlightColor(); + if (highlightColor == null) + highlightColor = set.getColor(index); + mRenderPaint.setColor(highlightColor); + + final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * radius); + + final float sliceSpaceAngleShifted = visibleAngleCount == 1 ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * highlightedRadius); + + final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; + float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; + if (sweepAngleOuter < 0.f) { + sweepAngleOuter = 0.f; + } + + final float startAngleShifted = rotationAngle + (angle + sliceSpaceAngleShifted / 2.f) * phaseY; + float sweepAngleShifted = (sliceAngle - sliceSpaceAngleShifted) * phaseY; + if (sweepAngleShifted < 0.f) { + sweepAngleShifted = 0.f; + } + + mPathBuffer.reset(); + + if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { + // Android is doing "mod 360" + mPathBuffer.addCircle(center.x, center.y, highlightedRadius, Path.Direction.CW); + } else { + + mPathBuffer.moveTo( + center.x + highlightedRadius * (float) Math.cos(startAngleShifted * Utils.FDEG2RAD), + center.y + highlightedRadius * (float) Math.sin(startAngleShifted * Utils.FDEG2RAD)); + + mPathBuffer.arcTo( + highlightedCircleBox, + startAngleShifted, + sweepAngleShifted + ); + } + + float sliceSpaceRadius = 0.f; + if (accountForSliceSpacing) { + sliceSpaceRadius = + calculateMinimumRadiusForSpacedSlice( + center, radius, + sliceAngle * phaseY, + center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD), + center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD), + startAngleOuter, + sweepAngleOuter); + } + + // API < 21 does not receive floats in addArc, but a RectF + mInnerRectBuffer.set( + center.x - innerRadius, + center.y - innerRadius, + center.x + innerRadius, + center.y + innerRadius); + + if (drawInnerArc && + (innerRadius > 0.f || accountForSliceSpacing)) { + + if (accountForSliceSpacing) { + float minSpacedRadius = sliceSpaceRadius; + + if (minSpacedRadius < 0.f) + minSpacedRadius = -minSpacedRadius; + + innerRadius = Math.max(innerRadius, minSpacedRadius); + } + + final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * innerRadius); + final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; + float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; + if (sweepAngleInner < 0.f) { + sweepAngleInner = 0.f; + } + final float endAngleInner = startAngleInner + sweepAngleInner; + + if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { + // Android is doing "mod 360" + mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); + } else { + + mPathBuffer.lineTo( + center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), + center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); + + mPathBuffer.arcTo( + mInnerRectBuffer, + endAngleInner, + -sweepAngleInner + ); + } + } else { + + if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) { + + if (accountForSliceSpacing) { + final float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; + + final float arcEndPointX = center.x + + sliceSpaceRadius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); + final float arcEndPointY = center.y + + sliceSpaceRadius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); + + mPathBuffer.lineTo( + arcEndPointX, + arcEndPointY); + + } else { + + mPathBuffer.lineTo( + center.x, + center.y); + } + + } + + } + + mPathBuffer.close(); + + mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); + } + + MPPointF.recycleInstance(center); + } + + /** + * This gives all pie-slices a rounded edge. + * + * @param c + */ + protected void drawRoundedSlices(Canvas c) { + + if (!mChart.isDrawRoundedSlicesEnabled()) + return; + + IPieDataSet dataSet = mChart.getData().getDataSet(); + + if (!dataSet.isVisible()) + return; + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + MPPointF center = mChart.getCenterCircleBox(); + float r = mChart.getRadius(); + + // calculate the radius of the "slice-circle" + float circleRadius = (r - (r * mChart.getHoleRadius() / 100f)) / 2f; + + float[] drawAngles = mChart.getDrawAngles(); + float angle = mChart.getRotationAngle(); + + for (int j = 0; j < dataSet.getEntryCount(); j++) { + + float sliceAngle = drawAngles[j]; + + Entry e = dataSet.getEntryForIndex(j); + + // draw only if the value is greater than zero + if ((Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { + + float x = (float) ((r - circleRadius) + * Math.cos(Math.toRadians((angle + sliceAngle) + * phaseY)) + center.x); + float y = (float) ((r - circleRadius) + * Math.sin(Math.toRadians((angle + sliceAngle) + * phaseY)) + center.y); + + mRenderPaint.setColor(dataSet.getColor(j)); + mBitmapCanvas.drawCircle(x, y, circleRadius, mRenderPaint); + } + + angle += sliceAngle * phaseX; + } + MPPointF.recycleInstance(center); + } + + /** + * Releases the drawing bitmap. This should be called when {@link LineChart#onDetachedFromWindow()}. + */ + public void releaseBitmap() { + if (mBitmapCanvas != null) { + mBitmapCanvas.setBitmap(null); + mBitmapCanvas = null; + } + if (mDrawBitmap != null) { + Bitmap drawBitmap = mDrawBitmap.get(); + if (drawBitmap != null) { + drawBitmap.recycle(); + } + mDrawBitmap.clear(); + mDrawBitmap = null; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RadarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RadarChartRenderer.java new file mode 100644 index 0000000..dbf0e8f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RadarChartRenderer.java @@ -0,0 +1,398 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.charts.RadarChart; +import com.github.mikephil.charting.data.RadarData; +import com.github.mikephil.charting.data.RadarEntry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IRadarDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +public class RadarChartRenderer extends LineRadarRenderer { + + protected RadarChart mChart; + + /** + * paint for drawing the web + */ + protected Paint mWebPaint; + protected Paint mHighlightCirclePaint; + + public RadarChartRenderer(RadarChart chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + + mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mHighlightPaint.setStyle(Paint.Style.STROKE); + mHighlightPaint.setStrokeWidth(2f); + mHighlightPaint.setColor(Color.rgb(255, 187, 115)); + + mWebPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mWebPaint.setStyle(Paint.Style.STROKE); + + mHighlightCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + } + + public Paint getWebPaint() { + return mWebPaint; + } + + @Override + public void initBuffers() { + // TODO Auto-generated method stub + + } + + @Override + public void drawData(Canvas c) { + + RadarData radarData = mChart.getData(); + + int mostEntries = radarData.getMaxEntryCountSet().getEntryCount(); + + for (IRadarDataSet set : radarData.getDataSets()) { + + if (set.isVisible()) { + drawDataSet(c, set, mostEntries); + } + } + } + + protected Path mDrawDataSetSurfacePathBuffer = new Path(); + /** + * Draws the RadarDataSet + * + * @param c + * @param dataSet + * @param mostEntries the entry count of the dataset with the most entries + */ + protected void drawDataSet(Canvas c, IRadarDataSet dataSet, int mostEntries) { + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + Path surface = mDrawDataSetSurfacePathBuffer; + surface.reset(); + + boolean hasMovedToPoint = false; + + for (int j = 0; j < dataSet.getEntryCount(); j++) { + + mRenderPaint.setColor(dataSet.getColor(j)); + + RadarEntry e = dataSet.getEntryForIndex(j); + + Utils.getPosition( + center, + (e.getY() - mChart.getYChartMin()) * factor * phaseY, + sliceangle * j * phaseX + mChart.getRotationAngle(), pOut); + + if (Float.isNaN(pOut.x)) + continue; + + if (!hasMovedToPoint) { + surface.moveTo(pOut.x, pOut.y); + hasMovedToPoint = true; + } else + surface.lineTo(pOut.x, pOut.y); + } + + if (dataSet.getEntryCount() > mostEntries) { + // if this is not the largest set, draw a line to the center before closing + surface.lineTo(center.x, center.y); + } + + surface.close(); + + if (dataSet.isDrawFilledEnabled()) { + + final Drawable drawable = dataSet.getFillDrawable(); + if (drawable != null) { + + drawFilledPath(c, surface, drawable); + } else { + + drawFilledPath(c, surface, dataSet.getFillColor(), dataSet.getFillAlpha()); + } + } + + mRenderPaint.setStrokeWidth(dataSet.getLineWidth()); + mRenderPaint.setStyle(Paint.Style.STROKE); + + // draw the line (only if filled is disabled or alpha is below 255) + if (!dataSet.isDrawFilledEnabled() || dataSet.getFillAlpha() < 255) + c.drawPath(surface, mRenderPaint); + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + } + + @Override + public void drawValues(Canvas c) { + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + MPPointF pIcon = MPPointF.getInstance(0,0); + + float yoffset = Utils.convertDpToPixel(5f); + + for (int i = 0; i < mChart.getData().getDataSetCount(); i++) { + + IRadarDataSet dataSet = mChart.getData().getDataSetByIndex(i); + + if (!shouldDrawValues(dataSet)) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < dataSet.getEntryCount(); j++) { + + RadarEntry entry = dataSet.getEntryForIndex(j); + + Utils.getPosition( + center, + (entry.getY() - mChart.getYChartMin()) * factor * phaseY, + sliceangle * j * phaseX + mChart.getRotationAngle(), + pOut); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + dataSet.getValueFormatter(), + entry.getY(), + entry, + i, + pOut.x, + pOut.y - yoffset, + dataSet.getValueTextColor + (j)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.getPosition( + center, + (entry.getY()) * factor * phaseY + iconsOffset.y, + sliceangle * j * phaseX + mChart.getRotationAngle(), + pIcon); + + //noinspection SuspiciousNameCombination + pIcon.y += iconsOffset.x; + + Utils.drawImage( + c, + icon, + (int)pIcon.x, + (int)pIcon.y, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + MPPointF.recycleInstance(pIcon); + } + + @Override + public void drawExtras(Canvas c) { + drawWeb(c); + } + + protected void drawWeb(Canvas c) { + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + float rotationangle = mChart.getRotationAngle(); + + MPPointF center = mChart.getCenterOffsets(); + + // draw the web lines that come from the center + mWebPaint.setStrokeWidth(mChart.getWebLineWidth()); + mWebPaint.setColor(mChart.getWebColor()); + mWebPaint.setAlpha(mChart.getWebAlpha()); + + final int xIncrements = 1 + mChart.getSkipWebLineCount(); + int maxEntryCount = mChart.getData().getMaxEntryCountSet().getEntryCount(); + + MPPointF p = MPPointF.getInstance(0,0); + for (int i = 0; i < maxEntryCount; i += xIncrements) { + + Utils.getPosition( + center, + mChart.getYRange() * factor, + sliceangle * i + rotationangle, + p); + + c.drawLine(center.x, center.y, p.x, p.y, mWebPaint); + } + MPPointF.recycleInstance(p); + + // draw the inner-web + mWebPaint.setStrokeWidth(mChart.getWebLineWidthInner()); + mWebPaint.setColor(mChart.getWebColorInner()); + mWebPaint.setAlpha(mChart.getWebAlpha()); + + int labelCount = mChart.getYAxis().mEntryCount; + + MPPointF p1out = MPPointF.getInstance(0,0); + MPPointF p2out = MPPointF.getInstance(0,0); + for (int j = 0; j < labelCount; j++) { + + for (int i = 0; i < mChart.getData().getEntryCount(); i++) { + + float r = (mChart.getYAxis().mEntries[j] - mChart.getYChartMin()) * factor; + + Utils.getPosition(center, r, sliceangle * i + rotationangle, p1out); + Utils.getPosition(center, r, sliceangle * (i + 1) + rotationangle, p2out); + + c.drawLine(p1out.x, p1out.y, p2out.x, p2out.y, mWebPaint); + + + } + } + MPPointF.recycleInstance(p1out); + MPPointF.recycleInstance(p2out); + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + + RadarData radarData = mChart.getData(); + + for (Highlight high : indices) { + + IRadarDataSet set = radarData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + RadarEntry e = set.getEntryForIndex((int) high.getX()); + + if (!isInBoundsX(e, set)) + continue; + + float y = (e.getY() - mChart.getYChartMin()); + + Utils.getPosition(center, + y * factor * mAnimator.getPhaseY(), + sliceangle * high.getX() * mAnimator.getPhaseX() + mChart.getRotationAngle(), + pOut); + + high.setDraw(pOut.x, pOut.y); + + // draw the lines + drawHighlightLines(c, pOut.x, pOut.y, set); + + if (set.isDrawHighlightCircleEnabled()) { + + if (!Float.isNaN(pOut.x) && !Float.isNaN(pOut.y)) { + + int strokeColor = set.getHighlightCircleStrokeColor(); + if (strokeColor == ColorTemplate.COLOR_NONE) { + strokeColor = set.getColor(0); + } + + if (set.getHighlightCircleStrokeAlpha() < 255) { + strokeColor = ColorTemplate.colorWithAlpha(strokeColor, set.getHighlightCircleStrokeAlpha()); + } + + drawHighlightCircle(c, + pOut, + set.getHighlightCircleInnerRadius(), + set.getHighlightCircleOuterRadius(), + set.getHighlightCircleFillColor(), + strokeColor, + set.getHighlightCircleStrokeWidth()); + } + } + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + } + + protected Path mDrawHighlightCirclePathBuffer = new Path(); + public void drawHighlightCircle(Canvas c, + MPPointF point, + float innerRadius, + float outerRadius, + int fillColor, + int strokeColor, + float strokeWidth) { + c.save(); + + outerRadius = Utils.convertDpToPixel(outerRadius); + innerRadius = Utils.convertDpToPixel(innerRadius); + + if (fillColor != ColorTemplate.COLOR_NONE) { + Path p = mDrawHighlightCirclePathBuffer; + p.reset(); + p.addCircle(point.x, point.y, outerRadius, Path.Direction.CW); + if (innerRadius > 0.f) { + p.addCircle(point.x, point.y, innerRadius, Path.Direction.CCW); + } + mHighlightCirclePaint.setColor(fillColor); + mHighlightCirclePaint.setStyle(Paint.Style.FILL); + c.drawPath(p, mHighlightCirclePaint); + } + + if (strokeColor != ColorTemplate.COLOR_NONE) { + mHighlightCirclePaint.setColor(strokeColor); + mHighlightCirclePaint.setStyle(Paint.Style.STROKE); + mHighlightCirclePaint.setStrokeWidth(Utils.convertDpToPixel(strokeWidth)); + c.drawCircle(point.x, point.y, outerRadius, mHighlightCirclePaint); + } + + c.restore(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/Renderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/Renderer.java new file mode 100644 index 0000000..90f4968 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/Renderer.java @@ -0,0 +1,21 @@ + +package com.github.mikephil.charting.renderer; + +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Abstract baseclass of all Renderers. + * + * @author Philipp Jahoda + */ +public abstract class Renderer { + + /** + * the component that handles the drawing area of the chart and it's offsets + */ + protected ViewPortHandler mViewPortHandler; + + public Renderer(ViewPortHandler viewPortHandler) { + this.mViewPortHandler = viewPortHandler; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/ScatterChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/ScatterChartRenderer.java new file mode 100644 index 0000000..ccd077e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/ScatterChartRenderer.java @@ -0,0 +1,197 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.ScatterDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.renderer.scatter.IShapeRenderer; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class ScatterChartRenderer extends LineScatterCandleRadarRenderer { + + protected ScatterDataProvider mChart; + + public ScatterChartRenderer(ScatterDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + } + + @Override + public void initBuffers() { + } + + @Override + public void drawData(Canvas c) { + + ScatterData scatterData = mChart.getScatterData(); + + for (IScatterDataSet set : scatterData.getDataSets()) { + + if (set.isVisible()) + drawDataSet(c, set); + } + } + + float[] mPixelBuffer = new float[2]; + + protected void drawDataSet(Canvas c, IScatterDataSet dataSet) { + + if (dataSet.getEntryCount() < 1) + return; + + ViewPortHandler viewPortHandler = mViewPortHandler; + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + float phaseY = mAnimator.getPhaseY(); + + IShapeRenderer renderer = dataSet.getShapeRenderer(); + if (renderer == null) { + Log.i("MISSING", "There's no IShapeRenderer specified for ScatterDataSet"); + return; + } + + int max = (int)(Math.min( + Math.ceil((float)dataSet.getEntryCount() * mAnimator.getPhaseX()), + (float)dataSet.getEntryCount())); + + for (int i = 0; i < max; i++) { + + Entry e = dataSet.getEntryForIndex(i); + + mPixelBuffer[0] = e.getX(); + mPixelBuffer[1] = e.getY() * phaseY; + + trans.pointValuesToPixel(mPixelBuffer); + + if (!viewPortHandler.isInBoundsRight(mPixelBuffer[0])) + break; + + if (!viewPortHandler.isInBoundsLeft(mPixelBuffer[0]) + || !viewPortHandler.isInBoundsY(mPixelBuffer[1])) + continue; + + mRenderPaint.setColor(dataSet.getColor(i / 2)); + renderer.renderShape( + c, dataSet, mViewPortHandler, + mPixelBuffer[0], mPixelBuffer[1], + mRenderPaint); + } + } + + @Override + public void drawValues(Canvas c) { + + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getScatterData().getDataSets(); + + for (int i = 0; i < mChart.getScatterData().getDataSetCount(); i++) { + + IScatterDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + mXBounds.set(mChart, dataSet); + + float[] positions = mChart.getTransformer(dataSet.getAxisDependency()) + .generateTransformedValuesScatter(dataSet, + mAnimator.getPhaseX(), mAnimator.getPhaseY(), mXBounds.min, mXBounds.max); + + float shapeSize = Utils.convertDpToPixel(dataSet.getScatterShapeSize()); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < positions.length; j += 2) { + + if (!mViewPortHandler.isInBoundsRight(positions[j])) + break; + + // make sure the lines don't do shitty things outside bounds + if ((!mViewPortHandler.isInBoundsLeft(positions[j]) + || !mViewPortHandler.isInBoundsY(positions[j + 1]))) + continue; + + Entry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + dataSet.getValueFormatter(), + entry.getY(), + entry, + i, + positions[j], + positions[j + 1] - shapeSize, + dataSet.getValueTextColor(j / 2 + mXBounds.min)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(positions[j] + iconsOffset.x), + (int)(positions[j + 1] + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawExtras(Canvas c) { + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + ScatterData scatterData = mChart.getScatterData(); + + for (Highlight high : indices) { + + IScatterDataSet set = scatterData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + final Entry e = set.getEntryForXValue(high.getX(), high.getY()); + + if (!isInBoundsX(e, set)) + continue; + + MPPointD pix = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(e.getX(), e.getY() * mAnimator + .getPhaseY()); + + high.setDraw((float) pix.x, (float) pix.y); + + // draw the lines + drawHighlightLines(c, (float) pix.x, (float) pix.y, set); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRenderer.java new file mode 100644 index 0000000..b973137 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRenderer.java @@ -0,0 +1,401 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.XAxis.XAxisPosition; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class XAxisRenderer extends AxisRenderer { + + protected XAxis mXAxis; + + public XAxisRenderer(ViewPortHandler viewPortHandler, XAxis xAxis, Transformer trans) { + super(viewPortHandler, trans, xAxis); + + this.mXAxis = xAxis; + + mAxisLabelPaint.setColor(Color.BLACK); + mAxisLabelPaint.setTextAlign(Align.CENTER); + mAxisLabelPaint.setTextSize(Utils.convertDpToPixel(10f)); + } + + protected void setupGridPaint() { + mGridPaint.setColor(mXAxis.getGridColor()); + mGridPaint.setStrokeWidth(mXAxis.getGridLineWidth()); + mGridPaint.setPathEffect(mXAxis.getGridDashPathEffect()); + } + + @Override + public void computeAxis(float min, float max, boolean inverted) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (mViewPortHandler.contentWidth() > 10 && !mViewPortHandler.isFullyZoomedOutX()) { + + MPPointD p1 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop()); + MPPointD p2 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentRight(), mViewPortHandler.contentTop()); + + if (inverted) { + + min = (float) p2.x; + max = (float) p1.x; + } else { + + min = (float) p1.x; + max = (float) p2.x; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + computeAxisValues(min, max); + } + + @Override + protected void computeAxisValues(float min, float max) { + super.computeAxisValues(min, max); + + computeSize(); + } + + protected void computeSize() { + + String longest = mXAxis.getLongestLabel(); + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + + final FSize labelSize = Utils.calcTextSize(mAxisLabelPaint, longest); + + final float labelWidth = labelSize.width; + final float labelHeight = Utils.calcTextHeight(mAxisLabelPaint, "我"); + + final FSize labelRotatedSize = Utils.getSizeOfRotatedRectangleByDegrees( + labelWidth, + labelHeight, + mXAxis.getLabelRotationAngle()); + + + mXAxis.mLabelWidth = Math.round(labelWidth); + mXAxis.mLabelHeight = Math.round(labelHeight); + mXAxis.mLabelRotatedWidth = Math.round(labelRotatedSize.width); + mXAxis.mLabelRotatedHeight = Math.round(labelRotatedSize.height); + + FSize.recycleInstance(labelRotatedSize); + FSize.recycleInstance(labelSize); + } + + @Override + public void renderAxisLabels(Canvas c) { + + if (!mXAxis.isEnabled() || !mXAxis.isDrawLabelsEnabled()) + return; + + float yoffset = mXAxis.getYOffset(); + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + mAxisLabelPaint.setColor(mXAxis.getTextColor()); + + MPPointF pointF = MPPointF.getInstance(0,0); + if (mXAxis.getPosition() == XAxisPosition.TOP) { + pointF.x = 0.5f; + pointF.y = 1.0f; + drawLabels(c, mViewPortHandler.contentTop() - yoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.TOP_INSIDE) { + pointF.x = 0.5f; + pointF.y = 1.0f; + drawLabels(c, mViewPortHandler.contentTop() + yoffset + mXAxis.mLabelRotatedHeight, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { + pointF.x = 0.5f; + pointF.y = 0.0f; + drawLabels(c, mViewPortHandler.contentBottom() + yoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE) { + pointF.x = 0.5f; + pointF.y = 0.0f; + drawLabels(c, mViewPortHandler.contentBottom() - yoffset - mXAxis.mLabelRotatedHeight, pointF); + + } else { // BOTH SIDED + pointF.x = 0.5f; + pointF.y = 1.0f; + drawLabels(c, mViewPortHandler.contentTop() - yoffset, pointF); + pointF.x = 0.5f; + pointF.y = 0.0f; + drawLabels(c, mViewPortHandler.contentBottom() + yoffset, pointF); + } + MPPointF.recycleInstance(pointF); + } + + @Override + public void renderAxisLine(Canvas c) { + + if (!mXAxis.isDrawAxisLineEnabled() || !mXAxis.isEnabled()) + return; + + mAxisLinePaint.setColor(mXAxis.getAxisLineColor()); + mAxisLinePaint.setStrokeWidth(mXAxis.getAxisLineWidth()); + mAxisLinePaint.setPathEffect(mXAxis.getAxisLineDashPathEffect()); + + if (mXAxis.getPosition() == XAxisPosition.TOP + || mXAxis.getPosition() == XAxisPosition.TOP_INSIDE + || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentTop(), mAxisLinePaint); + } + + if (mXAxis.getPosition() == XAxisPosition.BOTTOM + || mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE + || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + } + + /** + * draws the x-labels on the specified y-position + * + * @param pos + */ + protected void drawLabels(Canvas c, float pos, MPPointF anchor) { + + final float labelRotationAngleDegrees = mXAxis.getLabelRotationAngle(); + boolean centeringEnabled = mXAxis.isCenterAxisLabelsEnabled(); + + float[] positions = new float[mXAxis.mEntryCount * 2]; + + for (int i = 0; i < positions.length; i += 2) { + + // only fill x values + if (centeringEnabled) { + positions[i] = mXAxis.mCenteredEntries[i / 2]; + } else { + positions[i] = mXAxis.mEntries[i / 2]; + } + } + + mTrans.pointValuesToPixel(positions); + + for (int i = 0; i < positions.length; i += 2) { + + float x = positions[i]; + + if (mViewPortHandler.isInBoundsX(x)) { + + String label = mXAxis.getValueFormatter().getFormattedValue(mXAxis.mEntries[i / 2], mXAxis); + + if (mXAxis.isAvoidFirstLastClippingEnabled()) { + + // avoid clipping of the last + if (i / 2 == mXAxis.mEntryCount - 1 && mXAxis.mEntryCount > 1) { + float width = Utils.calcTextWidth(mAxisLabelPaint, label); + + if (width > mViewPortHandler.offsetRight() * 2 + && x + width > mViewPortHandler.getChartWidth()) + x -= width / 2; + + // avoid clipping of the first + } else if (i == 0) { + + float width = Utils.calcTextWidth(mAxisLabelPaint, label); + x += width / 2; + } + } + + drawLabel(c, label, x, pos, anchor, labelRotationAngleDegrees); + } + } + } + + protected void drawLabel(Canvas c, String formattedLabel, float x, float y, MPPointF anchor, float angleDegrees) { + Utils.drawXAxisValue(c, formattedLabel, x, y, mAxisLabelPaint, anchor, angleDegrees); + } + protected Path mRenderGridLinesPath = new Path(); + protected float[] mRenderGridLinesBuffer = new float[2]; + @Override + public void renderGridLines(Canvas c) { + + if (!mXAxis.isDrawGridLinesEnabled() || !mXAxis.isEnabled()) + return; + + int clipRestoreCount = c.save(); + c.clipRect(getGridClippingRect()); + + if(mRenderGridLinesBuffer.length != mAxis.mEntryCount * 2){ + mRenderGridLinesBuffer = new float[mXAxis.mEntryCount * 2]; + } + float[] positions = mRenderGridLinesBuffer; + + for (int i = 0; i < positions.length; i += 2) { + positions[i] = mXAxis.mEntries[i / 2]; + positions[i + 1] = mXAxis.mEntries[i / 2]; + } + + mTrans.pointValuesToPixel(positions); + + setupGridPaint(); + + Path gridLinePath = mRenderGridLinesPath; + gridLinePath.reset(); + + for (int i = 0; i < positions.length; i += 2) { + + drawGridLine(c, positions[i], positions[i + 1], gridLinePath); + } + + c.restoreToCount(clipRestoreCount); + } + + protected RectF mGridClippingRect = new RectF(); + + public RectF getGridClippingRect() { + mGridClippingRect.set(mViewPortHandler.getContentRect()); + mGridClippingRect.inset(-mAxis.getGridLineWidth(), 0.f); + return mGridClippingRect; + } + + /** + * Draws the grid line at the specified position using the provided path. + * + * @param c + * @param x + * @param y + * @param gridLinePath + */ + protected void drawGridLine(Canvas c, float x, float y, Path gridLinePath) { + + gridLinePath.moveTo(x, mViewPortHandler.contentBottom()); + gridLinePath.lineTo(x, mViewPortHandler.contentTop()); + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(gridLinePath, mGridPaint); + + gridLinePath.reset(); + } + + protected float[] mRenderLimitLinesBuffer = new float[2]; + protected RectF mLimitLineClippingRect = new RectF(); + + /** + * Draws the LimitLines associated with this axis to the screen. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mXAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0) + return; + + float[] position = mRenderLimitLinesBuffer; + position[0] = 0; + position[1] = 0; + + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + int clipRestoreCount = c.save(); + mLimitLineClippingRect.set(mViewPortHandler.getContentRect()); + mLimitLineClippingRect.inset(-l.getLineWidth(), 0.f); + c.clipRect(mLimitLineClippingRect); + + position[0] = l.getLimit(); + position[1] = 0.f; + + mTrans.pointValuesToPixel(position); + + renderLimitLineLine(c, l, position); + renderLimitLineLabel(c, l, position, 2.f + l.getYOffset()); + + c.restoreToCount(clipRestoreCount); + } + } + + float[] mLimitLineSegmentsBuffer = new float[4]; + private Path mLimitLinePath = new Path(); + + public void renderLimitLineLine(Canvas c, LimitLine limitLine, float[] position) { + mLimitLineSegmentsBuffer[0] = position[0]; + mLimitLineSegmentsBuffer[1] = mViewPortHandler.contentTop(); + mLimitLineSegmentsBuffer[2] = position[0]; + mLimitLineSegmentsBuffer[3] = mViewPortHandler.contentBottom(); + + mLimitLinePath.reset(); + mLimitLinePath.moveTo(mLimitLineSegmentsBuffer[0], mLimitLineSegmentsBuffer[1]); + mLimitLinePath.lineTo(mLimitLineSegmentsBuffer[2], mLimitLineSegmentsBuffer[3]); + + mLimitLinePaint.setStyle(Paint.Style.STROKE); + mLimitLinePaint.setColor(limitLine.getLineColor()); + mLimitLinePaint.setStrokeWidth(limitLine.getLineWidth()); + mLimitLinePaint.setPathEffect(limitLine.getDashPathEffect()); + + c.drawPath(mLimitLinePath, mLimitLinePaint); + } + + public void renderLimitLineLabel(Canvas c, LimitLine limitLine, float[] position, float yOffset) { + String label = limitLine.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && !label.equals("")) { + + mLimitLinePaint.setStyle(limitLine.getTextStyle()); + mLimitLinePaint.setPathEffect(null); + mLimitLinePaint.setColor(limitLine.getTextColor()); + mLimitLinePaint.setStrokeWidth(0.5f); + mLimitLinePaint.setTextSize(limitLine.getTextSize()); + + + float xOffset = limitLine.getLineWidth() + limitLine.getXOffset(); + + final LimitLine.LimitLabelPosition labelPosition = limitLine.getLabelPosition(); + + if (labelPosition == LimitLine.LimitLabelPosition.RIGHT_TOP) { + + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, position[0] + xOffset, mViewPortHandler.contentTop() + yOffset + labelLineHeight, + mLimitLinePaint); + } else if (labelPosition == LimitLine.LimitLabelPosition.RIGHT_BOTTOM) { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, position[0] + xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } else if (labelPosition == LimitLine.LimitLabelPosition.LEFT_TOP) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + c.drawText(label, position[0] - xOffset, mViewPortHandler.contentTop() + yOffset + labelLineHeight, + mLimitLinePaint); + } else { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, position[0] - xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererHorizontalBarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererHorizontalBarChart.java new file mode 100644 index 0000000..86047cf --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererHorizontalBarChart.java @@ -0,0 +1,310 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.charts.BarChart; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.XAxis.XAxisPosition; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class XAxisRendererHorizontalBarChart extends XAxisRenderer { + + protected BarChart mChart; + + public XAxisRendererHorizontalBarChart(ViewPortHandler viewPortHandler, XAxis xAxis, + Transformer trans, BarChart chart) { + super(viewPortHandler, xAxis, trans); + + this.mChart = chart; + } + + @Override + public void computeAxis(float min, float max, boolean inverted) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (mViewPortHandler.contentWidth() > 10 && !mViewPortHandler.isFullyZoomedOutY()) { + + MPPointD p1 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentBottom()); + MPPointD p2 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop()); + + if (inverted) { + + min = (float) p2.y; + max = (float) p1.y; + } else { + + min = (float) p1.y; + max = (float) p2.y; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + computeAxisValues(min, max); + } + + @Override + protected void computeSize() { + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + + String longest = mXAxis.getLongestLabel(); + + final FSize labelSize = Utils.calcTextSize(mAxisLabelPaint, longest); + + final float labelWidth = (int)(labelSize.width + mXAxis.getXOffset() * 3.5f); + final float labelHeight = labelSize.height; + + final FSize labelRotatedSize = Utils.getSizeOfRotatedRectangleByDegrees( + labelSize.width, + labelHeight, + mXAxis.getLabelRotationAngle()); + + mXAxis.mLabelWidth = Math.round(labelWidth); + mXAxis.mLabelHeight = Math.round(labelHeight); + mXAxis.mLabelRotatedWidth = (int)(labelRotatedSize.width + mXAxis.getXOffset() * 3.5f); + mXAxis.mLabelRotatedHeight = Math.round(labelRotatedSize.height); + + FSize.recycleInstance(labelRotatedSize); + } + + @Override + public void renderAxisLabels(Canvas c) { + + if (!mXAxis.isEnabled() || !mXAxis.isDrawLabelsEnabled()) + return; + + float xoffset = mXAxis.getXOffset(); + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + mAxisLabelPaint.setColor(mXAxis.getTextColor()); + + MPPointF pointF = MPPointF.getInstance(0,0); + + if (mXAxis.getPosition() == XAxisPosition.TOP) { + pointF.x = 0.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentRight() + xoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.TOP_INSIDE) { + pointF.x = 1.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentRight() - xoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { + pointF.x = 1.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentLeft() - xoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE) { + pointF.x = 1.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentLeft() + xoffset, pointF); + + } else { // BOTH SIDED + pointF.x = 0.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentRight() + xoffset, pointF); + pointF.x = 1.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentLeft() - xoffset, pointF); + } + + MPPointF.recycleInstance(pointF); + } + + @Override + protected void drawLabels(Canvas c, float pos, MPPointF anchor) { + + final float labelRotationAngleDegrees = mXAxis.getLabelRotationAngle(); + boolean centeringEnabled = mXAxis.isCenterAxisLabelsEnabled(); + + float[] positions = new float[mXAxis.mEntryCount * 2]; + + for (int i = 0; i < positions.length; i += 2) { + + // only fill x values + if (centeringEnabled) { + positions[i + 1] = mXAxis.mCenteredEntries[i / 2]; + } else { + positions[i + 1] = mXAxis.mEntries[i / 2]; + } + } + + mTrans.pointValuesToPixel(positions); + + for (int i = 0; i < positions.length; i += 2) { + + float y = positions[i + 1]; + + if (mViewPortHandler.isInBoundsY(y)) { + + String label = mXAxis.getValueFormatter().getFormattedValue(mXAxis.mEntries[i / 2], mXAxis); + drawLabel(c, label, pos, y, anchor, labelRotationAngleDegrees); + } + } + } + + @Override + public RectF getGridClippingRect() { + mGridClippingRect.set(mViewPortHandler.getContentRect()); + mGridClippingRect.inset(0.f, -mAxis.getGridLineWidth()); + return mGridClippingRect; + } + + @Override + protected void drawGridLine(Canvas c, float x, float y, Path gridLinePath) { + + gridLinePath.moveTo(mViewPortHandler.contentRight(), y); + gridLinePath.lineTo(mViewPortHandler.contentLeft(), y); + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(gridLinePath, mGridPaint); + + gridLinePath.reset(); + } + + @Override + public void renderAxisLine(Canvas c) { + + if (!mXAxis.isDrawAxisLineEnabled() || !mXAxis.isEnabled()) + return; + + mAxisLinePaint.setColor(mXAxis.getAxisLineColor()); + mAxisLinePaint.setStrokeWidth(mXAxis.getAxisLineWidth()); + + if (mXAxis.getPosition() == XAxisPosition.TOP + || mXAxis.getPosition() == XAxisPosition.TOP_INSIDE + || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + c.drawLine(mViewPortHandler.contentRight(), + mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + + if (mXAxis.getPosition() == XAxisPosition.BOTTOM + || mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE + || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + } + + protected Path mRenderLimitLinesPathBuffer = new Path(); + /** + * Draws the LimitLines associated with this axis to the screen. + * This is the standard YAxis renderer using the XAxis limit lines. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mXAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0) + return; + + float[] pts = mRenderLimitLinesBuffer; + pts[0] = 0; + pts[1] = 0; + + Path limitLinePath = mRenderLimitLinesPathBuffer; + limitLinePath.reset(); + + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if(!l.isEnabled()) + continue; + + int clipRestoreCount = c.save(); + mLimitLineClippingRect.set(mViewPortHandler.getContentRect()); + mLimitLineClippingRect.inset(0.f, -l.getLineWidth()); + c.clipRect(mLimitLineClippingRect); + + mLimitLinePaint.setStyle(Paint.Style.STROKE); + mLimitLinePaint.setColor(l.getLineColor()); + mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + mLimitLinePaint.setPathEffect(l.getDashPathEffect()); + + pts[1] = l.getLimit(); + + mTrans.pointValuesToPixel(pts); + + limitLinePath.moveTo(mViewPortHandler.contentLeft(), pts[1]); + limitLinePath.lineTo(mViewPortHandler.contentRight(), pts[1]); + + c.drawPath(limitLinePath, mLimitLinePaint); + limitLinePath.reset(); + // c.drawLines(pts, mLimitLinePaint); + + String label = l.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && !label.equals("")) { + + mLimitLinePaint.setStyle(l.getTextStyle()); + mLimitLinePaint.setPathEffect(null); + mLimitLinePaint.setColor(l.getTextColor()); + mLimitLinePaint.setStrokeWidth(0.5f); + mLimitLinePaint.setTextSize(l.getTextSize()); + + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + float xOffset = Utils.convertDpToPixel(4f) + l.getXOffset(); + float yOffset = l.getLineWidth() + labelLineHeight + l.getYOffset(); + + final LimitLine.LimitLabelPosition position = l.getLabelPosition(); + + if (position == LimitLine.LimitLabelPosition.RIGHT_TOP) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, + mViewPortHandler.contentRight() - xOffset, + pts[1] - yOffset + labelLineHeight, mLimitLinePaint); + + } else if (position == LimitLine.LimitLabelPosition.RIGHT_BOTTOM) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, + mViewPortHandler.contentRight() - xOffset, + pts[1] + yOffset, mLimitLinePaint); + + } else if (position == LimitLine.LimitLabelPosition.LEFT_TOP) { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, + mViewPortHandler.contentLeft() + xOffset, + pts[1] - yOffset + labelLineHeight, mLimitLinePaint); + + } else { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, + mViewPortHandler.offsetLeft() + xOffset, + pts[1] + yOffset, mLimitLinePaint); + } + } + + c.restoreToCount(clipRestoreCount); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererRadarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererRadarChart.java new file mode 100644 index 0000000..956e8c7 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererRadarChart.java @@ -0,0 +1,71 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.PointF; + +import com.github.mikephil.charting.charts.RadarChart; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +public class XAxisRendererRadarChart extends XAxisRenderer { + + private RadarChart mChart; + + public XAxisRendererRadarChart(ViewPortHandler viewPortHandler, XAxis xAxis, RadarChart chart) { + super(viewPortHandler, xAxis, null); + + mChart = chart; + } + + @Override + public void renderAxisLabels(Canvas c) { + + if (!mXAxis.isEnabled() || !mXAxis.isDrawLabelsEnabled()) + return; + + final float labelRotationAngleDegrees = mXAxis.getLabelRotationAngle(); + final MPPointF drawLabelAnchor = MPPointF.getInstance(0.5f, 0.25f); + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + mAxisLabelPaint.setColor(mXAxis.getTextColor()); + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + for (int i = 0; i < mChart.getData().getMaxEntryCountSet().getEntryCount(); i++) { + + String label = mXAxis.getValueFormatter().getFormattedValue(i, mXAxis); + + float angle = (sliceangle * i + mChart.getRotationAngle()) % 360f; + + Utils.getPosition(center, mChart.getYRange() * factor + + mXAxis.mLabelRotatedWidth / 2f, angle, pOut); + + drawLabel(c, label, pOut.x, pOut.y - mXAxis.mLabelRotatedHeight / 2.f, + drawLabelAnchor, labelRotationAngleDegrees); + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + MPPointF.recycleInstance(drawLabelAnchor); + } + + /** + * XAxis LimitLines on RadarChart not yet supported. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + // this space intentionally left blank + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRenderer.java new file mode 100644 index 0000000..53cca7e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRenderer.java @@ -0,0 +1,352 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.components.YAxis.YAxisLabelPosition; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class YAxisRenderer extends AxisRenderer { + + protected YAxis mYAxis; + + protected Paint mZeroLinePaint; + + public YAxisRenderer(ViewPortHandler viewPortHandler, YAxis yAxis, Transformer trans) { + super(viewPortHandler, trans, yAxis); + + this.mYAxis = yAxis; + + if(mViewPortHandler != null) { + + mAxisLabelPaint.setColor(Color.BLACK); + mAxisLabelPaint.setTextSize(Utils.convertDpToPixel(10f)); + + mZeroLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mZeroLinePaint.setColor(Color.GRAY); + mZeroLinePaint.setStrokeWidth(1f); + mZeroLinePaint.setStyle(Paint.Style.STROKE); + } + } + + /** + * draws the y-axis labels to the screen + */ + @Override + public void renderAxisLabels(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawLabelsEnabled()) + return; + + float[] positions = getTransformedPositions(); + + mAxisLabelPaint.setTypeface(mYAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mYAxis.getTextSize()); + mAxisLabelPaint.setColor(mYAxis.getTextColor()); + + float xoffset = mYAxis.getXOffset(); + float yoffset = Utils.calcTextHeight(mAxisLabelPaint, "A") / 2.5f + mYAxis.getYOffset(); + + AxisDependency dependency = mYAxis.getAxisDependency(); + YAxisLabelPosition labelPosition = mYAxis.getLabelPosition(); + + float xPos = 0f; + + if (dependency == AxisDependency.LEFT) { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + mAxisLabelPaint.setTextAlign(Align.RIGHT); + xPos = mViewPortHandler.offsetLeft() - xoffset; + } else { + mAxisLabelPaint.setTextAlign(Align.LEFT); + xPos = mViewPortHandler.offsetLeft() + xoffset; + } + + } else { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + mAxisLabelPaint.setTextAlign(Align.LEFT); + xPos = mViewPortHandler.contentRight() + xoffset; + } else { + mAxisLabelPaint.setTextAlign(Align.RIGHT); + xPos = mViewPortHandler.contentRight() - xoffset; + } + } + + drawYLabels(c, xPos, positions, yoffset); + } + + @Override + public void renderAxisLine(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawAxisLineEnabled()) + return; + + mAxisLinePaint.setColor(mYAxis.getAxisLineColor()); + mAxisLinePaint.setStrokeWidth(mYAxis.getAxisLineWidth()); + + if (mYAxis.getAxisDependency() == AxisDependency.LEFT) { + c.drawLine(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } else { + c.drawLine(mViewPortHandler.contentRight(), mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + } + + /** + * draws the y-labels on the specified x-position + * + * @param fixedPosition + * @param positions + */ + protected void drawYLabels(Canvas c, float fixedPosition, float[] positions, float offset) { + + final int from = mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + final int to = mYAxis.isDrawTopYLabelEntryEnabled() + ? mYAxis.mEntryCount + : (mYAxis.mEntryCount - 1); + + float xOffset = mYAxis.getLabelXOffset(); + + // draw + for (int i = from; i < to; i++) { + + String text = mYAxis.getFormattedLabel(i); + + c.drawText(text, + fixedPosition + xOffset, + positions[i * 2 + 1] + offset, + mAxisLabelPaint); + } + } + + protected Path mRenderGridLinesPath = new Path(); + @Override + public void renderGridLines(Canvas c) { + + if (!mYAxis.isEnabled()) + return; + + if (mYAxis.isDrawGridLinesEnabled()) { + + int clipRestoreCount = c.save(); + c.clipRect(getGridClippingRect()); + + float[] positions = getTransformedPositions(); + + mGridPaint.setColor(mYAxis.getGridColor()); + mGridPaint.setStrokeWidth(mYAxis.getGridLineWidth()); + mGridPaint.setPathEffect(mYAxis.getGridDashPathEffect()); + + Path gridLinePath = mRenderGridLinesPath; + gridLinePath.reset(); + + // draw the grid + for (int i = 0; i < positions.length; i += 2) { + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(linePath(gridLinePath, i, positions), mGridPaint); + gridLinePath.reset(); + } + + c.restoreToCount(clipRestoreCount); + } + + if (mYAxis.isDrawZeroLineEnabled()) { + drawZeroLine(c); + } + } + + protected RectF mGridClippingRect = new RectF(); + + public RectF getGridClippingRect() { + mGridClippingRect.set(mViewPortHandler.getContentRect()); + mGridClippingRect.inset(0.f, -mAxis.getGridLineWidth()); + return mGridClippingRect; + } + + /** + * Calculates the path for a grid line. + * + * @param p + * @param i + * @param positions + * @return + */ + protected Path linePath(Path p, int i, float[] positions) { + + p.moveTo(mViewPortHandler.offsetLeft(), positions[i + 1]); + p.lineTo(mViewPortHandler.contentRight(), positions[i + 1]); + + return p; + } + + protected float[] mGetTransformedPositionsBuffer = new float[2]; + /** + * Transforms the values contained in the axis entries to screen pixels and returns them in form of a float array + * of x- and y-coordinates. + * + * @return + */ + protected float[] getTransformedPositions() { + + if(mGetTransformedPositionsBuffer.length != mYAxis.mEntryCount * 2){ + mGetTransformedPositionsBuffer = new float[mYAxis.mEntryCount * 2]; + } + float[] positions = mGetTransformedPositionsBuffer; + + for (int i = 0; i < positions.length; i += 2) { + // only fill y values, x values are not needed for y-labels + positions[i + 1] = mYAxis.mEntries[i / 2]; + } + + mTrans.pointValuesToPixel(positions); + return positions; + } + + protected Path mDrawZeroLinePath = new Path(); + protected RectF mZeroLineClippingRect = new RectF(); + + /** + * Draws the zero line. + */ + protected void drawZeroLine(Canvas c) { + + int clipRestoreCount = c.save(); + mZeroLineClippingRect.set(mViewPortHandler.getContentRect()); + mZeroLineClippingRect.inset(0.f, -mYAxis.getZeroLineWidth()); + c.clipRect(mZeroLineClippingRect); + + // draw zero line + MPPointD pos = mTrans.getPixelForValues(0f, 0f); + + mZeroLinePaint.setColor(mYAxis.getZeroLineColor()); + mZeroLinePaint.setStrokeWidth(mYAxis.getZeroLineWidth()); + + Path zeroLinePath = mDrawZeroLinePath; + zeroLinePath.reset(); + + zeroLinePath.moveTo(mViewPortHandler.contentLeft(), (float) pos.y); + zeroLinePath.lineTo(mViewPortHandler.contentRight(), (float) pos.y); + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(zeroLinePath, mZeroLinePaint); + + c.restoreToCount(clipRestoreCount); + } + + protected Path mRenderLimitLines = new Path(); + protected float[] mRenderLimitLinesBuffer = new float[2]; + protected RectF mLimitLineClippingRect = new RectF(); + /** + * Draws the LimitLines associated with this axis to the screen. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mYAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0) + return; + + float[] pts = mRenderLimitLinesBuffer; + pts[0] = 0; + pts[1] = 0; + Path limitLinePath = mRenderLimitLines; + limitLinePath.reset(); + + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + int clipRestoreCount = c.save(); + mLimitLineClippingRect.set(mViewPortHandler.getContentRect()); + mLimitLineClippingRect.inset(0.f, -l.getLineWidth()); + c.clipRect(mLimitLineClippingRect); + + mLimitLinePaint.setStyle(Paint.Style.STROKE); + mLimitLinePaint.setColor(l.getLineColor()); + mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + mLimitLinePaint.setPathEffect(l.getDashPathEffect()); + + pts[1] = l.getLimit(); + + mTrans.pointValuesToPixel(pts); + + limitLinePath.moveTo(mViewPortHandler.contentLeft(), pts[1]); + limitLinePath.lineTo(mViewPortHandler.contentRight(), pts[1]); + + c.drawPath(limitLinePath, mLimitLinePaint); + limitLinePath.reset(); + // c.drawLines(pts, mLimitLinePaint); + + String label = l.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && !label.equals("")) { + + mLimitLinePaint.setStyle(l.getTextStyle()); + mLimitLinePaint.setPathEffect(null); + mLimitLinePaint.setColor(l.getTextColor()); + mLimitLinePaint.setTypeface(l.getTypeface()); + mLimitLinePaint.setStrokeWidth(0.5f); + mLimitLinePaint.setTextSize(l.getTextSize()); + + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + float xOffset = Utils.convertDpToPixel(4f) + l.getXOffset(); + float yOffset = l.getLineWidth() + labelLineHeight + l.getYOffset(); + + final LimitLine.LimitLabelPosition position = l.getLabelPosition(); + + if (position == LimitLine.LimitLabelPosition.RIGHT_TOP) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, + mViewPortHandler.contentRight() - xOffset, + pts[1] - yOffset + labelLineHeight, mLimitLinePaint); + + } else if (position == LimitLine.LimitLabelPosition.RIGHT_BOTTOM) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, + mViewPortHandler.contentRight() - xOffset, + pts[1] + yOffset, mLimitLinePaint); + + } else if (position == LimitLine.LimitLabelPosition.LEFT_TOP) { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, + mViewPortHandler.contentLeft() + xOffset, + pts[1] - yOffset + labelLineHeight, mLimitLinePaint); + + } else { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, + mViewPortHandler.offsetLeft() + xOffset, + pts[1] + yOffset, mLimitLinePaint); + } + } + + c.restoreToCount(clipRestoreCount); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererHorizontalBarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererHorizontalBarChart.java new file mode 100644 index 0000000..fedf805 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererHorizontalBarChart.java @@ -0,0 +1,315 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.components.YAxis.YAxisLabelPosition; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class YAxisRendererHorizontalBarChart extends YAxisRenderer { + + public YAxisRendererHorizontalBarChart(ViewPortHandler viewPortHandler, YAxis yAxis, + Transformer trans) { + super(viewPortHandler, yAxis, trans); + + mLimitLinePaint.setTextAlign(Align.LEFT); + } + + /** + * Computes the axis values. + * + * @param yMin - the minimum y-value in the data object for this axis + * @param yMax - the maximum y-value in the data object for this axis + */ + @Override + public void computeAxis(float yMin, float yMax, boolean inverted) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (mViewPortHandler.contentHeight() > 10 && !mViewPortHandler.isFullyZoomedOutX()) { + + MPPointD p1 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop()); + MPPointD p2 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentRight(), + mViewPortHandler.contentTop()); + + if (!inverted) { + yMin = (float) p1.x; + yMax = (float) p2.x; + } else { + yMin = (float) p2.x; + yMax = (float) p1.x; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + computeAxisValues(yMin, yMax); + } + + /** + * draws the y-axis labels to the screen + */ + @Override + public void renderAxisLabels(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawLabelsEnabled()) + return; + + float[] positions = getTransformedPositions(); + + mAxisLabelPaint.setTypeface(mYAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mYAxis.getTextSize()); + mAxisLabelPaint.setColor(mYAxis.getTextColor()); + mAxisLabelPaint.setTextAlign(Align.CENTER); + + float baseYOffset = Utils.convertDpToPixel(2.5f); + float textHeight = Utils.calcTextHeight(mAxisLabelPaint, "Q"); + + AxisDependency dependency = mYAxis.getAxisDependency(); + YAxisLabelPosition labelPosition = mYAxis.getLabelPosition(); + + float yPos = 0f; + + if (dependency == AxisDependency.LEFT) { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + yPos = mViewPortHandler.contentTop() - baseYOffset; + } else { + yPos = mViewPortHandler.contentTop() - baseYOffset; + } + + } else { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + yPos = mViewPortHandler.contentBottom() + textHeight + baseYOffset; + } else { + yPos = mViewPortHandler.contentBottom() + textHeight + baseYOffset; + } + } + + drawYLabels(c, yPos, positions, mYAxis.getYOffset()); + } + + @Override + public void renderAxisLine(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawAxisLineEnabled()) + return; + + mAxisLinePaint.setColor(mYAxis.getAxisLineColor()); + mAxisLinePaint.setStrokeWidth(mYAxis.getAxisLineWidth()); + + if (mYAxis.getAxisDependency() == AxisDependency.LEFT) { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentTop(), mAxisLinePaint); + } else { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + } + + /** + * draws the y-labels on the specified x-position + * + * @param fixedPosition + * @param positions + */ + @Override + protected void drawYLabels(Canvas c, float fixedPosition, float[] positions, float offset) { + + mAxisLabelPaint.setTypeface(mYAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mYAxis.getTextSize()); + mAxisLabelPaint.setColor(mYAxis.getTextColor()); + + final int from = mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + final int to = mYAxis.isDrawTopYLabelEntryEnabled() + ? mYAxis.mEntryCount + : (mYAxis.mEntryCount - 1); + + float xOffset = mYAxis.getLabelXOffset(); + + for (int i = from; i < to; i++) { + + String text = mYAxis.getFormattedLabel(i); + + c.drawText(text, + positions[i * 2], + fixedPosition - offset + xOffset, + mAxisLabelPaint); + } + } + + @Override + protected float[] getTransformedPositions() { + + if(mGetTransformedPositionsBuffer.length != mYAxis.mEntryCount * 2) { + mGetTransformedPositionsBuffer = new float[mYAxis.mEntryCount * 2]; + } + float[] positions = mGetTransformedPositionsBuffer; + + for (int i = 0; i < positions.length; i += 2) { + // only fill x values, y values are not needed for x-labels + positions[i] = mYAxis.mEntries[i / 2]; + } + + mTrans.pointValuesToPixel(positions); + return positions; + } + + @Override + public RectF getGridClippingRect() { + mGridClippingRect.set(mViewPortHandler.getContentRect()); + mGridClippingRect.inset(-mAxis.getGridLineWidth(), 0.f); + return mGridClippingRect; + } + + @Override + protected Path linePath(Path p, int i, float[] positions) { + + p.moveTo(positions[i], mViewPortHandler.contentTop()); + p.lineTo(positions[i], mViewPortHandler.contentBottom()); + + return p; + } + + protected Path mDrawZeroLinePathBuffer = new Path(); + + @Override + protected void drawZeroLine(Canvas c) { + + int clipRestoreCount = c.save(); + mZeroLineClippingRect.set(mViewPortHandler.getContentRect()); + mZeroLineClippingRect.inset(-mYAxis.getZeroLineWidth(), 0.f); + c.clipRect(mLimitLineClippingRect); + + // draw zero line + MPPointD pos = mTrans.getPixelForValues(0f, 0f); + + mZeroLinePaint.setColor(mYAxis.getZeroLineColor()); + mZeroLinePaint.setStrokeWidth(mYAxis.getZeroLineWidth()); + + Path zeroLinePath = mDrawZeroLinePathBuffer; + zeroLinePath.reset(); + + zeroLinePath.moveTo((float) pos.x - 1, mViewPortHandler.contentTop()); + zeroLinePath.lineTo((float) pos.x - 1, mViewPortHandler.contentBottom()); + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(zeroLinePath, mZeroLinePaint); + + c.restoreToCount(clipRestoreCount); + } + + protected Path mRenderLimitLinesPathBuffer = new Path(); + protected float[] mRenderLimitLinesBuffer = new float[4]; + /** + * Draws the LimitLines associated with this axis to the screen. + * This is the standard XAxis renderer using the YAxis limit lines. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mYAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0) + return; + + float[] pts = mRenderLimitLinesBuffer; + pts[0] = 0; + pts[1] = 0; + pts[2] = 0; + pts[3] = 0; + Path limitLinePath = mRenderLimitLinesPathBuffer; + limitLinePath.reset(); + + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + int clipRestoreCount = c.save(); + mLimitLineClippingRect.set(mViewPortHandler.getContentRect()); + mLimitLineClippingRect.inset(-l.getLineWidth(), 0.f); + c.clipRect(mLimitLineClippingRect); + + pts[0] = l.getLimit(); + pts[2] = l.getLimit(); + + mTrans.pointValuesToPixel(pts); + + pts[1] = mViewPortHandler.contentTop(); + pts[3] = mViewPortHandler.contentBottom(); + + limitLinePath.moveTo(pts[0], pts[1]); + limitLinePath.lineTo(pts[2], pts[3]); + + mLimitLinePaint.setStyle(Paint.Style.STROKE); + mLimitLinePaint.setColor(l.getLineColor()); + mLimitLinePaint.setPathEffect(l.getDashPathEffect()); + mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + + c.drawPath(limitLinePath, mLimitLinePaint); + limitLinePath.reset(); + + String label = l.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && !label.equals("")) { + + mLimitLinePaint.setStyle(l.getTextStyle()); + mLimitLinePaint.setPathEffect(null); + mLimitLinePaint.setColor(l.getTextColor()); + mLimitLinePaint.setTypeface(l.getTypeface()); + mLimitLinePaint.setStrokeWidth(0.5f); + mLimitLinePaint.setTextSize(l.getTextSize()); + + float xOffset = l.getLineWidth() + l.getXOffset(); + float yOffset = Utils.convertDpToPixel(2f) + l.getYOffset(); + + final LimitLine.LimitLabelPosition position = l.getLabelPosition(); + + if (position == LimitLine.LimitLabelPosition.RIGHT_TOP) { + + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, pts[0] + xOffset, mViewPortHandler.contentTop() + yOffset + labelLineHeight, mLimitLinePaint); + } else if (position == LimitLine.LimitLabelPosition.RIGHT_BOTTOM) { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, pts[0] + xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } else if (position == LimitLine.LimitLabelPosition.LEFT_TOP) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + c.drawText(label, pts[0] - xOffset, mViewPortHandler.contentTop() + yOffset + labelLineHeight, mLimitLinePaint); + } else { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, pts[0] - xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } + } + + c.restoreToCount(clipRestoreCount); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererRadarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererRadarChart.java new file mode 100644 index 0000000..f7b1ad9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererRadarChart.java @@ -0,0 +1,232 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.PointF; + +import com.github.mikephil.charting.charts.RadarChart; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class YAxisRendererRadarChart extends YAxisRenderer { + + private RadarChart mChart; + + public YAxisRendererRadarChart(ViewPortHandler viewPortHandler, YAxis yAxis, RadarChart chart) { + super(viewPortHandler, yAxis, null); + + this.mChart = chart; + } + + @Override + protected void computeAxisValues(float min, float max) { + + float yMin = min; + float yMax = max; + + int labelCount = mAxis.getLabelCount(); + double range = Math.abs(yMax - yMin); + + if (labelCount == 0 || range <= 0 || Double.isInfinite(range)) { + mAxis.mEntries = new float[]{}; + mAxis.mCenteredEntries = new float[]{}; + mAxis.mEntryCount = 0; + return; + } + + // Find out how much spacing (in y value space) between axis values + double rawInterval = range / labelCount; + double interval = Utils.roundToNextSignificant(rawInterval); + + // If granularity is enabled, then do not allow the interval to go below specified granularity. + // This is used to avoid repeated values when rounding values for display. + if (mAxis.isGranularityEnabled()) + interval = interval < mAxis.getGranularity() ? mAxis.getGranularity() : interval; + + // Normalize interval + double intervalMagnitude = Utils.roundToNextSignificant(Math.pow(10, (int) Math.log10(interval))); + int intervalSigDigit = (int) (interval / intervalMagnitude); + if (intervalSigDigit > 5) { + // Use one order of magnitude higher, to avoid intervals like 0.9 or 90 + // if it's 0.0 after floor(), we use the old value + interval = Math.floor(10.0 * intervalMagnitude) == 0.0 + ? interval + : Math.floor(10.0 * intervalMagnitude); + } + + boolean centeringEnabled = mAxis.isCenterAxisLabelsEnabled(); + int n = centeringEnabled ? 1 : 0; + + // force label count + if (mAxis.isForceLabelsEnabled()) { + + float step = (float) range / (float) (labelCount - 1); + mAxis.mEntryCount = labelCount; + + if (mAxis.mEntries.length < labelCount) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = new float[labelCount]; + } + + float v = min; + + for (int i = 0; i < labelCount; i++) { + mAxis.mEntries[i] = v; + v += step; + } + + n = labelCount; + + // no forced count + } else { + + double first = interval == 0.0 ? 0.0 : Math.ceil(yMin / interval) * interval; + if (centeringEnabled) { + first -= interval; + } + + double last = interval == 0.0 ? 0.0 : Utils.nextUp(Math.floor(yMax / interval) * interval); + + double f; + int i; + + if (interval != 0.0) { + for (f = first; f <= last; f += interval) { + ++n; + } + } + + n++; + + mAxis.mEntryCount = n; + + if (mAxis.mEntries.length < n) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = new float[n]; + } + + for (f = first, i = 0; i < n; f += interval, ++i) { + + if (f == 0.0) // Fix for negative zero case (Where value == -0.0, and 0.0 == -0.0) + f = 0.0; + + mAxis.mEntries[i] = (float) f; + } + } + + // set decimals + if (interval < 1) { + mAxis.mDecimals = (int) Math.ceil(-Math.log10(interval)); + } else { + mAxis.mDecimals = 0; + } + + if (centeringEnabled) { + + if (mAxis.mCenteredEntries.length < n) { + mAxis.mCenteredEntries = new float[n]; + } + + float offset = (mAxis.mEntries[1] - mAxis.mEntries[0]) / 2f; + + for (int i = 0; i < n; i++) { + mAxis.mCenteredEntries[i] = mAxis.mEntries[i] + offset; + } + } + + mAxis.mAxisMinimum = mAxis.mEntries[0]; + mAxis.mAxisMaximum = mAxis.mEntries[n-1]; + mAxis.mAxisRange = Math.abs(mAxis.mAxisMaximum - mAxis.mAxisMinimum); + } + + @Override + public void renderAxisLabels(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawLabelsEnabled()) + return; + + mAxisLabelPaint.setTypeface(mYAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mYAxis.getTextSize()); + mAxisLabelPaint.setColor(mYAxis.getTextColor()); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + float factor = mChart.getFactor(); + + final int from = mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + final int to = mYAxis.isDrawTopYLabelEntryEnabled() + ? mYAxis.mEntryCount + : (mYAxis.mEntryCount - 1); + + float xOffset = mYAxis.getLabelXOffset(); + + for (int j = from; j < to; j++) { + + float r = (mYAxis.mEntries[j] - mYAxis.mAxisMinimum) * factor; + + Utils.getPosition(center, r, mChart.getRotationAngle(), pOut); + + String label = mYAxis.getFormattedLabel(j); + + c.drawText(label, pOut.x + xOffset, pOut.y, mAxisLabelPaint); + } + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + } + + private Path mRenderLimitLinesPathBuffer = new Path(); + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mYAxis.getLimitLines(); + + if (limitLines == null) + return; + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + mLimitLinePaint.setColor(l.getLineColor()); + mLimitLinePaint.setPathEffect(l.getDashPathEffect()); + mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + + float r = (l.getLimit() - mChart.getYChartMin()) * factor; + + Path limitPath = mRenderLimitLinesPathBuffer; + limitPath.reset(); + + + for (int j = 0; j < mChart.getData().getMaxEntryCountSet().getEntryCount(); j++) { + + Utils.getPosition(center, r, sliceangle * j + mChart.getRotationAngle(), pOut); + + if (j == 0) + limitPath.moveTo(pOut.x, pOut.y); + else + limitPath.lineTo(pOut.x, pOut.y); + } + limitPath.close(); + + c.drawPath(limitPath, mLimitLinePaint); + } + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronDownShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronDownShapeRenderer.java new file mode 100644 index 0000000..9328b27 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronDownShapeRenderer.java @@ -0,0 +1,41 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class ChevronDownShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeHalf = dataSet.getScatterShapeSize() / 2f; + + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + + c.drawLine( + posX, + posY + (2 * shapeHalf), + posX + (2 * shapeHalf), + posY, + renderPaint); + + c.drawLine( + posX, + posY + (2 * shapeHalf), + posX - (2 * shapeHalf), + posY, + renderPaint); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronUpShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronUpShapeRenderer.java new file mode 100644 index 0000000..6dea0ab --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronUpShapeRenderer.java @@ -0,0 +1,42 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class ChevronUpShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeHalf = dataSet.getScatterShapeSize() / 2f; + + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + + c.drawLine( + posX, + posY - (2 * shapeHalf), + posX + (2 * shapeHalf), + posY, + renderPaint); + + c.drawLine( + posX, + posY - (2 * shapeHalf), + posX - (2 * shapeHalf), + posY, + renderPaint); + + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CircleShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CircleShapeRenderer.java new file mode 100644 index 0000000..ac7abb9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CircleShapeRenderer.java @@ -0,0 +1,63 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class CircleShapeRenderer implements IShapeRenderer +{ + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeSize = dataSet.getScatterShapeSize(); + final float shapeHalf = shapeSize / 2f; + final float shapeHoleSizeHalf = Utils.convertDpToPixel(dataSet.getScatterShapeHoleRadius()); + final float shapeHoleSize = shapeHoleSizeHalf * 2.f; + final float shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.f; + final float shapeStrokeSizeHalf = shapeStrokeSize / 2.f; + + final int shapeHoleColor = dataSet.getScatterShapeHoleColor(); + + if (shapeSize > 0.0) { + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(shapeStrokeSize); + + c.drawCircle( + posX, + posY, + shapeHoleSizeHalf + shapeStrokeSizeHalf, + renderPaint); + + if (shapeHoleColor != ColorTemplate.COLOR_NONE) { + renderPaint.setStyle(Paint.Style.FILL); + + renderPaint.setColor(shapeHoleColor); + c.drawCircle( + posX, + posY, + shapeHoleSizeHalf, + renderPaint); + } + } else { + renderPaint.setStyle(Paint.Style.FILL); + + c.drawCircle( + posX, + posY, + shapeHalf, + renderPaint); + } + + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CrossShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CrossShapeRenderer.java new file mode 100644 index 0000000..202670d --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CrossShapeRenderer.java @@ -0,0 +1,41 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class CrossShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeHalf = dataSet.getScatterShapeSize() / 2f; + + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + + c.drawLine( + posX - shapeHalf, + posY, + posX + shapeHalf, + posY, + renderPaint); + c.drawLine( + posX, + posY - shapeHalf, + posX, + posY + shapeHalf, + renderPaint); + + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/IShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/IShapeRenderer.java new file mode 100644 index 0000000..20b57a9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/IShapeRenderer.java @@ -0,0 +1,28 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:07 + */ +public interface IShapeRenderer +{ + + /** + * Renders the provided ScatterDataSet with a shape. + * + * @param c Canvas object for drawing the shape + * @param dataSet The DataSet to be drawn + * @param viewPortHandler Contains information about the current state of the view + * @param posX Position to draw the shape at + * @param posY Position to draw the shape at + * @param renderPaint Paint object used for styling and drawing + */ + void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/SquareShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/SquareShapeRenderer.java new file mode 100644 index 0000000..ac98679 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/SquareShapeRenderer.java @@ -0,0 +1,63 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class SquareShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeSize = dataSet.getScatterShapeSize(); + final float shapeHalf = shapeSize / 2f; + final float shapeHoleSizeHalf = Utils.convertDpToPixel(dataSet.getScatterShapeHoleRadius()); + final float shapeHoleSize = shapeHoleSizeHalf * 2.f; + final float shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.f; + final float shapeStrokeSizeHalf = shapeStrokeSize / 2.f; + + final int shapeHoleColor = dataSet.getScatterShapeHoleColor(); + + if (shapeSize > 0.0) { + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(shapeStrokeSize); + + c.drawRect(posX - shapeHoleSizeHalf - shapeStrokeSizeHalf, + posY - shapeHoleSizeHalf - shapeStrokeSizeHalf, + posX + shapeHoleSizeHalf + shapeStrokeSizeHalf, + posY + shapeHoleSizeHalf + shapeStrokeSizeHalf, + renderPaint); + + if (shapeHoleColor != ColorTemplate.COLOR_NONE) { + renderPaint.setStyle(Paint.Style.FILL); + + renderPaint.setColor(shapeHoleColor); + c.drawRect(posX - shapeHoleSizeHalf, + posY - shapeHoleSizeHalf, + posX + shapeHoleSizeHalf, + posY + shapeHoleSizeHalf, + renderPaint); + } + + } else { + renderPaint.setStyle(Paint.Style.FILL); + + c.drawRect(posX - shapeHalf, + posY - shapeHalf, + posX + shapeHalf, + posY + shapeHalf, + renderPaint); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/TriangleShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/TriangleShapeRenderer.java new file mode 100644 index 0000000..5343454 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/TriangleShapeRenderer.java @@ -0,0 +1,80 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class TriangleShapeRenderer implements IShapeRenderer +{ + + protected Path mTrianglePathBuffer = new Path(); + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeSize = dataSet.getScatterShapeSize(); + final float shapeHalf = shapeSize / 2f; + final float shapeHoleSizeHalf = Utils.convertDpToPixel(dataSet.getScatterShapeHoleRadius()); + final float shapeHoleSize = shapeHoleSizeHalf * 2.f; + final float shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.f; + + final int shapeHoleColor = dataSet.getScatterShapeHoleColor(); + + renderPaint.setStyle(Paint.Style.FILL); + + // create a triangle path + Path tri = mTrianglePathBuffer; + tri.reset(); + + tri.moveTo(posX, posY - shapeHalf); + tri.lineTo(posX + shapeHalf, posY + shapeHalf); + tri.lineTo(posX - shapeHalf, posY + shapeHalf); + + if (shapeSize > 0.0) { + tri.lineTo(posX, posY - shapeHalf); + + tri.moveTo(posX - shapeHalf + shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + tri.lineTo(posX + shapeHalf - shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + tri.lineTo(posX, + posY - shapeHalf + shapeStrokeSize); + tri.lineTo(posX - shapeHalf + shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + } + + tri.close(); + + c.drawPath(tri, renderPaint); + tri.reset(); + + if (shapeSize > 0.0 && + shapeHoleColor != ColorTemplate.COLOR_NONE) { + + renderPaint.setColor(shapeHoleColor); + + tri.moveTo(posX, + posY - shapeHalf + shapeStrokeSize); + tri.lineTo(posX + shapeHalf - shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + tri.lineTo(posX - shapeHalf + shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + tri.close(); + + c.drawPath(tri, renderPaint); + tri.reset(); + } + + } + +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/XShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/XShapeRenderer.java new file mode 100644 index 0000000..225640e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/XShapeRenderer.java @@ -0,0 +1,42 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class XShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeHalf = dataSet.getScatterShapeSize() / 2f; + + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + + c.drawLine( + posX - shapeHalf, + posY - shapeHalf, + posX + shapeHalf, + posY + shapeHalf, + renderPaint); + c.drawLine( + posX + shapeHalf, + posY - shapeHalf, + posX - shapeHalf, + posY + shapeHalf, + renderPaint); + + } + +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ColorTemplate.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ColorTemplate.java new file mode 100644 index 0000000..4d9c1de --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ColorTemplate.java @@ -0,0 +1,128 @@ + +package com.github.mikephil.charting.utils; + +import android.content.res.Resources; +import android.graphics.Color; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class that holds predefined color integer arrays (e.g. + * ColorTemplate.VORDIPLOM_COLORS) and convenience methods for loading colors + * from resources. + * + * @author Philipp Jahoda + */ +public class ColorTemplate { + + /** + * an "invalid" color that indicates that no color is set + */ + public static final int COLOR_NONE = 0x00112233; + + /** + * this "color" is used for the Legend creation and indicates that the next + * form should be skipped + */ + public static final int COLOR_SKIP = 0x00112234; + + /** + * THE COLOR THEMES ARE PREDEFINED (predefined color integer arrays), FEEL + * FREE TO CREATE YOUR OWN WITH AS MANY DIFFERENT COLORS AS YOU WANT + */ + public static final int[] LIBERTY_COLORS = { + Color.rgb(207, 248, 246), Color.rgb(148, 212, 212), Color.rgb(136, 180, 187), + Color.rgb(118, 174, 175), Color.rgb(42, 109, 130) + }; + public static final int[] JOYFUL_COLORS = { + Color.rgb(217, 80, 138), Color.rgb(254, 149, 7), Color.rgb(254, 247, 120), + Color.rgb(106, 167, 134), Color.rgb(53, 194, 209) + }; + public static final int[] PASTEL_COLORS = { + Color.rgb(64, 89, 128), Color.rgb(149, 165, 124), Color.rgb(217, 184, 162), + Color.rgb(191, 134, 134), Color.rgb(179, 48, 80) + }; + public static final int[] COLORFUL_COLORS = { + Color.rgb(193, 37, 82), Color.rgb(255, 102, 0), Color.rgb(245, 199, 0), + Color.rgb(106, 150, 31), Color.rgb(179, 100, 53) + }; + public static final int[] VORDIPLOM_COLORS = { + Color.rgb(192, 255, 140), Color.rgb(255, 247, 140), Color.rgb(255, 208, 140), + Color.rgb(140, 234, 255), Color.rgb(255, 140, 157) + }; + public static final int[] MATERIAL_COLORS = { + rgb("#2ecc71"), rgb("#f1c40f"), rgb("#e74c3c"), rgb("#3498db") + }; + + /** + * Converts the given hex-color-string to rgb. + * + * @param hex + * @return + */ + public static int rgb(String hex) { + int color = (int) Long.parseLong(hex.replace("#", ""), 16); + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = (color >> 0) & 0xFF; + return Color.rgb(r, g, b); + } + + /** + * Returns the Android ICS holo blue light color. + * + * @return + */ + public static int getHoloBlue() { + return Color.rgb(51, 181, 229); + } + + /** + * Sets the alpha component of the given color. + * + * @param color + * @param alpha 0 - 255 + * @return + */ + public static int colorWithAlpha(int color, int alpha) { + return (color & 0xffffff) | ((alpha & 0xff) << 24); + } + + /** + * turn an array of resource-colors (contains resource-id integers) into an + * array list of actual color integers + * + * @param r + * @param colors an integer array of resource id's of colors + * @return + */ + public static List createColors(Resources r, int[] colors) { + + List result = new ArrayList(); + + for (int i : colors) { + result.add(r.getColor(i)); + } + + return result; + } + + /** + * Turns an array of colors (integer color values) into an ArrayList of + * colors. + * + * @param colors + * @return + */ + public static List createColors(int[] colors) { + + List result = new ArrayList(); + + for (int i : colors) { + result.add(i); + } + + return result; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/EntryXComparator.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/EntryXComparator.java new file mode 100644 index 0000000..8f59c12 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/EntryXComparator.java @@ -0,0 +1,22 @@ +package com.github.mikephil.charting.utils; + +import com.github.mikephil.charting.data.Entry; + +import java.util.Comparator; + +/** + * Comparator for comparing Entry-objects by their x-value. + * Created by philipp on 17/06/15. + */ +public class EntryXComparator implements Comparator { + @Override + public int compare(Entry entry1, Entry entry2) { + float diff = entry1.getX() - entry2.getX(); + + if (diff == 0f) return 0; + else { + if (diff > 0f) return 1; + else return -1; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FSize.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FSize.java new file mode 100644 index 0000000..a12bc45 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FSize.java @@ -0,0 +1,79 @@ + +package com.github.mikephil.charting.utils; + +import java.util.List; + +/** + * Class for describing width and height dimensions in some arbitrary + * unit. Replacement for the android.Util.SizeF which is available only on API >= 21. + */ +public final class FSize extends ObjectPool.Poolable{ + + // TODO : Encapsulate width & height + + public float width; + public float height; + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(256, new FSize(0,0)); + pool.setReplenishPercentage(0.5f); + } + + + protected ObjectPool.Poolable instantiate(){ + return new FSize(0,0); + } + + public static FSize getInstance(final float width, final float height){ + FSize result = pool.get(); + result.width = width; + result.height = height; + return result; + } + + public static void recycleInstance(FSize instance){ + pool.recycle(instance); + } + + public static void recycleInstances(List instances){ + pool.recycle(instances); + } + + public FSize() { + } + + public FSize(final float width, final float height) { + this.width = width; + this.height = height; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj instanceof FSize) { + final FSize other = (FSize) obj; + return width == other.width && height == other.height; + } + return false; + } + + @Override + public String toString() { + return width + "x" + height; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Float.floatToIntBits(width) ^ Float.floatToIntBits(height); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FileUtils.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FileUtils.java new file mode 100644 index 0000000..5aff51f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FileUtils.java @@ -0,0 +1,301 @@ + +package com.github.mikephil.charting.utils; + +import android.content.res.AssetManager; +import android.os.Environment; +import android.util.Log; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Utilities class for interacting with the assets and the devices storage to + * load and save DataSet objects from and to .txt files. + * + * @author Philipp Jahoda + */ +public class FileUtils { + + private static final String LOG = "MPChart-FileUtils"; + + /** + * Loads a an Array of Entries from a textfile from the sd-card. + * + * @param path the name of the file on the sd-card (+ path if needed) + * @return + */ + public static List loadEntriesFromFile(String path) { + + File sdcard = Environment.getExternalStorageDirectory(); + + // Get the text file + File file = new File(sdcard, path); + + List entries = new ArrayList(); + + try { + @SuppressWarnings("resource") + BufferedReader br = new BufferedReader(new FileReader(file)); + String line; + + while ((line = br.readLine()) != null) { + String[] split = line.split("#"); + + if (split.length <= 2) { + entries.add(new Entry(Float.parseFloat(split[0]), Integer.parseInt(split[1]))); + } else { + + float[] vals = new float[split.length - 1]; + + for (int i = 0; i < vals.length; i++) { + vals[i] = Float.parseFloat(split[i]); + } + + entries.add(new BarEntry(Integer.parseInt(split[split.length - 1]), vals)); + } + } + } catch (IOException e) { + Log.e(LOG, e.toString()); + } + + return entries; + + // File sdcard = Environment.getExternalStorageDirectory(); + // + // // Get the text file + // File file = new File(sdcard, path); + // + // List entries = new ArrayList(); + // String label = ""; + // + // try { + // @SuppressWarnings("resource") + // BufferedReader br = new BufferedReader(new FileReader(file)); + // String line = br.readLine(); + // + // // firstline is the label + // label = line; + // + // while ((line = br.readLine()) != null) { + // String[] split = line.split("#"); + // entries.add(new Entry(Float.parseFloat(split[0]), + // Integer.parseInt(split[1]))); + // } + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // } + // + // DataSet ds = new DataSet(entries, label); + // return ds; + } + + /** + * Loads an array of Entries from a textfile from the assets folder. + * + * @param am + * @param path the name of the file in the assets folder (+ path if needed) + * @return + */ + public static List loadEntriesFromAssets(AssetManager am, String path) { + + List entries = new ArrayList(); + + BufferedReader reader = null; + try { + reader = new BufferedReader( + new InputStreamReader(am.open(path), "UTF-8")); + + String line = reader.readLine(); + + while (line != null) { + // process line + String[] split = line.split("#"); + + if (split.length <= 2) { + entries.add(new Entry(Float.parseFloat(split[1]), Float.parseFloat(split[0]))); + } else { + + float[] vals = new float[split.length - 1]; + + for (int i = 0; i < vals.length; i++) { + vals[i] = Float.parseFloat(split[i]); + } + + entries.add(new BarEntry(Integer.parseInt(split[split.length - 1]), vals)); + } + line = reader.readLine(); + } + } catch (IOException e) { + Log.e(LOG, e.toString()); + + } finally { + + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + Log.e(LOG, e.toString()); + } + } + } + + return entries; + + // String label = null; + // List entries = new ArrayList(); + // + // BufferedReader reader = null; + // try { + // reader = new BufferedReader( + // new InputStreamReader(am.open(path), "UTF-8")); + // + // // do reading, usually loop until end of file reading + // label = reader.readLine(); + // String line = reader.readLine(); + // + // while (line != null) { + // // process line + // String[] split = line.split("#"); + // entries.add(new Entry(Float.parseFloat(split[0]), + // Integer.parseInt(split[1]))); + // line = reader.readLine(); + // } + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // + // } finally { + // + // if (reader != null) { + // try { + // reader.close(); + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // } + // } + // } + // + // DataSet ds = new DataSet(entries, label); + // return ds; + } + + /** + * Saves an Array of Entries to the specified location on the sdcard + * + * @param entries + * @param path + */ + public static void saveToSdCard(List entries, String path) { + + File sdcard = Environment.getExternalStorageDirectory(); + + File saved = new File(sdcard, path); + if (!saved.exists()) + { + try + { + saved.createNewFile(); + } catch (IOException e) + { + Log.e(LOG, e.toString()); + } + } + try + { + // BufferedWriter for performance, true to set append to file flag + BufferedWriter buf = new BufferedWriter(new FileWriter(saved, true)); + + for (Entry e : entries) { + + buf.append(e.getY() + "#" + e.getX()); + buf.newLine(); + } + + buf.close(); + } catch (IOException e) + { + Log.e(LOG, e.toString()); + } + } + + public static List loadBarEntriesFromAssets(AssetManager am, String path) { + + List entries = new ArrayList(); + + BufferedReader reader = null; + try { + reader = new BufferedReader( + new InputStreamReader(am.open(path), "UTF-8")); + + String line = reader.readLine(); + + while (line != null) { + // process line + String[] split = line.split("#"); + + entries.add(new BarEntry(Float.parseFloat(split[1]), Float.parseFloat(split[0]))); + + line = reader.readLine(); + } + } catch (IOException e) { + Log.e(LOG, e.toString()); + + } finally { + + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + Log.e(LOG, e.toString()); + } + } + } + + return entries; + + // String label = null; + // ArrayList entries = new ArrayList(); + // + // BufferedReader reader = null; + // try { + // reader = new BufferedReader( + // new InputStreamReader(am.open(path), "UTF-8")); + // + // // do reading, usually loop until end of file reading + // label = reader.readLine(); + // String line = reader.readLine(); + // + // while (line != null) { + // // process line + // String[] split = line.split("#"); + // entries.add(new Entry(Float.parseFloat(split[0]), + // Integer.parseInt(split[1]))); + // line = reader.readLine(); + // } + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // + // } finally { + // + // if (reader != null) { + // try { + // reader.close(); + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // } + // } + // } + // + // DataSet ds = new DataSet(entries, label); + // return ds; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Fill.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Fill.java new file mode 100644 index 0000000..d12e1fb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Fill.java @@ -0,0 +1,342 @@ +package com.github.mikephil.charting.utils; + +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class Fill +{ + public enum Type + { + EMPTY, COLOR, LINEAR_GRADIENT, DRAWABLE + } + + public enum Direction + { + DOWN, UP, RIGHT, LEFT + } + + /** + * the type of fill + */ + private Type mType = Type.EMPTY; + + /** + * the color that is used for filling + */ + @Nullable + private Integer mColor = null; + + private Integer mFinalColor = null; + + /** + * the drawable to be used for filling + */ + @Nullable + protected Drawable mDrawable; + + @Nullable + private int[] mGradientColors; + + @Nullable + private float[] mGradientPositions; + + /** + * transparency used for filling + */ + private int mAlpha = 255; + + public Fill() + { + } + + public Fill(int color) + { + this.mType = Type.COLOR; + this.mColor = color; + calculateFinalColor(); + } + + public Fill(int startColor, int endColor) + { + this.mType = Type.LINEAR_GRADIENT; + this.mGradientColors = new int[]{startColor, endColor}; + } + + public Fill(@NonNull int[] gradientColors) + { + this.mType = Type.LINEAR_GRADIENT; + this.mGradientColors = gradientColors; + } + + public Fill(@NonNull int[] gradientColors, @NonNull float[] gradientPositions) + { + this.mType = Type.LINEAR_GRADIENT; + this.mGradientColors = gradientColors; + this.mGradientPositions = gradientPositions; + } + + public Fill(@NonNull Drawable drawable) + { + this.mType = Type.DRAWABLE; + this.mDrawable = drawable; + } + + public Type getType() + { + return mType; + } + + public void setType(Type type) + { + this.mType = type; + } + + @Nullable + public Integer getColor() + { + return mColor; + } + + public void setColor(int color) + { + this.mColor = color; + calculateFinalColor(); + } + + public int[] getGradientColors() + { + return mGradientColors; + } + + public void setGradientColors(int[] colors) + { + this.mGradientColors = colors; + } + + public float[] getGradientPositions() + { + return mGradientPositions; + } + + public void setGradientPositions(float[] positions) + { + this.mGradientPositions = positions; + } + + public void setGradientColors(int startColor, int endColor) + { + this.mGradientColors = new int[]{startColor, endColor}; + } + + public int getAlpha() + { + return mAlpha; + } + + public void setAlpha(int alpha) + { + this.mAlpha = alpha; + calculateFinalColor(); + } + + private void calculateFinalColor() + { + if (mColor == null) + { + mFinalColor = null; + } else + { + int alpha = (int) Math.floor(((mColor >> 24) / 255.0) * (mAlpha / 255.0) * 255.0); + mFinalColor = (alpha << 24) | (mColor & 0xffffff); + } + } + + public void fillRect(Canvas c, Paint paint, + float left, float top, float right, float bottom, + Direction gradientDirection) + { + switch (mType) + { + case EMPTY: + return; + + case COLOR: + { + if (mFinalColor == null) return; + + if (isClipPathSupported()) + { + int save = c.save(); + + c.clipRect(left, top, right, bottom); + c.drawColor(mFinalColor); + + c.restoreToCount(save); + } + else + { + // save + Paint.Style previous = paint.getStyle(); + int previousColor = paint.getColor(); + + // set + paint.setStyle(Paint.Style.FILL); + paint.setColor(mFinalColor); + + c.drawRect(left, top, right, bottom, paint); + + // restore + paint.setColor(previousColor); + paint.setStyle(previous); + } + } + break; + + case LINEAR_GRADIENT: + { + if (mGradientColors == null) return; + + LinearGradient gradient = new LinearGradient( + (int) (gradientDirection == Direction.RIGHT + ? right + : gradientDirection == Direction.LEFT + ? left + : left), + (int) (gradientDirection == Direction.UP + ? bottom + : gradientDirection == Direction.DOWN + ? top + : top), + (int) (gradientDirection == Direction.RIGHT + ? left + : gradientDirection == Direction.LEFT + ? right + : left), + (int) (gradientDirection == Direction.UP + ? top + : gradientDirection == Direction.DOWN + ? bottom + : top), + mGradientColors, + mGradientPositions, + android.graphics.Shader.TileMode.MIRROR); + + paint.setShader(gradient); + + c.drawRect(left, top, right, bottom, paint); + } + break; + + case DRAWABLE: + { + if (mDrawable == null) return; + + mDrawable.setBounds((int) left, (int) top, (int) right, (int) bottom); + mDrawable.draw(c); + } + break; + } + } + + public void fillPath(Canvas c, Path path, Paint paint, + @Nullable RectF clipRect) + { + switch (mType) + { + case EMPTY: + return; + + case COLOR: + { + if (mFinalColor == null) return; + + if (clipRect != null && isClipPathSupported()) + { + int save = c.save(); + + c.clipPath(path); + c.drawColor(mFinalColor); + + c.restoreToCount(save); + } + else + { + // save + Paint.Style previous = paint.getStyle(); + int previousColor = paint.getColor(); + + // set + paint.setStyle(Paint.Style.FILL); + paint.setColor(mFinalColor); + + c.drawPath(path, paint); + + // restore + paint.setColor(previousColor); + paint.setStyle(previous); + } + } + break; + + case LINEAR_GRADIENT: + { + if (mGradientColors == null) return; + + LinearGradient gradient = new LinearGradient( + 0, + 0, + c.getWidth(), + c.getHeight(), + mGradientColors, + mGradientPositions, + android.graphics.Shader.TileMode.MIRROR); + + paint.setShader(gradient); + + c.drawPath(path, paint); + } + break; + + case DRAWABLE: + { + if (mDrawable == null) return; + + ensureClipPathSupported(); + + int save = c.save(); + c.clipPath(path); + + mDrawable.setBounds( + clipRect == null ? 0 : (int) clipRect.left, + clipRect == null ? 0 : (int) clipRect.top, + clipRect == null ? c.getWidth() : (int) clipRect.right, + clipRect == null ? c.getHeight() : (int) clipRect.bottom); + mDrawable.draw(c); + + c.restoreToCount(save); + } + break; + } + } + + private boolean isClipPathSupported() + { + return Utils.getSDKInt() >= 18; + } + + private void ensureClipPathSupported() + { + if (Utils.getSDKInt() < 18) + { + throw new RuntimeException("Fill-drawables not (yet) supported below API level 18, " + + "this code was run on API level " + Utils.getSDKInt() + "."); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/HorizontalViewPortHandler.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/HorizontalViewPortHandler.java new file mode 100644 index 0000000..5a415b0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/HorizontalViewPortHandler.java @@ -0,0 +1,29 @@ + +package com.github.mikephil.charting.utils; + +/** + * ViewPortHandler for HorizontalBarChart. + */ +public class HorizontalViewPortHandler extends ViewPortHandler { + + +// @Override +// public void setMinimumScaleX(float xScale) { +// setMinimumScaleY(xScale); +// } +// +// @Override +// public void setMinimumScaleY(float yScale) { +// setMinimumScaleX(yScale); +// } +// +// @Override +// public void setMinMaxScaleX(float minScaleX, float maxScaleX) { +// setMinMaxScaleY(minScaleX, maxScaleX); +// } +// +// @Override +// public void setMinMaxScaleY(float minScaleY, float maxScaleY) { +// setMinMaxScaleX(minScaleY, maxScaleY); +// } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointD.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointD.java new file mode 100644 index 0000000..f6220a7 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointD.java @@ -0,0 +1,53 @@ + +package com.github.mikephil.charting.utils; + +import java.util.List; + +/** + * Point encapsulating two double values. + * + * @author Philipp Jahoda + */ +public class MPPointD extends ObjectPool.Poolable { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(64, new MPPointD(0,0)); + pool.setReplenishPercentage(0.5f); + } + + public static MPPointD getInstance(double x, double y){ + MPPointD result = pool.get(); + result.x = x; + result.y = y; + return result; + } + + public static void recycleInstance(MPPointD instance){ + pool.recycle(instance); + } + + public static void recycleInstances(List instances){ + pool.recycle(instances); + } + + public double x; + public double y; + + protected ObjectPool.Poolable instantiate(){ + return new MPPointD(0,0); + } + + private MPPointD(double x, double y) { + this.x = x; + this.y = y; + } + + /** + * returns a string representation of the object + */ + public String toString() { + return "MPPointD, x: " + x + ", y: " + y; + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointF.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointF.java new file mode 100644 index 0000000..fb5a00f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointF.java @@ -0,0 +1,99 @@ +package com.github.mikephil.charting.utils; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.List; + +/** + * Created by Tony Patino on 6/24/16. + */ +public class MPPointF extends ObjectPool.Poolable { + + private static ObjectPool pool; + + public float x; + public float y; + + static { + pool = ObjectPool.create(32, new MPPointF(0,0)); + pool.setReplenishPercentage(0.5f); + } + + public MPPointF() { + } + + public MPPointF(float x, float y) { + this.x = x; + this.y = y; + } + + public static MPPointF getInstance(float x, float y) { + MPPointF result = pool.get(); + result.x = x; + result.y = y; + return result; + } + + public static MPPointF getInstance() { + return pool.get(); + } + + public static MPPointF getInstance(MPPointF copy) { + MPPointF result = pool.get(); + result.x = copy.x; + result.y = copy.y; + return result; + } + + public static void recycleInstance(MPPointF instance){ + pool.recycle(instance); + } + + public static void recycleInstances(List instances){ + pool.recycle(instances); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + /** + * Return a new point from the data in the specified parcel. + */ + public MPPointF createFromParcel(Parcel in) { + MPPointF r = new MPPointF(0,0); + r.my_readFromParcel(in); + return r; + } + + /** + * Return an array of rectangles of the specified size. + */ + public MPPointF[] newArray(int size) { + return new MPPointF[size]; + } + }; + + /** + * Set the point's coordinates from the data stored in the specified + * parcel. To write a point to a parcel, call writeToParcel(). + * Provided to support older Android devices. + * + * @param in The parcel to read the point's coordinates from + */ + public void my_readFromParcel(Parcel in) { + x = in.readFloat(); + y = in.readFloat(); + } + + public float getX(){ + return this.x; + } + + public float getY(){ + return this.y; + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new MPPointF(0,0); + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ObjectPool.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ObjectPool.java new file mode 100644 index 0000000..d1d5437 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ObjectPool.java @@ -0,0 +1,218 @@ +package com.github.mikephil.charting.utils; + +import java.util.List; + +/** + * An object pool for recycling of object instances extending Poolable. + * + * + * Cost/Benefit : + * Cost - The pool can only contain objects extending Poolable. + * Benefit - The pool can very quickly determine if an object is elligable for storage without iteration. + * Benefit - The pool can also know if an instance of Poolable is already stored in a different pool instance. + * Benefit - The pool can grow as needed, if it is empty + * Cost - However, refilling the pool when it is empty might incur a time cost with sufficiently large capacity. Set the replenishPercentage to a lower number if this is a concern. + * + * Created by Tony Patino on 6/20/16. + */ +public class ObjectPool { + + private static int ids = 0; + + private int poolId; + private int desiredCapacity; + private Object[] objects; + private int objectsPointer; + private T modelObject; + private float replenishPercentage; + + + /** + * Returns the id of the given pool instance. + * + * @return an integer ID belonging to this pool instance. + */ + public int getPoolId(){ + return poolId; + } + + /** + * Returns an ObjectPool instance, of a given starting capacity, that recycles instances of a given Poolable object. + * + * @param withCapacity A positive integer value. + * @param object An instance of the object that the pool should recycle. + * @return + */ + public static synchronized ObjectPool create(int withCapacity, Poolable object){ + ObjectPool result = new ObjectPool(withCapacity, object); + result.poolId = ids; + ids++; + + return result; + } + + private ObjectPool(int withCapacity, T object){ + if(withCapacity <= 0){ + throw new IllegalArgumentException("Object Pool must be instantiated with a capacity greater than 0!"); + } + this.desiredCapacity = withCapacity; + this.objects = new Object[this.desiredCapacity]; + this.objectsPointer = 0; + this.modelObject = object; + this.replenishPercentage = 1.0f; + this.refillPool(); + } + + /** + * Set the percentage of the pool to replenish on empty. Valid values are between + * 0.00f and 1.00f + * + * @param percentage a value between 0 and 1, representing the percentage of the pool to replenish. + */ + public void setReplenishPercentage(float percentage){ + float p = percentage; + if(p > 1){ + p = 1; + } + else if(p < 0f){ + p = 0f; + } + this.replenishPercentage = p; + } + + public float getReplenishPercentage(){ + return replenishPercentage; + } + + private void refillPool(){ + this.refillPool(this.replenishPercentage); + } + + private void refillPool(float percentage){ + int portionOfCapacity = (int) (desiredCapacity * percentage); + + if(portionOfCapacity < 1){ + portionOfCapacity = 1; + }else if(portionOfCapacity > desiredCapacity){ + portionOfCapacity = desiredCapacity; + } + + for(int i = 0 ; i < portionOfCapacity ; i++){ + this.objects[i] = modelObject.instantiate(); + } + objectsPointer = portionOfCapacity - 1; + } + + /** + * Returns an instance of Poolable. If get() is called with an empty pool, the pool will be + * replenished. If the pool capacity is sufficiently large, this could come at a performance + * cost. + * + * @return An instance of Poolable object T + */ + public synchronized T get(){ + + if(this.objectsPointer == -1 && this.replenishPercentage > 0.0f){ + this.refillPool(); + } + + T result = (T)objects[this.objectsPointer]; + result.currentOwnerId = Poolable.NO_OWNER; + this.objectsPointer--; + + return result; + } + + /** + * Recycle an instance of Poolable that this pool is capable of generating. + * The T instance passed must not already exist inside this or any other ObjectPool instance. + * + * @param object An object of type T to recycle + */ + public synchronized void recycle(T object){ + if(object.currentOwnerId != Poolable.NO_OWNER){ + if(object.currentOwnerId == this.poolId){ + throw new IllegalArgumentException("The object passed is already stored in this pool!"); + }else { + throw new IllegalArgumentException("The object to recycle already belongs to poolId " + object.currentOwnerId + ". Object cannot belong to two different pool instances simultaneously!"); + } + } + + this.objectsPointer++; + if(this.objectsPointer >= objects.length){ + this.resizePool(); + } + + object.currentOwnerId = this.poolId; + objects[this.objectsPointer] = object; + + } + + /** + * Recycle a List of Poolables that this pool is capable of generating. + * The T instances passed must not already exist inside this or any other ObjectPool instance. + * + * @param objects A list of objects of type T to recycle + */ + public synchronized void recycle(List objects){ + while(objects.size() + this.objectsPointer + 1 > this.desiredCapacity){ + this.resizePool(); + } + final int objectsListSize = objects.size(); + + // Not relying on recycle(T object) because this is more performant. + for(int i = 0 ; i < objectsListSize ; i++){ + T object = objects.get(i); + if(object.currentOwnerId != Poolable.NO_OWNER){ + if(object.currentOwnerId == this.poolId){ + throw new IllegalArgumentException("The object passed is already stored in this pool!"); + }else { + throw new IllegalArgumentException("The object to recycle already belongs to poolId " + object.currentOwnerId + ". Object cannot belong to two different pool instances simultaneously!"); + } + } + object.currentOwnerId = this.poolId; + this.objects[this.objectsPointer + 1 + i] = object; + } + this.objectsPointer += objectsListSize; + } + + private void resizePool() { + final int oldCapacity = this.desiredCapacity; + this.desiredCapacity *= 2; + Object[] temp = new Object[this.desiredCapacity]; + for(int i = 0 ; i < oldCapacity ; i++){ + temp[i] = this.objects[i]; + } + this.objects = temp; + } + + /** + * Returns the capacity of this object pool. Note : The pool will automatically resize + * to contain additional objects if the user tries to add more objects than the pool's + * capacity allows, but this comes at a performance cost. + * + * @return The capacity of the pool. + */ + public int getPoolCapacity(){ + return this.objects.length; + } + + /** + * Returns the number of objects remaining in the pool, for diagnostic purposes. + * + * @return The number of objects remaining in the pool. + */ + public int getPoolCount(){ + return this.objectsPointer + 1; + } + + + public static abstract class Poolable{ + + public static int NO_OWNER = -1; + int currentOwnerId = NO_OWNER; + + protected abstract Poolable instantiate(); + + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Transformer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Transformer.java new file mode 100644 index 0000000..0496bd0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Transformer.java @@ -0,0 +1,459 @@ + +package com.github.mikephil.charting.utils; + +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.data.CandleEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet; +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; + +import java.util.List; + +/** + * Transformer class that contains all matrices and is responsible for + * transforming values into pixels on the screen and backwards. + * + * @author Philipp Jahoda + */ +public class Transformer { + + /** + * matrix to map the values to the screen pixels + */ + protected Matrix mMatrixValueToPx = new Matrix(); + + /** + * matrix for handling the different offsets of the chart + */ + protected Matrix mMatrixOffset = new Matrix(); + + protected ViewPortHandler mViewPortHandler; + + public Transformer(ViewPortHandler viewPortHandler) { + this.mViewPortHandler = viewPortHandler; + } + + /** + * Prepares the matrix that transforms values to pixels. Calculates the + * scale factors from the charts size and offsets. + * + * @param xChartMin + * @param deltaX + * @param deltaY + * @param yChartMin + */ + public void prepareMatrixValuePx(float xChartMin, float deltaX, float deltaY, float yChartMin) { + + float scaleX = (float) ((mViewPortHandler.contentWidth()) / deltaX); + float scaleY = (float) ((mViewPortHandler.contentHeight()) / deltaY); + + if (Float.isInfinite(scaleX)) { + scaleX = 0; + } + if (Float.isInfinite(scaleY)) { + scaleY = 0; + } + + // setup all matrices + mMatrixValueToPx.reset(); + mMatrixValueToPx.postTranslate(-xChartMin, -yChartMin); + mMatrixValueToPx.postScale(scaleX, -scaleY); + } + + /** + * Prepares the matrix that contains all offsets. + * + * @param inverted + */ + public void prepareMatrixOffset(boolean inverted) { + + mMatrixOffset.reset(); + + // offset.postTranslate(mOffsetLeft, getHeight() - mOffsetBottom); + + if (!inverted) + mMatrixOffset.postTranslate(mViewPortHandler.offsetLeft(), + mViewPortHandler.getChartHeight() - mViewPortHandler.offsetBottom()); + else { + mMatrixOffset + .setTranslate(mViewPortHandler.offsetLeft(), -mViewPortHandler.offsetTop()); + mMatrixOffset.postScale(1.0f, -1.0f); + } + } + + protected float[] valuePointsForGenerateTransformedValuesScatter = new float[1]; + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the SCATTERCHART. + * + * @param data + * @return + */ + public float[] generateTransformedValuesScatter(IScatterDataSet data, float phaseX, + float phaseY, int from, int to) { + + final int count = (int) ((to - from) * phaseX + 1) * 2; + + if (valuePointsForGenerateTransformedValuesScatter.length != count) { + valuePointsForGenerateTransformedValuesScatter = new float[count]; + } + float[] valuePoints = valuePointsForGenerateTransformedValuesScatter; + + for (int j = 0; j < count; j += 2) { + + Entry e = data.getEntryForIndex(j / 2 + from); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected float[] valuePointsForGenerateTransformedValuesBubble = new float[1]; + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the BUBBLECHART. + * + * @param data + * @return + */ + public float[] generateTransformedValuesBubble(IBubbleDataSet data, float phaseY, int from, int to) { + + final int count = (to - from + 1) * 2; // (int) Math.ceil((to - from) * phaseX) * 2; + + if (valuePointsForGenerateTransformedValuesBubble.length != count) { + valuePointsForGenerateTransformedValuesBubble = new float[count]; + } + float[] valuePoints = valuePointsForGenerateTransformedValuesBubble; + + for (int j = 0; j < count; j += 2) { + + Entry e = data.getEntryForIndex(j / 2 + from); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected float[] valuePointsForGenerateTransformedValuesLine = new float[1]; + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the LINECHART. + * + * @param data + * @return + */ + public float[] generateTransformedValuesLine(ILineDataSet data, + float phaseX, float phaseY, + int min, int max) { + + final int count = ((int) ((max - min) * phaseX) + 1) * 2; + + if (valuePointsForGenerateTransformedValuesLine.length != count) { + valuePointsForGenerateTransformedValuesLine = new float[count]; + } + float[] valuePoints = valuePointsForGenerateTransformedValuesLine; + + for (int j = 0; j < count; j += 2) { + + Entry e = data.getEntryForIndex(j / 2 + min); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected float[] valuePointsForGenerateTransformedValuesCandle = new float[1]; + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the CANDLESTICKCHART. + * + * @param data + * @return + */ + public float[] generateTransformedValuesCandle(ICandleDataSet data, + float phaseX, float phaseY, int from, int to) { + + final int count = (int) ((to - from) * phaseX + 1) * 2; + + if (valuePointsForGenerateTransformedValuesCandle.length != count) { + valuePointsForGenerateTransformedValuesCandle = new float[count]; + } + float[] valuePoints = valuePointsForGenerateTransformedValuesCandle; + + for (int j = 0; j < count; j += 2) { + + CandleEntry e = data.getEntryForIndex(j / 2 + from); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getHigh() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + /** + * transform a path with all the given matrices VERY IMPORTANT: keep order + * to value-touch-offset + * + * @param path + */ + public void pathValueToPixel(Path path) { + + path.transform(mMatrixValueToPx); + path.transform(mViewPortHandler.getMatrixTouch()); + path.transform(mMatrixOffset); + } + + /** + * Transforms multiple paths will all matrices. + * + * @param paths + */ + public void pathValuesToPixel(List paths) { + + for (int i = 0; i < paths.size(); i++) { + pathValueToPixel(paths.get(i)); + } + } + + /** + * Transform an array of points with all matrices. VERY IMPORTANT: Keep + * matrix order "value-touch-offset" when transforming. + * + * @param pts + */ + public void pointValuesToPixel(float[] pts) { + + mMatrixValueToPx.mapPoints(pts); + mViewPortHandler.getMatrixTouch().mapPoints(pts); + mMatrixOffset.mapPoints(pts); + } + + /** + * Transform a rectangle with all matrices. + * + * @param r + */ + public void rectValueToPixel(RectF r) { + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + /** + * Transform a rectangle with all matrices with potential animation phases. + * + * @param r + * @param phaseY + */ + public void rectToPixelPhase(RectF r, float phaseY) { + + // multiply the height of the rect with the phase + r.top *= phaseY; + r.bottom *= phaseY; + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + public void rectToPixelPhaseHorizontal(RectF r, float phaseY) { + + // multiply the height of the rect with the phase + r.left *= phaseY; + r.right *= phaseY; + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + /** + * Transform a rectangle with all matrices with potential animation phases. + * + * @param r + */ + public void rectValueToPixelHorizontal(RectF r) { + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + /** + * Transform a rectangle with all matrices with potential animation phases. + * + * @param r + * @param phaseY + */ + public void rectValueToPixelHorizontal(RectF r, float phaseY) { + + // multiply the height of the rect with the phase + r.left *= phaseY; + r.right *= phaseY; + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + /** + * transforms multiple rects with all matrices + * + * @param rects + */ + public void rectValuesToPixel(List rects) { + + Matrix m = getValueToPixelMatrix(); + + for (int i = 0; i < rects.size(); i++) + m.mapRect(rects.get(i)); + } + + protected Matrix mPixelToValueMatrixBuffer = new Matrix(); + + /** + * Transforms the given array of touch positions (pixels) (x, y, x, y, ...) + * into values on the chart. + * + * @param pixels + */ + public void pixelsToValue(float[] pixels) { + + Matrix tmp = mPixelToValueMatrixBuffer; + tmp.reset(); + + // invert all matrixes to convert back to the original value + mMatrixOffset.invert(tmp); + tmp.mapPoints(pixels); + + mViewPortHandler.getMatrixTouch().invert(tmp); + tmp.mapPoints(pixels); + + mMatrixValueToPx.invert(tmp); + tmp.mapPoints(pixels); + } + + /** + * buffer for performance + */ + float[] ptsBuffer = new float[2]; + + /** + * Returns a recyclable MPPointD instance. + * returns the x and y values in the chart at the given touch point + * (encapsulated in a MPPointD). This method transforms pixel coordinates to + * coordinates / values in the chart. This is the opposite method to + * getPixelForValues(...). + * + * @param x + * @param y + * @return + */ + public MPPointD getValuesByTouchPoint(float x, float y) { + + MPPointD result = MPPointD.getInstance(0, 0); + getValuesByTouchPoint(x, y, result); + return result; + } + + public void getValuesByTouchPoint(float x, float y, MPPointD outputPoint) { + + ptsBuffer[0] = x; + ptsBuffer[1] = y; + + pixelsToValue(ptsBuffer); + + outputPoint.x = ptsBuffer[0]; + outputPoint.y = ptsBuffer[1]; + } + + /** + * Returns a recyclable MPPointD instance. + * Returns the x and y coordinates (pixels) for a given x and y value in the chart. + * + * @param x + * @param y + * @return + */ + public MPPointD getPixelForValues(float x, float y) { + + ptsBuffer[0] = x; + ptsBuffer[1] = y; + + pointValuesToPixel(ptsBuffer); + + double xPx = ptsBuffer[0]; + double yPx = ptsBuffer[1]; + + return MPPointD.getInstance(xPx, yPx); + } + + public Matrix getValueMatrix() { + return mMatrixValueToPx; + } + + public Matrix getOffsetMatrix() { + return mMatrixOffset; + } + + private Matrix mMBuffer1 = new Matrix(); + + public Matrix getValueToPixelMatrix() { + mMBuffer1.set(mMatrixValueToPx); + mMBuffer1.postConcat(mViewPortHandler.mMatrixTouch); + mMBuffer1.postConcat(mMatrixOffset); + return mMBuffer1; + } + + private Matrix mMBuffer2 = new Matrix(); + + public Matrix getPixelToValueMatrix() { + getValueToPixelMatrix().invert(mMBuffer2); + return mMBuffer2; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/TransformerHorizontalBarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/TransformerHorizontalBarChart.java new file mode 100644 index 0000000..05fa82a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/TransformerHorizontalBarChart.java @@ -0,0 +1,44 @@ + +package com.github.mikephil.charting.utils; + +/** + * Transformer class for the HorizontalBarChart. + * + * @author Philipp Jahoda + */ +public class TransformerHorizontalBarChart extends Transformer { + + public TransformerHorizontalBarChart(ViewPortHandler viewPortHandler) { + super(viewPortHandler); + } + + /** + * Prepares the matrix that contains all offsets. + * + * @param inverted + */ + public void prepareMatrixOffset(boolean inverted) { + + mMatrixOffset.reset(); + + // offset.postTranslate(mOffsetLeft, getHeight() - mOffsetBottom); + + if (!inverted) + mMatrixOffset.postTranslate(mViewPortHandler.offsetLeft(), + mViewPortHandler.getChartHeight() - mViewPortHandler.offsetBottom()); + else { + mMatrixOffset + .setTranslate( + -(mViewPortHandler.getChartWidth() - mViewPortHandler.offsetRight()), + mViewPortHandler.getChartHeight() - mViewPortHandler.offsetBottom()); + mMatrixOffset.postScale(-1.0f, 1.0f); + } + + // mMatrixOffset.set(offset); + + // mMatrixOffset.reset(); + // + // mMatrixOffset.postTranslate(mOffsetLeft, getHeight() - + // mOffsetBottom); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Utils.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Utils.java new file mode 100644 index 0000000..c302673 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Utils.java @@ -0,0 +1,779 @@ + +package com.github.mikephil.charting.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SizeF; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +import com.github.mikephil.charting.formatter.DefaultValueFormatter; +import com.github.mikephil.charting.formatter.IValueFormatter; + +import java.util.List; + +/** + * Utilities class that has some helper methods. Needs to be initialized by + * calling Utils.init(...) before usage. Inside the Chart.init() method, this is + * done, if the Utils are used before that, Utils.init(...) needs to be called + * manually. + * + * @author Philipp Jahoda + */ +public abstract class Utils { + + private static DisplayMetrics mMetrics; + private static int mMinimumFlingVelocity = 50; + private static int mMaximumFlingVelocity = 8000; + public final static double DEG2RAD = (Math.PI / 180.0); + public final static float FDEG2RAD = ((float) Math.PI / 180.f); + + @SuppressWarnings("unused") + public final static double DOUBLE_EPSILON = Double.longBitsToDouble(1); + + @SuppressWarnings("unused") + public final static float FLOAT_EPSILON = Float.intBitsToFloat(1); + + /** + * initialize method, called inside the Chart.init() method. + * + * @param context + */ + @SuppressWarnings("deprecation") + public static void init(Context context) { + + if (context == null) { + // noinspection deprecation + mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); + // noinspection deprecation + mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); + + Log.e("MPChartLib-Utils" + , "Utils.init(...) PROVIDED CONTEXT OBJECT IS NULL"); + + } else { + ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); + mMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); + + Resources res = context.getResources(); + mMetrics = res.getDisplayMetrics(); + } + } + + /** + * initialize method, called inside the Chart.init() method. backwards + * compatibility - to not break existing code + * + * @param res + */ + @Deprecated + public static void init(Resources res) { + + mMetrics = res.getDisplayMetrics(); + + // noinspection deprecation + mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); + // noinspection deprecation + mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); + } + + /** + * This method converts dp unit to equivalent pixels, depending on device + * density. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE. + * + * @param dp A value in dp (density independent pixels) unit. Which we need + * to convert into pixels + * @return A float value to represent px equivalent to dp depending on + * device density + */ + public static float convertDpToPixel(float dp) { + + if (mMetrics == null) { + + Log.e("MPChartLib-Utils", + "Utils NOT INITIALIZED. You need to call Utils.init(...) at least once before" + + " calling Utils.convertDpToPixel(...). Otherwise conversion does not " + + "take place."); + return dp; + } + + return dp * mMetrics.density; + } + + /** + * This method converts device specific pixels to density independent + * pixels. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE. + * + * @param px A value in px (pixels) unit. Which we need to convert into db + * @return A float value to represent dp equivalent to px value + */ + public static float convertPixelsToDp(float px) { + + if (mMetrics == null) { + + Log.e("MPChartLib-Utils", + "Utils NOT INITIALIZED. You need to call Utils.init(...) at least once before" + + " calling Utils.convertPixelsToDp(...). Otherwise conversion does not" + + " take place."); + return px; + } + + return px / mMetrics.density; + } + + /** + * calculates the approximate width of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @return + */ + public static int calcTextWidth(Paint paint, String demoText) { + return (int) paint.measureText(demoText); + } + + private static Rect mCalcTextHeightRect = new Rect(); + /** + * calculates the approximate height of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @return + */ + public static int calcTextHeight(Paint paint, String demoText) { + + Rect r = mCalcTextHeightRect; + r.set(0,0,0,0); + paint.getTextBounds(demoText, 0, demoText.length(), r); + return r.height(); + } + + private static Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); + + public static float getLineHeight(Paint paint) { + return getLineHeight(paint, mFontMetrics); + } + + public static float getLineHeight(Paint paint, Paint.FontMetrics fontMetrics){ + paint.getFontMetrics(fontMetrics); + return fontMetrics.descent - fontMetrics.ascent; + } + + public static float getLineSpacing(Paint paint) { + return getLineSpacing(paint, mFontMetrics); + } + + public static float getLineSpacing(Paint paint, Paint.FontMetrics fontMetrics){ + paint.getFontMetrics(fontMetrics); + return fontMetrics.ascent - fontMetrics.top + fontMetrics.bottom; + } + + /** + * Returns a recyclable FSize instance. + * calculates the approximate size of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @return A Recyclable FSize instance + */ + public static FSize calcTextSize(Paint paint, String demoText) { + + FSize result = FSize.getInstance(0,0); + calcTextSize(paint, demoText, result); + return result; + } + + private static Rect mCalcTextSizeRect = new Rect(); + /** + * calculates the approximate size of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @param outputFSize An output variable, modified by the function. + */ + public static void calcTextSize(Paint paint, String demoText, FSize outputFSize) { + + Rect r = mCalcTextSizeRect; + r.set(0,0,0,0); + paint.getTextBounds(demoText, 0, demoText.length(), r); + outputFSize.width = r.width(); + outputFSize.height = r.height(); + + } + + + /** + * Math.pow(...) is very expensive, so avoid calling it and create it + * yourself. + */ + private static final int POW_10[] = { + 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 + }; + + private static IValueFormatter mDefaultValueFormatter = generateDefaultValueFormatter(); + + private static IValueFormatter generateDefaultValueFormatter() { + final DefaultValueFormatter formatter = new DefaultValueFormatter(1); + return formatter; + } + + /// - returns: The default value formatter used for all chart components that needs a default + public static IValueFormatter getDefaultValueFormatter() + { + return mDefaultValueFormatter; + } + + /** + * Formats the given number to the given number of decimals, and returns the + * number as a string, maximum 35 characters. If thousands are separated, the separating + * character is a dot ("."). + * + * @param number + * @param digitCount + * @param separateThousands set this to true to separate thousands values + * @return + */ + public static String formatNumber(float number, int digitCount, boolean separateThousands) { + return formatNumber(number, digitCount, separateThousands, '.'); + } + + /** + * Formats the given number to the given number of decimals, and returns the + * number as a string, maximum 35 characters. + * + * @param number + * @param digitCount + * @param separateThousands set this to true to separate thousands values + * @param separateChar a caracter to be paced between the "thousands" + * @return + */ + public static String formatNumber(float number, int digitCount, boolean separateThousands, + char separateChar) { + + char[] out = new char[35]; + + boolean neg = false; + if (number == 0) { + return "0"; + } + + boolean zero = false; + if (number < 1 && number > -1) { + zero = true; + } + + if (number < 0) { + neg = true; + number = -number; + } + + if (digitCount > POW_10.length) { + digitCount = POW_10.length - 1; + } + + number *= POW_10[digitCount]; + long lval = Math.round(number); + int ind = out.length - 1; + int charCount = 0; + boolean decimalPointAdded = false; + + while (lval != 0 || charCount < (digitCount + 1)) { + int digit = (int) (lval % 10); + lval = lval / 10; + out[ind--] = (char) (digit + '0'); + charCount++; + + // add decimal point + if (charCount == digitCount) { + out[ind--] = ','; + charCount++; + decimalPointAdded = true; + + // add thousand separators + } else if (separateThousands && lval != 0 && charCount > digitCount) { + + if (decimalPointAdded) { + + if ((charCount - digitCount) % 4 == 0) { + out[ind--] = separateChar; + charCount++; + } + + } else { + + if ((charCount - digitCount) % 4 == 3) { + out[ind--] = separateChar; + charCount++; + } + } + } + } + + // if number around zero (between 1 and -1) + if (zero) { + out[ind--] = '0'; + charCount += 1; + } + + // if the number is negative + if (neg) { + out[ind--] = '-'; + charCount += 1; + } + + int start = out.length - charCount; + + // use this instead of "new String(...)" because of issue < Android 4.0 + return String.valueOf(out, start, out.length - start); + } + + /** + * rounds the given number to the next significant number + * + * @param number + * @return + */ + public static float roundToNextSignificant(double number) { + if (Double.isInfinite(number) || + Double.isNaN(number) || + number == 0.0) + return 0; + + final float d = (float) Math.ceil((float) Math.log10(number < 0 ? -number : number)); + final int pw = 1 - (int) d; + final float magnitude = (float) Math.pow(10, pw); + final long shifted = Math.round(number * magnitude); + return shifted / magnitude; + } + + /** + * Returns the appropriate number of decimals to be used for the provided + * number. + * + * @param number + * @return + */ + public static int getDecimals(float number) { + + float i = roundToNextSignificant(number); + + if (Float.isInfinite(i)) + return 0; + + return (int) Math.ceil(-Math.log10(i)) + 2; + } + + /** + * Converts the provided Integer List to an int array. + * + * @param integers + * @return + */ + public static int[] convertIntegers(List integers) { + + int[] ret = new int[integers.size()]; + + copyIntegers(integers, ret); + + return ret; + } + + public static void copyIntegers(List from, int[] to){ + int count = to.length < from.size() ? to.length : from.size(); + for(int i = 0 ; i < count ; i++){ + to[i] = from.get(i); + } + } + + /** + * Converts the provided String List to a String array. + * + * @param strings + * @return + */ + public static String[] convertStrings(List strings) { + + String[] ret = new String[strings.size()]; + + for (int i = 0; i < ret.length; i++) { + ret[i] = strings.get(i); + } + + return ret; + } + + public static void copyStrings(List from, String[] to){ + int count = to.length < from.size() ? to.length : from.size(); + for(int i = 0 ; i < count ; i++){ + to[i] = from.get(i); + } + } + + /** + * Replacement for the Math.nextUp(...) method that is only available in + * HONEYCOMB and higher. Dat's some seeeeek sheeet. + * + * @param d + * @return + */ + public static double nextUp(double d) { + if (d == Double.POSITIVE_INFINITY) + return d; + else { + d += 0.0d; + return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + + ((d >= 0.0d) ? +1L : -1L)); + } + } + + /** + * Returns a recyclable MPPointF instance. + * Calculates the position around a center point, depending on the distance + * from the center, and the angle of the position around the center. + * + * @param center + * @param dist + * @param angle in degrees, converted to radians internally + * @return + */ + public static MPPointF getPosition(MPPointF center, float dist, float angle) { + + MPPointF p = MPPointF.getInstance(0,0); + getPosition(center, dist, angle, p); + return p; + } + + public static void getPosition(MPPointF center, float dist, float angle, MPPointF outputPoint){ + outputPoint.x = (float) (center.x + dist * Math.cos(Math.toRadians(angle))); + outputPoint.y = (float) (center.y + dist * Math.sin(Math.toRadians(angle))); + } + + public static void velocityTrackerPointerUpCleanUpIfNecessary(MotionEvent ev, + VelocityTracker tracker) { + + // Check the dot product of current velocities. + // If the pointer that left was opposing another velocity vector, clear. + tracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + final int upIndex = ev.getActionIndex(); + final int id1 = ev.getPointerId(upIndex); + final float x1 = tracker.getXVelocity(id1); + final float y1 = tracker.getYVelocity(id1); + for (int i = 0, count = ev.getPointerCount(); i < count; i++) { + if (i == upIndex) + continue; + + final int id2 = ev.getPointerId(i); + final float x = x1 * tracker.getXVelocity(id2); + final float y = y1 * tracker.getYVelocity(id2); + + final float dot = x + y; + if (dot < 0) { + tracker.clear(); + break; + } + } + } + + /** + * Original method view.postInvalidateOnAnimation() only supportd in API >= + * 16, This is a replica of the code from ViewCompat. + * + * @param view + */ + @SuppressLint("NewApi") + public static void postInvalidateOnAnimation(View view) { + if (Build.VERSION.SDK_INT >= 16) + view.postInvalidateOnAnimation(); + else + view.postInvalidateDelayed(10); + } + + public static int getMinimumFlingVelocity() { + return mMinimumFlingVelocity; + } + + public static int getMaximumFlingVelocity() { + return mMaximumFlingVelocity; + } + + /** + * returns an angle between 0.f < 360.f (not less than zero, less than 360) + */ + public static float getNormalizedAngle(float angle) { + while (angle < 0.f) + angle += 360.f; + + return angle % 360.f; + } + + private static Rect mDrawableBoundsCache = new Rect(); + + public static void drawImage(Canvas canvas, + Drawable drawable, + int x, int y, + int width, int height) { + + MPPointF drawOffset = MPPointF.getInstance(); + drawOffset.x = x - (width / 2); + drawOffset.y = y - (height / 2); + + drawable.copyBounds(mDrawableBoundsCache); + drawable.setBounds( + mDrawableBoundsCache.left, + mDrawableBoundsCache.top, + mDrawableBoundsCache.left + width, + mDrawableBoundsCache.top + width); + + int saveId = canvas.save(); + // translate to the correct position and draw + canvas.translate(drawOffset.x, drawOffset.y); + drawable.draw(canvas); + canvas.restoreToCount(saveId); + } + + private static Rect mDrawTextRectBuffer = new Rect(); + private static Paint.FontMetrics mFontMetricsBuffer = new Paint.FontMetrics(); + + public static void drawXAxisValue(Canvas c, String text, float x, float y, + Paint paint, + MPPointF anchor, float angleDegrees) { + + float drawOffsetX = 0.f; + float drawOffsetY = 0.f; + + final float lineHeight = paint.getFontMetrics(mFontMetricsBuffer); + paint.getTextBounds(text, 0, text.length(), mDrawTextRectBuffer); + + // Android sometimes has pre-padding + drawOffsetX -= mDrawTextRectBuffer.left; + + // Android does not snap the bounds to line boundaries, + // and draws from bottom to top. + // And we want to normalize it. + drawOffsetY += -mFontMetricsBuffer.ascent; + + // To have a consistent point of reference, we always draw left-aligned + Paint.Align originalTextAlign = paint.getTextAlign(); + paint.setTextAlign(Paint.Align.LEFT); + + if (angleDegrees != 0.f) { + + // Move the text drawing rect in a way that it always rotates around its center + drawOffsetX -= mDrawTextRectBuffer.width() * 0.5f; + drawOffsetY -= lineHeight * 0.5f; + + float translateX = x; + float translateY = y; + + // Move the "outer" rect relative to the anchor, assuming its centered + if (anchor.x != 0.5f || anchor.y != 0.5f) { + final FSize rotatedSize = getSizeOfRotatedRectangleByDegrees( + mDrawTextRectBuffer.width(), + lineHeight, + angleDegrees); + + translateX -= rotatedSize.width * (anchor.x - 0.5f); + translateY -= rotatedSize.height * (anchor.y - 0.5f); + FSize.recycleInstance(rotatedSize); + } + + c.save(); + c.translate(translateX, translateY); + c.rotate(angleDegrees); + + c.drawText(text, drawOffsetX, drawOffsetY, paint); + + c.restore(); + } else { + if (anchor.x != 0.f || anchor.y != 0.f) { + + drawOffsetX -= mDrawTextRectBuffer.width() * anchor.x; + drawOffsetY -= lineHeight * anchor.y; + } + + drawOffsetX += x; + drawOffsetY += y; + + c.drawText(text, drawOffsetX, drawOffsetY, paint); + } + + paint.setTextAlign(originalTextAlign); + } + + public static void drawMultilineText(Canvas c, StaticLayout textLayout, + float x, float y, + TextPaint paint, + MPPointF anchor, float angleDegrees) { + + float drawOffsetX = 0.f; + float drawOffsetY = 0.f; + float drawWidth; + float drawHeight; + + final float lineHeight = paint.getFontMetrics(mFontMetricsBuffer); + + drawWidth = textLayout.getWidth(); + drawHeight = textLayout.getLineCount() * lineHeight; + + // Android sometimes has pre-padding + drawOffsetX -= mDrawTextRectBuffer.left; + + // Android does not snap the bounds to line boundaries, + // and draws from bottom to top. + // And we want to normalize it. + drawOffsetY += drawHeight; + + // To have a consistent point of reference, we always draw left-aligned + Paint.Align originalTextAlign = paint.getTextAlign(); + paint.setTextAlign(Paint.Align.LEFT); + + if (angleDegrees != 0.f) { + + // Move the text drawing rect in a way that it always rotates around its center + drawOffsetX -= drawWidth * 0.5f; + drawOffsetY -= drawHeight * 0.5f; + + float translateX = x; + float translateY = y; + + // Move the "outer" rect relative to the anchor, assuming its centered + if (anchor.x != 0.5f || anchor.y != 0.5f) { + final FSize rotatedSize = getSizeOfRotatedRectangleByDegrees( + drawWidth, + drawHeight, + angleDegrees); + + translateX -= rotatedSize.width * (anchor.x - 0.5f); + translateY -= rotatedSize.height * (anchor.y - 0.5f); + FSize.recycleInstance(rotatedSize); + } + + c.save(); + c.translate(translateX, translateY); + c.rotate(angleDegrees); + + c.translate(drawOffsetX, drawOffsetY); + textLayout.draw(c); + + c.restore(); + } else { + if (anchor.x != 0.f || anchor.y != 0.f) { + + drawOffsetX -= drawWidth * anchor.x; + drawOffsetY -= drawHeight * anchor.y; + } + + drawOffsetX += x; + drawOffsetY += y; + + c.save(); + + c.translate(drawOffsetX, drawOffsetY); + textLayout.draw(c); + + c.restore(); + } + + paint.setTextAlign(originalTextAlign); + } + + public static void drawMultilineText(Canvas c, String text, + float x, float y, + TextPaint paint, + FSize constrainedToSize, + MPPointF anchor, float angleDegrees) { + + StaticLayout textLayout = new StaticLayout( + text, 0, text.length(), + paint, + (int) Math.max(Math.ceil(constrainedToSize.width), 1.f), + Layout.Alignment.ALIGN_NORMAL, 1.f, 0.f, false); + + + drawMultilineText(c, textLayout, x, y, paint, anchor, angleDegrees); + } + + /** + * Returns a recyclable FSize instance. + * Represents size of a rotated rectangle by degrees. + * + * @param rectangleSize + * @param degrees + * @return A Recyclable FSize instance + */ + public static FSize getSizeOfRotatedRectangleByDegrees(FSize rectangleSize, float degrees) { + final float radians = degrees * FDEG2RAD; + return getSizeOfRotatedRectangleByRadians(rectangleSize.width, rectangleSize.height, + radians); + } + + /** + * Returns a recyclable FSize instance. + * Represents size of a rotated rectangle by radians. + * + * @param rectangleSize + * @param radians + * @return A Recyclable FSize instance + */ + public static FSize getSizeOfRotatedRectangleByRadians(FSize rectangleSize, float radians) { + return getSizeOfRotatedRectangleByRadians(rectangleSize.width, rectangleSize.height, + radians); + } + + /** + * Returns a recyclable FSize instance. + * Represents size of a rotated rectangle by degrees. + * + * @param rectangleWidth + * @param rectangleHeight + * @param degrees + * @return A Recyclable FSize instance + */ + public static FSize getSizeOfRotatedRectangleByDegrees(float rectangleWidth, float + rectangleHeight, float degrees) { + final float radians = degrees * FDEG2RAD; + return getSizeOfRotatedRectangleByRadians(rectangleWidth, rectangleHeight, radians); + } + + /** + * Returns a recyclable FSize instance. + * Represents size of a rotated rectangle by radians. + * + * @param rectangleWidth + * @param rectangleHeight + * @param radians + * @return A Recyclable FSize instance + */ + public static FSize getSizeOfRotatedRectangleByRadians(float rectangleWidth, float + rectangleHeight, float radians) { + return FSize.getInstance( + Math.abs(rectangleWidth * (float) Math.cos(radians)) + Math.abs(rectangleHeight * + (float) Math.sin(radians)), + Math.abs(rectangleWidth * (float) Math.sin(radians)) + Math.abs(rectangleHeight * + (float) Math.cos(radians)) + ); + } + + public static int getSDKInt() { + return android.os.Build.VERSION.SDK_INT; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ViewPortHandler.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ViewPortHandler.java new file mode 100644 index 0000000..71b4b9b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ViewPortHandler.java @@ -0,0 +1,759 @@ + +package com.github.mikephil.charting.utils; + +import android.graphics.Matrix; +import android.graphics.RectF; +import android.view.View; + +/** + * Class that contains information about the charts current viewport settings, including offsets, scale & translation + * levels, ... + * + * @author Philipp Jahoda + */ +public class ViewPortHandler { + + /** + * matrix used for touch events + */ + protected final Matrix mMatrixTouch = new Matrix(); + + /** + * this rectangle defines the area in which graph values can be drawn + */ + protected RectF mContentRect = new RectF(); + + protected float mChartWidth = 0f; + protected float mChartHeight = 0f; + + /** + * minimum scale value on the y-axis + */ + private float mMinScaleY = 1f; + + /** + * maximum scale value on the y-axis + */ + private float mMaxScaleY = Float.MAX_VALUE; + + /** + * minimum scale value on the x-axis + */ + private float mMinScaleX = 1f; + + /** + * maximum scale value on the x-axis + */ + private float mMaxScaleX = Float.MAX_VALUE; + + /** + * contains the current scale factor of the x-axis + */ + private float mScaleX = 1f; + + /** + * contains the current scale factor of the y-axis + */ + private float mScaleY = 1f; + + /** + * current translation (drag distance) on the x-axis + */ + private float mTransX = 0f; + + /** + * current translation (drag distance) on the y-axis + */ + private float mTransY = 0f; + + /** + * offset that allows the chart to be dragged over its bounds on the x-axis + */ + private float mTransOffsetX = 0f; + + /** + * offset that allows the chart to be dragged over its bounds on the x-axis + */ + private float mTransOffsetY = 0f; + + /** + * Constructor - don't forget calling setChartDimens(...) + */ + public ViewPortHandler() { + + } + + /** + * Sets the width and height of the chart. + * + * @param width + * @param height + */ + + public void setChartDimens(float width, float height) { + + float offsetLeft = this.offsetLeft(); + float offsetTop = this.offsetTop(); + float offsetRight = this.offsetRight(); + float offsetBottom = this.offsetBottom(); + + mChartHeight = height; + mChartWidth = width; + + restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom); + } + + public boolean hasChartDimens() { + if (mChartHeight > 0 && mChartWidth > 0) + return true; + else + return false; + } + + public void restrainViewPort(float offsetLeft, float offsetTop, float offsetRight, + float offsetBottom) { + mContentRect.set(offsetLeft, offsetTop, mChartWidth - offsetRight, mChartHeight + - offsetBottom); + } + + public float offsetLeft() { + return mContentRect.left; + } + + public float offsetRight() { + return mChartWidth - mContentRect.right; + } + + public float offsetTop() { + return mContentRect.top; + } + + public float offsetBottom() { + return mChartHeight - mContentRect.bottom; + } + + public float contentTop() { + return mContentRect.top; + } + + public float contentLeft() { + return mContentRect.left; + } + + public float contentRight() { + return mContentRect.right; + } + + public float contentBottom() { + return mContentRect.bottom; + } + + public float contentWidth() { + return mContentRect.width(); + } + + public float contentHeight() { + return mContentRect.height(); + } + + public RectF getContentRect() { + return mContentRect; + } + + public MPPointF getContentCenter() { + return MPPointF.getInstance(mContentRect.centerX(), mContentRect.centerY()); + } + + public float getChartHeight() { + return mChartHeight; + } + + public float getChartWidth() { + return mChartWidth; + } + + /** + * Returns the smallest extension of the content rect (width or height). + * + * @return + */ + public float getSmallestContentExtension() { + return Math.min(mContentRect.width(), mContentRect.height()); + } + + /** + * ################ ################ ################ ################ + */ + /** CODE BELOW THIS RELATED TO SCALING AND GESTURES */ + + /** + * Zooms in by 1.4f, x and y are the coordinates (in pixels) of the zoom + * center. + * + * @param x + * @param y + */ + public Matrix zoomIn(float x, float y) { + + Matrix save = new Matrix(); + zoomIn(x, y, save); + return save; + } + + public void zoomIn(float x, float y, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(1.4f, 1.4f, x, y); + } + + /** + * Zooms out by 0.7f, x and y are the coordinates (in pixels) of the zoom + * center. + */ + public Matrix zoomOut(float x, float y) { + + Matrix save = new Matrix(); + zoomOut(x, y, save); + return save; + } + + public void zoomOut(float x, float y, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(0.7f, 0.7f, x, y); + } + + /** + * Zooms out to original size. + * @param outputMatrix + */ + public void resetZoom(Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(1.0f, 1.0f, 0.0f, 0.0f); + } + + /** + * Post-scales by the specified scale factors. + * + * @param scaleX + * @param scaleY + * @return + */ + public Matrix zoom(float scaleX, float scaleY) { + + Matrix save = new Matrix(); + zoom(scaleX, scaleY, save); + return save; + } + + public void zoom(float scaleX, float scaleY, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(scaleX, scaleY); + } + + /** + * Post-scales by the specified scale factors. x and y is pivot. + * + * @param scaleX + * @param scaleY + * @param x + * @param y + * @return + */ + public Matrix zoom(float scaleX, float scaleY, float x, float y) { + + Matrix save = new Matrix(); + zoom(scaleX, scaleY, x, y, save); + return save; + } + + public void zoom(float scaleX, float scaleY, float x, float y, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(scaleX, scaleY, x, y); + } + + /** + * Sets the scale factor to the specified values. + * + * @param scaleX + * @param scaleY + * @return + */ + public Matrix setZoom(float scaleX, float scaleY) { + + Matrix save = new Matrix(); + setZoom(scaleX, scaleY, save); + return save; + } + + public void setZoom(float scaleX, float scaleY, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.setScale(scaleX, scaleY); + } + + /** + * Sets the scale factor to the specified values. x and y is pivot. + * + * @param scaleX + * @param scaleY + * @param x + * @param y + * @return + */ + public Matrix setZoom(float scaleX, float scaleY, float x, float y) { + + Matrix save = new Matrix(); + save.set(mMatrixTouch); + + save.setScale(scaleX, scaleY, x, y); + + return save; + } + + protected float[] valsBufferForFitScreen = new float[9]; + + /** + * Resets all zooming and dragging and makes the chart fit exactly it's + * bounds. + */ + public Matrix fitScreen() { + + Matrix save = new Matrix(); + fitScreen(save); + return save; + } + + /** + * Resets all zooming and dragging and makes the chart fit exactly it's + * bounds. Output Matrix is available for those who wish to cache the object. + */ + public void fitScreen(Matrix outputMatrix) { + mMinScaleX = 1f; + mMinScaleY = 1f; + + outputMatrix.set(mMatrixTouch); + + float[] vals = valsBufferForFitScreen; + for (int i = 0; i < 9; i++) { + vals[i] = 0; + } + + outputMatrix.getValues(vals); + + // reset all translations and scaling + vals[Matrix.MTRANS_X] = 0f; + vals[Matrix.MTRANS_Y] = 0f; + vals[Matrix.MSCALE_X] = 1f; + vals[Matrix.MSCALE_Y] = 1f; + + outputMatrix.setValues(vals); + } + + /** + * Post-translates to the specified points. Less Performant. + * + * @param transformedPts + * @return + */ + public Matrix translate(final float[] transformedPts) { + + Matrix save = new Matrix(); + translate(transformedPts, save); + return save; + } + + /** + * Post-translates to the specified points. Output matrix allows for caching objects. + * + * @param transformedPts + * @return + */ + public void translate(final float[] transformedPts, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + final float x = transformedPts[0] - offsetLeft(); + final float y = transformedPts[1] - offsetTop(); + outputMatrix.postTranslate(-x, -y); + } + + protected Matrix mCenterViewPortMatrixBuffer = new Matrix(); + + /** + * Centers the viewport around the specified position (x-index and y-value) + * in the chart. Centering the viewport outside the bounds of the chart is + * not possible. Makes most sense in combination with the + * setScaleMinima(...) method. + * + * @param transformedPts the position to center view viewport to + * @param view + * @return save + */ + public void centerViewPort(final float[] transformedPts, final View view) { + + Matrix save = mCenterViewPortMatrixBuffer; + save.reset(); + save.set(mMatrixTouch); + + final float x = transformedPts[0] - offsetLeft(); + final float y = transformedPts[1] - offsetTop(); + + save.postTranslate(-x, -y); + + refresh(save, view, true); + } + + /** + * buffer for storing the 9 matrix values of a 3x3 matrix + */ + protected final float[] matrixBuffer = new float[9]; + + /** + * call this method to refresh the graph with a given matrix + * + * @param newMatrix + * @return + */ + public Matrix refresh(Matrix newMatrix, View chart, boolean invalidate) { + + mMatrixTouch.set(newMatrix); + + // make sure scale and translation are within their bounds + limitTransAndScale(mMatrixTouch, mContentRect); + + if (invalidate) + chart.invalidate(); + + newMatrix.set(mMatrixTouch); + return newMatrix; + } + + /** + * limits the maximum scale and X translation of the given matrix + * + * @param matrix + */ + public void limitTransAndScale(Matrix matrix, RectF content) { + + matrix.getValues(matrixBuffer); + + float curTransX = matrixBuffer[Matrix.MTRANS_X]; + float curScaleX = matrixBuffer[Matrix.MSCALE_X]; + + float curTransY = matrixBuffer[Matrix.MTRANS_Y]; + float curScaleY = matrixBuffer[Matrix.MSCALE_Y]; + + // min scale-x is 1f + mScaleX = Math.min(Math.max(mMinScaleX, curScaleX), mMaxScaleX); + + // min scale-y is 1f + mScaleY = Math.min(Math.max(mMinScaleY, curScaleY), mMaxScaleY); + + float width = 0f; + float height = 0f; + + if (content != null) { + width = content.width(); + height = content.height(); + } + + float maxTransX = -width * (mScaleX - 1f); + mTransX = Math.min(Math.max(curTransX, maxTransX - mTransOffsetX), mTransOffsetX); + + float maxTransY = height * (mScaleY - 1f); + mTransY = Math.max(Math.min(curTransY, maxTransY + mTransOffsetY), -mTransOffsetY); + + matrixBuffer[Matrix.MTRANS_X] = mTransX; + matrixBuffer[Matrix.MSCALE_X] = mScaleX; + + matrixBuffer[Matrix.MTRANS_Y] = mTransY; + matrixBuffer[Matrix.MSCALE_Y] = mScaleY; + + matrix.setValues(matrixBuffer); + } + + /** + * Sets the minimum scale factor for the x-axis + * + * @param xScale + */ + public void setMinimumScaleX(float xScale) { + + if (xScale < 1f) + xScale = 1f; + + mMinScaleX = xScale; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Sets the maximum scale factor for the x-axis + * + * @param xScale + */ + public void setMaximumScaleX(float xScale) { + + if (xScale == 0.f) + xScale = Float.MAX_VALUE; + + mMaxScaleX = xScale; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Sets the minimum and maximum scale factors for the x-axis + * + * @param minScaleX + * @param maxScaleX + */ + public void setMinMaxScaleX(float minScaleX, float maxScaleX) { + + if (minScaleX < 1f) + minScaleX = 1f; + + if (maxScaleX == 0.f) + maxScaleX = Float.MAX_VALUE; + + mMinScaleX = minScaleX; + mMaxScaleX = maxScaleX; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Sets the minimum scale factor for the y-axis + * + * @param yScale + */ + public void setMinimumScaleY(float yScale) { + + if (yScale < 1f) + yScale = 1f; + + mMinScaleY = yScale; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Sets the maximum scale factor for the y-axis + * + * @param yScale + */ + public void setMaximumScaleY(float yScale) { + + if (yScale == 0.f) + yScale = Float.MAX_VALUE; + + mMaxScaleY = yScale; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + public void setMinMaxScaleY(float minScaleY, float maxScaleY) { + + if (minScaleY < 1f) + minScaleY = 1f; + + if (maxScaleY == 0.f) + maxScaleY = Float.MAX_VALUE; + + mMinScaleY = minScaleY; + mMaxScaleY = maxScaleY; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Returns the charts-touch matrix used for translation and scale on touch. + * + * @return + */ + public Matrix getMatrixTouch() { + return mMatrixTouch; + } + + /** + * ################ ################ ################ ################ + */ + /** + * BELOW METHODS FOR BOUNDS CHECK + */ + + public boolean isInBoundsX(float x) { + return isInBoundsLeft(x) && isInBoundsRight(x); + } + + public boolean isInBoundsY(float y) { + return isInBoundsTop(y) && isInBoundsBottom(y); + } + + public boolean isInBounds(float x, float y) { + return isInBoundsX(x) && isInBoundsY(y); + } + + public boolean isInBoundsLeft(float x) { + return mContentRect.left <= x + 1; + } + + public boolean isInBoundsRight(float x) { + x = (float) ((int) (x * 100.f)) / 100.f; + return mContentRect.right >= x - 1; + } + + public boolean isInBoundsTop(float y) { + return mContentRect.top <= y; + } + + public boolean isInBoundsBottom(float y) { + y = (float) ((int) (y * 100.f)) / 100.f; + return mContentRect.bottom >= y; + } + + /** + * returns the current x-scale factor + */ + public float getScaleX() { + return mScaleX; + } + + /** + * returns the current y-scale factor + */ + public float getScaleY() { + return mScaleY; + } + + public float getMinScaleX() { + return mMinScaleX; + } + + public float getMaxScaleX() { + return mMaxScaleX; + } + + public float getMinScaleY() { + return mMinScaleY; + } + + public float getMaxScaleY() { + return mMaxScaleY; + } + + /** + * Returns the translation (drag / pan) distance on the x-axis + * + * @return + */ + public float getTransX() { + return mTransX; + } + + /** + * Returns the translation (drag / pan) distance on the y-axis + * + * @return + */ + public float getTransY() { + return mTransY; + } + + /** + * if the chart is fully zoomed out, return true + * + * @return + */ + public boolean isFullyZoomedOut() { + + return isFullyZoomedOutX() && isFullyZoomedOutY(); + } + + /** + * Returns true if the chart is fully zoomed out on it's y-axis (vertical). + * + * @return + */ + public boolean isFullyZoomedOutY() { + return !(mScaleY > mMinScaleY || mMinScaleY > 1f); + } + + /** + * Returns true if the chart is fully zoomed out on it's x-axis + * (horizontal). + * + * @return + */ + public boolean isFullyZoomedOutX() { + return !(mScaleX > mMinScaleX || mMinScaleX > 1f); + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the x-axis. + * + * @param offset + */ + public void setDragOffsetX(float offset) { + mTransOffsetX = Utils.convertDpToPixel(offset); + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the y-axis. + * + * @param offset + */ + public void setDragOffsetY(float offset) { + mTransOffsetY = Utils.convertDpToPixel(offset); + } + + /** + * Returns true if both drag offsets (x and y) are zero or smaller. + * + * @return + */ + public boolean hasNoDragOffset() { + return mTransOffsetX <= 0 && mTransOffsetY <= 0; + } + + /** + * Returns true if the chart is not yet fully zoomed out on the x-axis + * + * @return + */ + public boolean canZoomOutMoreX() { + return mScaleX > mMinScaleX; + } + + /** + * Returns true if the chart is not yet fully zoomed in on the x-axis + * + * @return + */ + public boolean canZoomInMoreX() { + return mScaleX < mMaxScaleX; + } + + /** + * Returns true if the chart is not yet fully zoomed out on the y-axis + * + * @return + */ + public boolean canZoomOutMoreY() { + return mScaleY > mMinScaleY; + } + + /** + * Returns true if the chart is not yet fully zoomed in on the y-axis + * + * @return + */ + public boolean canZoomInMoreY() { + return mScaleY < mMaxScaleY; + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/ApproximatorTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ApproximatorTest.java new file mode 100644 index 0000000..784c290 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ApproximatorTest.java @@ -0,0 +1,43 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.filter.Approximator; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; + +/** + * Created by philipp on 07/06/16. + */ +public class ApproximatorTest { + + @Test + public void testApproximation() { + + float[] points = new float[]{ + 10, 20, + 20, 30, + 25, 25, + 30, 28, + 31, 31, + 33, 33, + 40, 40, + 44, 40, + 48, 23, + 50, 20, + 55, 20, + 60, 25}; + + assertEquals(24, points.length); + + Approximator a = new Approximator(); + + float[] reduced = a.reduceWithDouglasPeucker(points, 2); + + assertEquals(18, reduced.length); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/AxisRendererTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/AxisRendererTest.java new file mode 100644 index 0000000..05cb2f3 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/AxisRendererTest.java @@ -0,0 +1,105 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.renderer.AxisRenderer; +import com.github.mikephil.charting.renderer.YAxisRenderer; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +/** + * Created by philipp on 31/05/16. + */ +public class AxisRendererTest { + + + @Test + public void testComputeAxisValues() { + + YAxis yAxis = new YAxis(); + yAxis.setLabelCount(6); + AxisRenderer renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 100, false); + float[] entries = yAxis.mEntries; + + assertEquals(6, entries.length); + assertEquals(20, entries[1] - entries[0], 0.01); // interval 20 + assertEquals(0, entries[0], 0.01); + assertEquals(100, entries[entries.length - 1], 0.01); + + yAxis = new YAxis(); + yAxis.setLabelCount(6); + yAxis.setGranularity(50f); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 100, false); + entries = yAxis.mEntries; + + assertEquals(3, entries.length); + assertEquals(50, entries[1] - entries[0], 0.01); // interval 50 + assertEquals(0, entries[0], 0.01); + assertEquals(100, entries[entries.length - 1], 0.01); + + yAxis = new YAxis(); + yAxis.setLabelCount(5, true); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 100, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(25, entries[1] - entries[0], 0.01); // interval 25 + assertEquals(0, entries[0], 0.01); + assertEquals(100, entries[entries.length - 1], 0.01); + + yAxis = new YAxis(); + yAxis.setLabelCount(5, true); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 0.01f, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(0.0025, entries[1] - entries[0], 0.0001); + assertEquals(0, entries[0], 0.0001); + assertEquals(0.01, entries[entries.length - 1], 0.0001); + + yAxis = new YAxis(); + yAxis.setLabelCount(5, false); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 0.01f, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(0.0020, entries[1] - entries[0], 0.0001); + assertEquals(0, entries[0], 0.0001); + assertEquals(0.0080, entries[entries.length - 1], 0.0001); + + yAxis = new YAxis(); + yAxis.setLabelCount(6); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(-50, 50, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(-40, entries[0], 0.0001); + assertEquals(0, entries[2], 0.0001); + assertEquals(40, entries[entries.length - 1], 0.0001); + + yAxis = new YAxis(); + yAxis.setLabelCount(6); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(-50, 100, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(-30, entries[0], 0.0001); + assertEquals(30, entries[2], 0.0001); + assertEquals(90, entries[entries.length - 1], 0.0001); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/BarDataTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/BarDataTest.java new file mode 100644 index 0000000..99954bc --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/BarDataTest.java @@ -0,0 +1,72 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; + +/** + * Created by philipp on 06/06/16. + */ +public class BarDataTest { + + @Test + public void testGroupBars() { + + float groupSpace = 5f; + float barSpace = 1f; + + List values1 = new ArrayList<>(); + List values2 = new ArrayList<>(); + + for(int i = 0; i < 5; i++) { + values1.add(new BarEntry(i, 50)); + values2.add(new BarEntry(i, 60)); + } + + BarDataSet barDataSet1 = new BarDataSet(values1, "Set1"); + BarDataSet barDataSet2 = new BarDataSet(values2, "Set2"); + + BarData data = new BarData(barDataSet1, barDataSet2); + data.setBarWidth(10f); + + float groupWidth = data.getGroupWidth(groupSpace, barSpace); + assertEquals(27f, groupWidth, 0.01f); + + assertEquals(0f, values1.get(0).getX(), 0.01f); + assertEquals(1f, values1.get(1).getX(), 0.01f); + + data.groupBars(1000, groupSpace, barSpace); + + // 1000 + 2.5 + 0.5 + 5 + assertEquals(1008f, values1.get(0).getX(), 0.01f); + assertEquals(1019f, values2.get(0).getX(), 0.01f); + assertEquals(1035f, values1.get(1).getX(), 0.01f); + assertEquals(1046f, values2.get(1).getX(), 0.01f); + + data.groupBars(-1000, groupSpace, barSpace); + + assertEquals(-992f, values1.get(0).getX(), 0.01f); + assertEquals(-981f, values2.get(0).getX(), 0.01f); + assertEquals(-965f, values1.get(1).getX(), 0.01f); + assertEquals(-954f, values2.get(1).getX(), 0.01f); + + data.setBarWidth(20f); + groupWidth = data.getGroupWidth(groupSpace, barSpace); + assertEquals(47f, groupWidth, 0.01f); + + data.setBarWidth(10f); + data.groupBars(-20, groupSpace, barSpace); + + assertEquals(-12f, values1.get(0).getX(), 0.01f); + assertEquals(-1f, values2.get(0).getX(), 0.01f); + assertEquals(15f, values1.get(1).getX(), 0.01f); + assertEquals(26f, values2.get(1).getX(), 0.01f); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/ChartDataTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ChartDataTest.java new file mode 100644 index 0000000..ae464ee --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ChartDataTest.java @@ -0,0 +1,211 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.data.ScatterDataSet; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +/** + * Created by philipp on 06/06/16. + */ +public class ChartDataTest { + + @Test + public void testDynamicChartData() { + + List entries1 = new ArrayList(); + entries1.add(new Entry(10, 10)); + entries1.add(new Entry(15, -2)); + entries1.add(new Entry(21, 50)); + + ScatterDataSet set1 = new ScatterDataSet(entries1, ""); + + List entries2 = new ArrayList(); + entries2.add(new Entry(-1, 10)); + entries2.add(new Entry(10, 2)); + entries2.add(new Entry(20, 5)); + + ScatterDataSet set2 = new ScatterDataSet(entries2, ""); + + ScatterData data = new ScatterData(set1, set2); + + assertEquals(-2, data.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(50f, data.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(6, data.getEntryCount()); + + assertEquals(-1f, data.getXMin(), 0.01f); + assertEquals(21f, data.getXMax(), 0.01f); + + assertEquals(-2f, data.getYMin(), 0.01f); + assertEquals(50f, data.getYMax(), 0.01f); + + assertEquals(3, data.getMaxEntryCountSet().getEntryCount()); + + // now add and remove values + data.addEntry(new Entry(-10, -10), 0); + + assertEquals(set1, data.getMaxEntryCountSet()); + assertEquals(4, data.getMaxEntryCountSet().getEntryCount()); + + assertEquals(-10f, data.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(50f, data.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(-10f, data.getXMin(), 0.01f); + assertEquals(21f, data.getXMax(), 0.01f); + + assertEquals(-10f, data.getYMin(), 0.01f); + assertEquals(50f, data.getYMax(), 0.01f); + + data.addEntry(new Entry(-100, 100), 0); + data.addEntry(new Entry(0, -100), 0); + + assertEquals(-100f, data.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(100f, data.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + // right axis will adapt left axis values if no right axis values are present + assertEquals(-100, data.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(100f, data.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + List entries3 = new ArrayList(); + entries3.add(new Entry(0, 200)); + entries3.add(new Entry(0, -50)); + + ScatterDataSet set3 = new ScatterDataSet(entries3, ""); + set3.setAxisDependency(YAxis.AxisDependency.RIGHT); + + data.addDataSet(set3); + + assertEquals(3, data.getDataSetCount()); + + assertEquals(-100f, data.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(100f, data.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(-50f, data.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(200f, data.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + LineData lineData = new LineData(); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertEquals(0, lineData.getDataSetCount()); + + List lineEntries1 = new ArrayList(); + lineEntries1.add(new Entry(10, 90)); + lineEntries1.add(new Entry(1000, 1000)); + + LineDataSet lineSet1 = new LineDataSet(lineEntries1, ""); + + lineData.addDataSet(lineSet1); + + assertEquals(1, lineData.getDataSetCount()); + assertEquals(2, lineSet1.getEntryCount()); + assertEquals(2, lineData.getEntryCount()); + + assertEquals(10, lineData.getXMin(), 0.01f); + assertEquals(1000f, lineData.getXMax(), 0.01f); + + assertEquals(90, lineData.getYMin(), 0.01f); + assertEquals(1000, lineData.getYMax(), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(1000f, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(1000, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + List lineEntries2 = new ArrayList(); + lineEntries2.add(new Entry(-1000, 2000)); + lineEntries2.add(new Entry(2000, -3000)); + + Entry e = new Entry(-1000, 2500); + lineEntries2.add(e); + + LineDataSet lineSet2 = new LineDataSet(lineEntries2, ""); + lineSet2.setAxisDependency(YAxis.AxisDependency.RIGHT); + + lineData.addDataSet(lineSet2); + + assertEquals(2, lineData.getDataSetCount()); + assertEquals(3, lineSet2.getEntryCount()); + assertEquals(5, lineData.getEntryCount()); + + assertEquals(-1000, lineData.getXMin(), 0.01f); + assertEquals(2000, lineData.getXMax(), 0.01f); + + assertEquals(-3000, lineData.getYMin(), 0.01f); + assertEquals(2500, lineData.getYMax(), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(1000f, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(-3000, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(2500, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertTrue(lineData.removeEntry(e, 1)); + + assertEquals(-1000, lineData.getXMin(), 0.01f); + assertEquals(2000, lineData.getXMax(), 0.01f); + + assertEquals(-3000, lineData.getYMin(), 0.01f); + assertEquals(2000, lineData.getYMax(), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(1000f, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(-3000, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(2000, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertEquals(2, lineData.getDataSetCount()); + assertTrue(lineData.removeDataSet(lineSet2)); + assertEquals(1, lineData.getDataSetCount()); + + assertEquals(10, lineData.getXMin(), 0.01f); + assertEquals(1000, lineData.getXMax(), 0.01f); + + assertEquals(90, lineData.getYMin(), 0.01f); + assertEquals(1000, lineData.getYMax(), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(1000f, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(1000, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertTrue(lineData.removeDataSet(lineSet1)); + assertEquals(0, lineData.getDataSetCount()); + + assertEquals(Float.MAX_VALUE, lineData.getXMin(), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getXMax(), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertFalse(lineData.removeDataSet(lineSet1)); + assertFalse(lineData.removeDataSet(lineSet2)); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/DataSetTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/DataSetTest.java new file mode 100644 index 0000000..3be28d2 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/DataSetTest.java @@ -0,0 +1,238 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.ScatterDataSet; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +/** + * Created by philipp on 31/05/16. + */ +public class DataSetTest { + + @Test + public void testCalcMinMax() { + + List entries = new ArrayList(); + entries.add(new Entry(10, 10)); + entries.add(new Entry(15, 2)); + entries.add(new Entry(21, 5)); + + ScatterDataSet set = new ScatterDataSet(entries, ""); + + assertEquals(10f, set.getXMin(), 0.01f); + assertEquals(21f, set.getXMax(), 0.01f); + + assertEquals(2f, set.getYMin(), 0.01f); + assertEquals(10f, set.getYMax(), 0.01f); + + assertEquals(3, set.getEntryCount()); + + set.addEntry(new Entry(25, 1)); + + assertEquals(10f, set.getXMin(), 0.01f); + assertEquals(25f, set.getXMax(), 0.01f); + + assertEquals(1f, set.getYMin(), 0.01f); + assertEquals(10f, set.getYMax(), 0.01f); + + assertEquals(4, set.getEntryCount()); + + set.removeEntry(3); + + assertEquals(10f, set.getXMin(), 0.01f); + assertEquals(21, set.getXMax(), 0.01f); + + assertEquals(2f, set.getYMin(), 0.01f); + assertEquals(10f, set.getYMax(), 0.01f); + } + + @Test + public void testAddRemoveEntry() { + + List entries = new ArrayList(); + entries.add(new Entry(10, 10)); + entries.add(new Entry(15, 2)); + entries.add(new Entry(21, 5)); + + ScatterDataSet set = new ScatterDataSet(entries, ""); + + assertEquals(3, set.getEntryCount()); + + set.addEntryOrdered(new Entry(5, 1)); + + assertEquals(4, set.getEntryCount()); + + assertEquals(5, set.getXMin(), 0.01f); + assertEquals(21, set.getXMax(), 0.01f); + + assertEquals(1f, set.getYMin(), 0.01f); + assertEquals(10f, set.getYMax(), 0.01f); + + assertEquals(5, set.getEntryForIndex(0).getX(), 0.01f); + assertEquals(1, set.getEntryForIndex(0).getY(), 0.01f); + + set.addEntryOrdered(new Entry(20, 50)); + + assertEquals(5, set.getEntryCount()); + + assertEquals(20, set.getEntryForIndex(3).getX(), 0.01f); + assertEquals(50, set.getEntryForIndex(3).getY(), 0.01f); + + assertTrue(set.removeEntry(3)); + + assertEquals(4, set.getEntryCount()); + + assertEquals(21, set.getEntryForIndex(3).getX(), 0.01f); + assertEquals(5, set.getEntryForIndex(3).getY(), 0.01f); + + assertEquals(5, set.getEntryForIndex(0).getX(), 0.01f); + assertEquals(1, set.getEntryForIndex(0).getY(), 0.01f); + + assertTrue(set.removeFirst()); + + assertEquals(3, set.getEntryCount()); + + assertEquals(10, set.getEntryForIndex(0).getX(), 0.01f); + assertEquals(10, set.getEntryForIndex(0).getY(), 0.01f); + + set.addEntryOrdered(new Entry(15, 3)); + + assertEquals(4, set.getEntryCount()); + + assertEquals(15, set.getEntryForIndex(1).getX(), 0.01f); + assertEquals(3, set.getEntryForIndex(1).getY(), 0.01f); + + assertEquals(21, set.getEntryForIndex(3).getX(), 0.01f); + assertEquals(5, set.getEntryForIndex(3).getY(), 0.01f); + + assertTrue(set.removeLast()); + + assertEquals(3, set.getEntryCount()); + + assertEquals(15, set.getEntryForIndex(2).getX(), 0.01f); + assertEquals(2, set.getEntryForIndex(2).getY(), 0.01f); + + assertTrue(set.removeLast()); + + assertEquals(2, set.getEntryCount()); + + assertTrue(set.removeLast()); + + assertEquals(1, set.getEntryCount()); + + assertEquals(10, set.getEntryForIndex(0).getX(), 0.01f); + assertEquals(10, set.getEntryForIndex(0).getY(), 0.01f); + + assertTrue(set.removeLast()); + + assertEquals(0, set.getEntryCount()); + + assertFalse(set.removeLast()); + assertFalse(set.removeFirst()); + } + + @Test + public void testGetEntryForXValue() { + + List entries = new ArrayList(); + entries.add(new Entry(10, 10)); + entries.add(new Entry(15, 5)); + entries.add(new Entry(21, 5)); + + ScatterDataSet set = new ScatterDataSet(entries, ""); + + Entry closest = set.getEntryForXValue(17, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(15, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(17, Float.NaN, DataSet.Rounding.DOWN); + assertEquals(15, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(15, Float.NaN, DataSet.Rounding.DOWN); + assertEquals(15, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(14, Float.NaN, DataSet.Rounding.DOWN); + assertEquals(10, closest.getX(), 0.01f); + assertEquals(10, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(17, Float.NaN, DataSet.Rounding.UP); + assertEquals(21, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(21, Float.NaN, DataSet.Rounding.UP); + assertEquals(21, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(21, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(21, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + } + + @Test + public void testGetEntryForXValueWithDuplicates() { + + // sorted list of values (by x position) + List values = new ArrayList(); + values.add(new Entry(0, 10)); + values.add(new Entry(1, 20)); + values.add(new Entry(2, 30)); + values.add(new Entry(3, 40)); + values.add(new Entry(3, 50)); // duplicate + values.add(new Entry(4, 60)); + values.add(new Entry(4, 70)); // duplicate + values.add(new Entry(5, 80)); + values.add(new Entry(6, 90)); + values.add(new Entry(7, 100)); + values.add(new Entry(8, 110)); + values.add(new Entry(8, 120)); // duplicate + + ScatterDataSet set = new ScatterDataSet(values, ""); + + Entry closest = set.getEntryForXValue(0, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(0, closest.getX(), 0.01f); + assertEquals(10, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(5, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(5, closest.getX(), 0.01f); + assertEquals(80, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(5.4f, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(5, closest.getX(), 0.01f); + assertEquals(80, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(4.6f, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(5, closest.getX(), 0.01f); + assertEquals(80, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(7, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(7, closest.getX(), 0.01f); + assertEquals(100, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(4f, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(4, closest.getX(), 0.01f); + assertEquals(60, closest.getY(), 0.01f); + + List entries = set.getEntriesForXValue(4f); + assertEquals(2, entries.size()); + assertEquals(60, entries.get(0).getY(), 0.01f); + assertEquals(70, entries.get(1).getY(), 0.01f); + + entries = set.getEntriesForXValue(3.5f); + assertEquals(0, entries.size()); + + entries = set.getEntriesForXValue(2f); + assertEquals(1, entries.size()); + assertEquals(30, entries.get(0).getY(), 0.01f); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/LargeValueFormatterTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/LargeValueFormatterTest.java new file mode 100644 index 0000000..f1e1e02 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/LargeValueFormatterTest.java @@ -0,0 +1,95 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.formatter.LargeValueFormatter; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +/** + * Created by philipp on 06/06/16. + */ +public class LargeValueFormatterTest { + + @Test + public void test() { + + LargeValueFormatter formatter = new LargeValueFormatter(); + + String result = formatter.getFormattedValue(5f, null); + assertEquals("5", result); + + result = formatter.getFormattedValue(5.5f, null); + assertEquals("5.5", result); + + result = formatter.getFormattedValue(50f, null); + assertEquals("50", result); + + result = formatter.getFormattedValue(50.5f, null); + assertEquals("50.5", result); + + result = formatter.getFormattedValue(500f, null); + assertEquals("500", result); + + result = formatter.getFormattedValue(1100f, null); + assertEquals("1.1k", result); + + result = formatter.getFormattedValue(10000f, null); + assertEquals("10k", result); + + result = formatter.getFormattedValue(10500f, null); + assertEquals("10.5k", result); + + result = formatter.getFormattedValue(100000f, null); + assertEquals("100k", result); + + result = formatter.getFormattedValue(1000000f, null); + assertEquals("1m", result); + + result = formatter.getFormattedValue(1500000f, null); + assertEquals("1.5m", result); + + result = formatter.getFormattedValue(9500000f, null); + assertEquals("9.5m", result); + + result = formatter.getFormattedValue(22200000f, null); + assertEquals("22.2m", result); + + result = formatter.getFormattedValue(222000000f, null); + assertEquals("222m", result); + + result = formatter.getFormattedValue(1000000000f, null); + assertEquals("1b", result); + + result = formatter.getFormattedValue(9900000000f, null); + assertEquals("9.9b", result); + + result = formatter.getFormattedValue(99000000000f, null); + assertEquals("99b", result); + + result = formatter.getFormattedValue(99500000000f, null); + assertEquals("99.5b", result); + + result = formatter.getFormattedValue(999000000000f, null); + assertEquals("999b", result); + + result = formatter.getFormattedValue(1000000000000f, null); + assertEquals("1t", result); + + formatter.setSuffix(new String[]{"", "k", "m", "b", "t", "q"}); // quadrillion support + result = formatter.getFormattedValue(1000000000000000f, null); + assertEquals("1q", result); + + result = formatter.getFormattedValue(1100000000000000f, null); + assertEquals("1.1q", result); + + result = formatter.getFormattedValue(10000000000000000f, null); + assertEquals("10q", result); + + result = formatter.getFormattedValue(13300000000000000f, null); + assertEquals("13.3q", result); + + result = formatter.getFormattedValue(100000000000000000f, null); + assertEquals("100q", result); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/ObjectPoolTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ObjectPoolTest.java new file mode 100644 index 0000000..e1dbe81 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ObjectPoolTest.java @@ -0,0 +1,240 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.utils.ObjectPool; + +import junit.framework.Assert; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by otheruser on 6/28/16. + */ +public class ObjectPoolTest { + + static class TestPoolable extends ObjectPool.Poolable{ + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(4, new TestPoolable(0,0)); + } + + public int foo = 0; + public int bar = 0; + + protected ObjectPool.Poolable instantiate(){ + return new TestPoolable(0,0); + } + + private TestPoolable(int foo, int bar){ + this.foo = foo; + this.bar = bar; + } + + public static TestPoolable getInstance(int foo, int bar){ + TestPoolable result = pool.get(); + result.foo = foo; + result.bar = bar; + return result; + } + + public static void recycleInstance(TestPoolable instance){ + pool.recycle(instance); + } + + public static void recycleInstances(List instances){ + pool.recycle(instances); + } + + public static ObjectPool getPool(){ + return pool; + } + + } + + @Test + public void testObjectPool(){ + + int poolCapacity = TestPoolable.getPool().getPoolCapacity(); + int poolCount = TestPoolable.getPool().getPoolCount(); + TestPoolable testPoolable; + ArrayList testPoolables = new ArrayList<>(); + + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(4, poolCount); + + testPoolable = TestPoolable.getInstance(6,7); + Assert.assertEquals(6, testPoolable.foo); + Assert.assertEquals(7, testPoolable.bar); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(3, poolCount); + + TestPoolable.recycleInstance(testPoolable); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(4, poolCount); + + + testPoolable = TestPoolable.getInstance(20,30); + Assert.assertEquals(20, testPoolable.foo); + Assert.assertEquals(30, testPoolable.bar); + + TestPoolable.recycleInstance(testPoolable); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(4, poolCount); + + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(1,2)); + testPoolables.add(TestPoolable.getInstance(3,5)); + testPoolables.add(TestPoolable.getInstance(6,8)); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(0, poolCount); + + + TestPoolable.recycleInstances(testPoolables); + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(4, poolCount); + + testPoolables.clear(); + + + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(1,2)); + testPoolables.add(TestPoolable.getInstance(3,5)); + testPoolables.add(TestPoolable.getInstance(6,8)); + testPoolables.add(TestPoolable.getInstance(8,9)); + Assert.assertEquals(12, testPoolables.get(0).foo); + Assert.assertEquals(24, testPoolables.get(0).bar); + Assert.assertEquals(1, testPoolables.get(1).foo); + Assert.assertEquals(2, testPoolables.get(1).bar); + Assert.assertEquals(3, testPoolables.get(2).foo); + Assert.assertEquals(5, testPoolables.get(2).bar); + Assert.assertEquals(6, testPoolables.get(3).foo); + Assert.assertEquals(8, testPoolables.get(3).bar); + Assert.assertEquals(8, testPoolables.get(4).foo); + Assert.assertEquals(9, testPoolables.get(4).bar); + + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(3, poolCount); + + TestPoolable.recycleInstances(testPoolables); + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(8, poolCapacity); + Assert.assertEquals(8, poolCount); + + testPoolables.clear(); + + + testPoolables.add(TestPoolable.getInstance(0,0)); + testPoolables.add(TestPoolable.getInstance(6,8)); + testPoolables.add(TestPoolable.getInstance(1,2)); + testPoolables.add(TestPoolable.getInstance(3,5)); + testPoolables.add(TestPoolable.getInstance(8,9)); + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(6,8)); + testPoolables.add(TestPoolable.getInstance(6,8)); + Assert.assertEquals(0, testPoolables.get(0).foo); + Assert.assertEquals(0, testPoolables.get(0).bar); + Assert.assertEquals(6, testPoolables.get(1).foo); + Assert.assertEquals(8, testPoolables.get(1).bar); + Assert.assertEquals(1, testPoolables.get(2).foo); + Assert.assertEquals(2, testPoolables.get(2).bar); + Assert.assertEquals(3, testPoolables.get(3).foo); + Assert.assertEquals(5, testPoolables.get(3).bar); + Assert.assertEquals(8, testPoolables.get(4).foo); + Assert.assertEquals(9, testPoolables.get(4).bar); + Assert.assertEquals(12, testPoolables.get(5).foo); + Assert.assertEquals(24, testPoolables.get(5).bar); + Assert.assertEquals(12, testPoolables.get(6).foo); + Assert.assertEquals(24, testPoolables.get(6).bar); + Assert.assertEquals(12, testPoolables.get(7).foo); + Assert.assertEquals(24, testPoolables.get(7).bar); + Assert.assertEquals(6, testPoolables.get(8).foo); + Assert.assertEquals(8, testPoolables.get(8).bar); + Assert.assertEquals(6, testPoolables.get(9).foo); + Assert.assertEquals(8, testPoolables.get(9).bar); + + for(TestPoolable p : testPoolables){ + TestPoolable.recycleInstance(p); + } + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(16, poolCount); + + testPoolable = TestPoolable.getInstance(9001,9001); + Assert.assertEquals(9001, testPoolable.foo); + Assert.assertEquals(9001, testPoolable.bar); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(15, poolCount); + + + TestPoolable.recycleInstance(testPoolable); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(16, poolCount); + + Exception e = null; + try{ + // expect an exception. + TestPoolable.recycleInstance(testPoolable); + }catch (Exception ex){ + e = ex; + }finally{ + Assert.assertEquals(e.getMessage(), true, e != null); + } + + testPoolables.clear(); + + TestPoolable.getPool().setReplenishPercentage(0.5f); + int i = 16; + while(i > 0){ + testPoolables.add(TestPoolable.getInstance(0,0)); + i--; + } + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(0, poolCount); + + testPoolables.add(TestPoolable.getInstance(0,0)); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(7, poolCount); + + + } + +} diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..370075e --- /dev/null +++ b/README.en.md @@ -0,0 +1,36 @@ +# 一菜一识别 + +#### Description +{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**} + +#### Software Architecture +Software architecture description + +#### Installation + +1. xxxx +2. xxxx +3. xxxx + +#### Instructions + +1. xxxx +2. xxxx +3. xxxx + +#### Contribution + +1. Fork the repository +2. Create Feat_xxx branch +3. Commit your code +4. Create Pull Request + + +#### Gitee Feature + +1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md +2. Gitee blog [blog.gitee.com](https://blog.gitee.com) +3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) +4. The most valuable open source project [GVP](https://gitee.com/gvp) +5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) +6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..c98c929 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,131 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdk 33 + def app_version_code = 10 + def app_version_name = '1.0' + + defaultConfig { + applicationId "com.sw.ingredient" + minSdk 26 + targetSdk 33 + versionCode app_version_code + versionName app_version_name + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true + manifestPlaceholders = [ + JPUSH_PKGNAME: applicationId, + JPUSH_APPKEY : "e09fd1a00814b34bb15492a8", //JPush 上注册的包名对应的 Appkey. + JPUSH_CHANNEL: "developer-default", //暂时填写默认值即可. + ] + ndk { + abiFilters 'armeabi-v7a' + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + signingConfigs { + debug { + storeFile file("system_3568.keystore") + storePassword "android" + keyAlias "androiddebugkey" + keyPassword "android" + } + } + android.applicationVariants.all { + variant -> + variant.outputs.all { + outputFileName = "app-release${app_version_name}.apk" + } + } + packagingOptions { + pickFirst 'lib/armeabi-v7a/libserial_port.so' +// pickFirst 'lib/arme64-v8a/libserial_port.so' + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) + implementation 'androidx.appcompat:appcompat:1.3.0-alpha02' + + // 引入support支持库的multidex库 + implementation 'com.android.support:multidex:1.0.3' + //或androidx支持库的multidex库 + implementation 'androidx.multidex:multidex:2.0.1' +// implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation project(path: ':MPChartLib') +// implementation project(path: ':jiguang') +// implementation files('libs\\libWeighingscale.jar') + + implementation files('libs\\org.eclipse.paho.client.mqttv3-1.2.5.jar') + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + implementation 'com.github.bumptech.glide:glide:4.12.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' + + implementation 'com.jakewharton:butterknife:10.2.3' + annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3' + + implementation 'com.lzy.net:okgo:3.0.4' + implementation('com.lzy.net:okrx2:2.0.2') + implementation("io.reactivex.rxjava2:rxandroid:2.0.1") + implementation 'com.google.code.gson:gson:2.8.6' + + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + + implementation 'com.youth.banner:banner:2.1.0' + + implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4' + + implementation 'com.scwang.smart:refresh-layout-kernel:2.0.3' //核心必须依赖 + implementation 'com.scwang.smart:refresh-header-classics:2.0.3' //经典刷新头 + + implementation 'com.github.li-xiaojun:XPopup:2.9.19' + + implementation 'com.hyman:flowlayout-lib:1.1.2' +// implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' + + implementation 'com.umeng.umsdk:common:9.4.2'// 必选 + implementation 'com.umeng.umsdk:asms:1.4.1'// 必选 + implementation 'com.umeng.umsdk:apm:1.4.2' + + implementation files('libs/WbIntelligentHardwareSDK-debug.aar') + implementation files('libs/utilcode.jar') + // 串口 +// implementation 'com.licheedev:android-serialport:2.1.2' +// implementation 'com.licheedev:hardwareutils:1.0.0' +// implementation 'com.licheedev:logplus:1.0.0' + // eventbus + implementation 'org.greenrobot:eventbus:3.2.0' + + implementation 'pub.devrel:easypermissions:3.0.0' + + implementation 'com.github.bingoogolapple.BGAQRCode-Android:zxing:1.3.8' + + + def roomVersion = "2.2.5" + implementation("androidx.room:room-runtime:$roomVersion") + annotationProcessor("androidx.room:room-compiler:$roomVersion") +} \ No newline at end of file diff --git a/app/libs/WbIntelligentHardwareSDK-debug.aar b/app/libs/WbIntelligentHardwareSDK-debug.aar new file mode 100644 index 0000000..c25490d Binary files /dev/null and b/app/libs/WbIntelligentHardwareSDK-debug.aar differ diff --git a/app/libs/YNHAPI.jar b/app/libs/YNHAPI.jar new file mode 100644 index 0000000..09859c9 Binary files /dev/null and b/app/libs/YNHAPI.jar differ diff --git a/app/libs/org.eclipse.paho.client.mqttv3-1.2.5.jar b/app/libs/org.eclipse.paho.client.mqttv3-1.2.5.jar new file mode 100644 index 0000000..e602407 Binary files /dev/null and b/app/libs/org.eclipse.paho.client.mqttv3-1.2.5.jar differ diff --git a/app/libs/utilcode.jar b/app/libs/utilcode.jar new file mode 100644 index 0000000..4992673 Binary files /dev/null and b/app/libs/utilcode.jar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/sw/arcdemo/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/sw/arcdemo/ExampleInstrumentedTest.java new file mode 100644 index 0000000..3b46fd3 --- /dev/null +++ b/app/src/androidTest/java/com/sw/arcdemo/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.sw.arcdemo; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.sw.arcdemo", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d4682b5 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/fonts/FZYTK.TTF b/app/src/main/assets/fonts/FZYTK.TTF new file mode 100644 index 0000000..45c8c19 Binary files /dev/null and b/app/src/main/assets/fonts/FZYTK.TTF differ diff --git a/app/src/main/assets/fonts/HarmonyOS_Sans_SC_Black.ttf b/app/src/main/assets/fonts/HarmonyOS_Sans_SC_Black.ttf new file mode 100644 index 0000000..824b3d4 Binary files /dev/null and b/app/src/main/assets/fonts/HarmonyOS_Sans_SC_Black.ttf differ diff --git a/app/src/main/assets/fonts/IMPACT.TTF b/app/src/main/assets/fonts/IMPACT.TTF new file mode 100644 index 0000000..2675688 Binary files /dev/null and b/app/src/main/assets/fonts/IMPACT.TTF differ diff --git a/app/src/main/java/com/sw/st/BootReceiver.java b/app/src/main/java/com/sw/st/BootReceiver.java new file mode 100644 index 0000000..35693b6 --- /dev/null +++ b/app/src/main/java/com/sw/st/BootReceiver.java @@ -0,0 +1,27 @@ +package com.sw.st; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.sw.st.ui.foodinfo.FoodInfoActivity; +import com.sw.st.ui.init.InitActivity; +import com.sw.st.ui.setting.FoodSettingActivity; + +public class BootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { + + Intent newIntent = new Intent(context, FoodSettingActivity.class); // 要启动的Activity + //1.如果自启动APP,参数为需要自动启动的应用包名 + //Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); + //这句话必须加上才能开机自动运行app的界面 + newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + //2.如果自启动Activity + context.startActivity(newIntent); + //3.如果自启动服务 + //context.startService(newIntent); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/MainActivity.java b/app/src/main/java/com/sw/st/MainActivity.java new file mode 100644 index 0000000..0d19254 --- /dev/null +++ b/app/src/main/java/com/sw/st/MainActivity.java @@ -0,0 +1,16 @@ +package com.sw.st; + +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; + +import com.sw.st.R; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/Test.java b/app/src/main/java/com/sw/st/Test.java new file mode 100644 index 0000000..03b5db9 --- /dev/null +++ b/app/src/main/java/com/sw/st/Test.java @@ -0,0 +1,252 @@ +package com.sw.st; + +import android.util.Log; + +import com.google.gson.Gson; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class Test { + public static List getDate(String startTime, String endTime) { + //定义时间格式 + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + List list = new ArrayList<>(); + try { + // 转化成日期类型 + Date startDate = simpleDateFormat.parse(startTime); + Date endDate = simpleDateFormat.parse(endTime); + + //用Calendar 进行日期比较判断 + Calendar calendar = Calendar.getInstance(); + while (startDate.getTime() <= endDate.getTime()) { + // 把日期添加到集合 +// list.add(simpleDateFormat.format(startDate)); + list.add(startDate.getTime()); + // 设置日期 + calendar.setTime(startDate); + //把日期增加一分 + calendar.add(Calendar.MINUTE, 1); + // 获取增加后的日期 + startDate = calendar.getTime(); + } + } catch (ParseException e) { + e.printStackTrace(); + } + return list; + } + + public static String formatLong2StringSDC(long time) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + String date_string = sdf.format(new Date(time)) + ":00"; + return date_string; + } + + public static void main(String[] args) throws Exception { +// System.out.println(formatLong2StringSDC(1677564000000l)); +// +// List betweenDate = getDate("2023-2-28 14:00:00", "2023-3-1 13:59:00"); +// System.out.println(betweenDate.size()); +// +// for (int i = 1; i < betweenDate.size(); i++) { +// System.out.println(betweenDate.get(i - 1) + "=" + betweenDate.get(i)); +// } + + +// String startTime = "2019-05-15 10:00:00"; +// String endTime = "2019-05-15 12:00:00"; +// +// List list = getMinutes(startTime,endTime); +// System.out.println(list.size()); +// for (String time : list) { +// System.out.println(time); +// } +// long startMillis = System.currentTimeMillis() - 1000 * 60 * 60 * 14 * 1; +// long millis = System.currentTimeMillis(); +// for (String s : getBetweenDays(startMillis, millis)) { +// System.out.println(s); +// } + +// for (String s : getTwoDaysSpace("2023-3-15", "2023-3-17")) { +// System.out.println(s); +// } + + } + + /** + * 姓名脱敏 + * + * @param fullName + * @return + */ + public static String desensitizedName(String fullName) { + if (fullName == null || fullName.length() <= 1) { + return fullName; + } + char[] nameArr = fullName.toCharArray(); + if (nameArr.length > 2) { + for (int i = 1; i < nameArr.length - 1; i++) { + nameArr[i] = '*'; + } + } else { + nameArr[1] = '*'; + } + + return new String(nameArr); + } + + + public static float[] stringToArray(String str) { + str = str.replace("[", "").replace("]", ""); + String[] strings = str.split(","); + float[] floatArray = new float[strings.length]; + + for (int i = 1; i < strings.length; i++) { + floatArray[i] = Float.parseFloat(strings[i]); + } + return floatArray; + } + + + /** + * 获取某段时间内每分钟 + * + * @param startTime + * @param endTime + * @return + * @throws Exception + */ + public static List getMinutes(String startTime, String endTime) throws Exception { + List list = new ArrayList<>(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Calendar c1 = Calendar.getInstance(); + c1.setTime(sdf.parse(startTime)); + Calendar c2 = Calendar.getInstance(); + c2.setTime(sdf.parse(endTime)); + while (c1.before(c2)) { + list.add(sdf.format(c1.getTime())); + c1.add(Calendar.MINUTE, 1); + } + return list; + } + + private static List getBetweenDays(long start, long current) { + List dateList = new ArrayList<>(); + // 将毫秒数转换成Instant对象 + Instant startInstant = Instant.ofEpochMilli(start); + Instant currentInstant = Instant.ofEpochMilli(current); + // 将Instant对象转换成LocalDate对象 + LocalDate currentDate = currentInstant.atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate startDate = startInstant.atZone(ZoneId.systemDefault()).toLocalDate(); + + // 计算两个日期之间的天数 + long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate); + + // 输出每一天的日期 + for (int i = 0; i < daysBetween; i++) { + dateList.add(startDate.plusDays(i).toString()); + } + return dateList; + } + + public static List getTwoDaysSpace(String startDate, String endDate) { + List dateList = new ArrayList<>(); + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date dateOne = sdf.parse(startDate); + Date dateTwo = sdf.parse(endDate); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(dateOne); + dateList.add(startDate); + while (dateTwo.after(calendar.getTime())) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + dateList.add(sdf.format(calendar.getTime())); + } + } catch (Exception e) { + e.printStackTrace(); + } + return dateList; + } + + public static String removeDuplicates(String nums) { + ArrayList arrayList = new ArrayList<>(); + for (int i = nums.length() - 1; i > -1; i--) { + int value = Integer.parseInt(String.valueOf(nums.charAt(i))); + arrayList.add(value); + } + ArrayList removeList = new ArrayList<>(); + for (int j = 0; j < 10; j++) { + if (!nums.contains(j + "")) { + continue; + } + int count = 0; + for (int i = 0; i < arrayList.size(); i++) { + int value = arrayList.get(i); + + if (value == j) { + count++; + if (count > 2) { + removeList.add(i); + } + } + + } + } + + Collections.sort(removeList); + int temp = 0; + for (int i : removeList) { +// System.out.println(i + "===" + temp); + arrayList.remove(i + temp); + temp--; + } + + StringBuilder sb = new StringBuilder(); + for (int i = arrayList.size() - 1; i > -1; i--) { + sb.append(arrayList.get(i) + ""); + } + return sb.toString(); + } + + public static String delete(String str) { + // 建立一个 HashMap 来存放每个数字出现的次数 + Map map = new HashMap<>(); + // 创建一个 StringBuilder 来存放最终结果 + StringBuilder sb = new StringBuilder(); + // 遍历字符串中的每个字符 + for (char c : str.toCharArray()) { + // 如果 HashMap 中存在当前字符,则将该字符出现的次数加 1 + if (map.containsKey(c)) { + int count = map.get(c); + // 如果该字符出现的次数大于 2,则跳过 + if (count >= 2) { + continue; + } + map.put(c, count + 1); + } else { + // 如果不存在,则将该字符出现的次数设置为 1 + map.put(c, 1); + } + // 将字符加入 StringBuilder 中 + sb.append(c); + } + // 返回最终结果 + return sb.toString(); + } +} diff --git a/app/src/main/java/com/sw/st/UDPServer.java b/app/src/main/java/com/sw/st/UDPServer.java new file mode 100644 index 0000000..73c6cdd --- /dev/null +++ b/app/src/main/java/com/sw/st/UDPServer.java @@ -0,0 +1,47 @@ +package com.sw.st; + +import java.io.IOException;//导入IOException类 +import java.net.DatagramPacket;//导入DatagramPacket类 +import java.net.DatagramSocket;//导入DatagramSocket类 +import java.net.InetAddress;//导入InetAddress类 +import java.util.Scanner;//导入Scanner类 + +/* + * 服务器端,实现基于UDP的用户登陆 + */ +public class UDPServer {//公共类 + + public static void main(String[] args) throws IOException {//主程序入口 + /* + * 接收客户端发送的数据 + */ + DatagramSocket socket = new DatagramSocket(8800); // 1.创建服务器端DatagramSocket,指定端口 + // 2.创建数据报,用于接收客户端发送的数据 + byte[] data = new byte[1024];//创建字节数组,指定接收的数据包的大小 + DatagramPacket packet = new DatagramPacket(data, data.length); + + // 3.接收客户端发送的数据 + System.out.println("****服务器端已经启动,等待客户端发送数据");//输出提示信息 + while (true) {//通过循环不停的向客户端发送数据和接收数据 + socket.receive(packet);// 此方法在接收到数据报之前会一直阻塞 + // 4.读取数据 + String info = new String(data, 0, packet.getLength());//创建字符串对象 + System.out.println("我是服务器,客户端说:" + info);//输出提示信息 + + /* + * 向客户端响应数据 + */ + // 1.定义客户端的地址、端口号、数据 + InetAddress address = packet.getAddress();//获取发送端的地址 + int port = packet.getPort();//获取 发送端进程所绑定的端口 +// Scanner scanner = new Scanner(System.in);//从键盘接受数据 +// String send = scanner.nextLine();//nextLine方式接受字符串 + String send = "swServer:http://192.168.6.144:8800"; + byte[] data2 = send.getBytes();//将接收到的数据转换为字节数组 + DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port);// 2.创建数据报,包含响应的数据信息 + socket.send(packet2); // 3.响应客户端 + } + + } +} + diff --git a/app/src/main/java/com/sw/st/api/SwService.java b/app/src/main/java/com/sw/st/api/SwService.java new file mode 100644 index 0000000..3ed5078 --- /dev/null +++ b/app/src/main/java/com/sw/st/api/SwService.java @@ -0,0 +1,521 @@ +package com.sw.st.api; + + +import com.google.gson.JsonObject; +import com.lzy.okgo.OkGo; +import com.lzy.okgo.cache.CacheMode; +import com.lzy.okrx2.adapter.ObservableBody; +import com.sw.st.application.App; +import com.sw.st.application.AppConst; +import com.sw.st.model.DeviceMacModel; +import com.sw.st.model.DinnerType; +import com.sw.st.model.FoodGoodsInfo; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.MealRecordsInfo; +import com.sw.st.model.NewFoodInfoModel; +import com.sw.st.model.RecommendValueModel; +import com.sw.st.model.ResponseData; +import com.sw.st.model.UserFaceModel; +import com.sw.st.model.UserInfo; +import com.sw.st.net.helper.JsonConvert; +import com.sw.st.ui.device.BindDeviceModel; +import com.sw.st.ui.setting.FoodListModel; +import com.sw.st.utils.AppUtil; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import io.reactivex.Observable; + +import static com.sw.st.ui.init.InitActivity.BASE_URL; + + +public class SwService { + private static final String BASE_URL = "http://192.168.10.7:9092"; +// private static final String BASE_URL = "http://192.168.1.250:9999/"; +// private static final String BASE_URL = "http://192.168.1.199:9999/"; +// private static final String BASE_URL = "http://vip.shuziweidao.com"; + + public static String getServerAddress = "/equipment/stEquipment/queryByEquipmentCode";//外网服务获取业务服务器地址 + public static final String checkUpdate = "/app/stApp/appVersionPpgrade"; + + public static String getUserFace = "/stapi/cquser/getUserFaceCache";//获取人脸信息 + public static String rfidBindUser = "/zhstapi/zhst/bindUser";//绑定RFID + public static String getUserInfo = "/zhstapi/zhst/getUserInfo";//rfid获取用户信息 + public static String getUserInfoById = "/stapi/cquser/getUserInfo";//获取用户信息 + + public static String getDinnerType = "/zhstapi/zhst/getDinnerType";//实时获取当前餐次 + public static String getRestInfoByRestNo = "/zhstapi/zhst/getRestInfoByRestNo/";//查询食堂信息 + + + public static String createRecord = "/rest/slMealRecordsController/create";//创建就餐记录 + + // public static String getRestInfoFoodsByType = "/zhstapi/zhst/getRestInfoFoodsByType/";//菜品信息 + public static String getUserNutrition = "/zhstapi/zhst/getUserNutrition";//获取用户本餐次信息 + + // public static String getUserFace = + "rest/zhctUserController/getUserFaceCache";//获取人脸信息 + public static String getUserFaceByIds = "/rest/zhctUserController/getUserFaceCacheByIds";//获取人脸信息 + // public static String getUserInfo = + "rest/slBaseController/getUserInfo";//获取用户信息 + public static String bindDeviceMac = "/rest/zhctUserController/bindDeviceMac";//用户id与设备绑定 + public static String getDeviceMac = "/rest/zhctUserController/getDeviceMac";//获取用户id与设备绑定 + public static String getFoodInfoByMac = "/rest/slBaseDeviceController/getFoodInfoByMac";//获取当前设备菜品信息 + public static String getMealRecordsInfo = "/rest/zhctController/getMealRecordsInfo";//获取用户本餐所需能量 + // public static String getDinnerType = + "rest/slBaseController/getDinnerType";//实时获取当前餐次 + public static String getFoodList = "/rest/zhctController/getFoodList";//搜索菜品 + public static String changeDeviceFood = "/rest/slBaseDeviceController/changeDeviceFood";//切换当前设备展示信息 + public static String relRest = "/rest/slBaseDeviceController/relRest";//根据食堂编码和设备MAC绑定信息 + public static String addFoodTotalWeight = "/rest/slMealRecordsController/addFoodTotalWeight";//增加菜品总量 + // public static String getRestInfoByRestNo = + "rest/zhctController/getRestInfoByRestNo";//查询店铺信息 + public static String onLineRenewal = "/rest/slBaseDeviceController/onLineRenewal";//上传在线状态 + + public static String addDeviceInit = "/equipment/stEquipment/addInitialize";//设备初始化 + + //================================新版本======================================= + + public static String getRestInfoFoodsByType = "/shuwei-zhct/scales/getRestInfoFoodsByType";//菜品信息 + public static String getUserNutritionData = "/shuwei-zhct/scales/getFoodInfoByplateNumber";//获取用户本餐次就餐数据 + + public static String startMealService = "/shuwei-zhct/scales/startMealService";//开餐 + public static String userEatFood = "/shuwei-zhct/scales/userEatFood";//取餐 + + public static String getToken = "/shuwei-zhct/scales/generateToken";//获取token + public static String getDeviceInfoByEquipmentId = "/shuwei-zhct/scales/queryByEquipmentId";//查询设备参数 + public static String saveMarginWeight = "/shuwei-zhct/scales/margin/saveMargin";//定时提交菜品重量 + public static String getRecommendValue = "/shuwei-zhct/scales/getRecommended/values";//查询推荐值 + public static String submitTakeFoodState = "/shuwei-zhct/scales/getFoodInfoByplateNumber/state";//提交餐盘信息,解决数据查询为空问题 + public static String getGoodsUseList = "/shuwei-zhct/scales/goodsUseList";//获取菜品食材信息 + public static String saveFoodGoodsUseInfo = "/shuwei-zhct/scales/saveFoodGoodsUseInfo";//提交菜品食材信息 + + + public static Observable>> getUserFace() { + return OkGo.>>get(BASE_URL + getUserFace) +// .params("restNo", restNo) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>>() { + }) + .adapt(new ObservableBody>>()); + } + + public static Observable rfidBindUser(String uid, + String rfid, + String restId) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("userId", uid); + jsonObject.put("deviceCode", rfid); + jsonObject.put("restId", restId); + } catch (JSONException e) { + e.printStackTrace(); + } + + return OkGo.post(BASE_URL + rfidBindUser) + .upJson(jsonObject) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable getUserInfo(String rfid) { + return OkGo.get(BASE_URL + getUserInfo + "/" + rfid) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable getUserNutritionData(String foodId, + String eaId, + String plateNumber) { + return OkGo.get(BASE_URL + getUserNutritionData) + .params("foodId", foodId) + .params("eaId", eaId) + .params("plateNumber", plateNumber) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable submitTakeFoodState( + String eaId, + String plateNumber) { + return OkGo.get(BASE_URL + submitTakeFoodState) + .params("eaId", eaId) + .params("plateNumber", plateNumber) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + + public static Observable getUserInfoById(String uId) { + return OkGo.get(BASE_URL + getUserInfoById + "/" + uId) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + + /** + * @param restId + * @param type 0全部 1餐次 + * @return + */ + public static Observable>> getRestInfoFoodsByType(String restId, String type) { + return OkGo.>>get(BASE_URL + getRestInfoFoodsByType) + .params("eaId", restId) + .params("type", type) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>>() { + }) + .adapt(new ObservableBody>>()); + } + + /** + * 搜索菜品 + * + * @param restId + * @param foodName + * @return + */ + public static Observable>> getRestInfoFoodsByName(String restId, String foodName) { + return OkGo.>>get(BASE_URL + getRestInfoFoodsByType) + .params("eaId", restId) + .params("type", 0) + .params("foodName", foodName) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>>() { + }) + .adapt(new ObservableBody>>()); + } + + /** + * @param foodId + * @return + */ + public static Observable>> getRestInfoFoodsByFoodId(String restId, String foodId) { + return OkGo.>>get(BASE_URL + getRestInfoFoodsByType) + .params("eaId", restId) + .params("foodId", foodId) + .params("type", 0) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>>() { + }) + .adapt(new ObservableBody>>()); + } + + /** + * @param foodId + * @param type 1开餐,2加菜 + * @param foodWeight + * @param restId + * @return + */ + public static Observable startMealService(String foodId, + int type, + double foodWeight, + String restId) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("foodId", foodId); + jsonObject.put("type", type); + jsonObject.put("foodWeight", foodWeight); + jsonObject.put("eaId", restId); + + jsonObject.put("deviceId", AppUtil.getUDID(App.getmContext())); + } catch (JSONException e) { + e.printStackTrace(); + } + + return OkGo.post(BASE_URL + startMealService) + .upJson(jsonObject) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable saveMarginWeight(String foodId, + String eaId, + double marginWeight) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("foodId", foodId); + jsonObject.put("eaId", eaId); + jsonObject.put("marginWeight", marginWeight); + + jsonObject.put("deviceId", AppUtil.getUDID(App.getmContext())); + } catch (JSONException e) { + e.printStackTrace(); + } + + return OkGo.post(BASE_URL + saveMarginWeight) + .upJson(jsonObject) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + + public static Observable userEatFood(String plateNumber, + String foodId, + String eaId, + double eatWeight, + double foodWeight) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("foodId", foodId); + jsonObject.put("plateNumber", plateNumber); + jsonObject.put("eaId", eaId); + jsonObject.put("eatWeight", eatWeight); + jsonObject.put("foodWeight", foodWeight); + + jsonObject.put("deviceId", AppUtil.getUDID(App.getmContext())); + } catch (JSONException e) { + e.printStackTrace(); + } + + return OkGo.post(BASE_URL + userEatFood) + .upJson(jsonObject) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable getUserNutrition(String restId, + String userId) { + + return OkGo.get(BASE_URL + getUserNutrition) + .params("restId", restId) + .params("userId", userId) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + + } + +// public static Observable> getUserInfo(String rfid) { +// return OkGo.>get(getUserInfo + "/" + rfid) +// .cacheMode(CacheMode.NO_CACHE) +// .converter(new JsonConvert>() { +// }) +// .adapt(new ObservableBody>()); +// } + + public static Observable>> getUserFaceByIds(String ids) { + return OkGo.>>post(BASE_URL + getUserFaceByIds + "/" + ids) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>>() { + }) + .adapt(new ObservableBody>>()); + } + + public static Observable> bindDeviceMac(String uid, String deviceMac) { + return OkGo.>post(BASE_URL + bindDeviceMac + "/" + uid + "/" + deviceMac) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>() { + }) + .adapt(new ObservableBody>()); + } + + public static Observable> getDeviceMac(String uid) { + return OkGo.>post(BASE_URL + getDeviceMac + "/" + uid) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>() { + }) + .adapt(new ObservableBody>()); + } + + public static Observable> getFoodInfoByMac(String deviceMac) { + return OkGo.>get(BASE_URL + getFoodInfoByMac + "/" + deviceMac) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>() { + }) + .adapt(new ObservableBody>()); + } + + public static Observable> getMealRecordsInfo(String userId) { + return OkGo.>get(BASE_URL + getMealRecordsInfo + "/" + userId) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>() { + }) + .adapt(new ObservableBody>()); + } + + public static Observable> getDinnerType() { + return OkGo.>get(BASE_URL + getDinnerType) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>() { + }) + .adapt(new ObservableBody>()); + } + + public static Observable> relRest(String deviceMac, String restId, String deviceDesc) { + return OkGo.>post(BASE_URL + relRest + "/" + deviceMac + "/" + restId) + .params("deviceDesc", deviceDesc) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>() { + }) + .adapt(new ObservableBody>()); + } + + public static Observable> createRecord(String userId, String foodId, double intake, double residueWeight) { + return OkGo.>post(BASE_URL + createRecord + "/" + userId + "/" + foodId + "/" + intake + "?residueWeight=" + residueWeight) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>() { + }) + .adapt(new ObservableBody>()); + } + + public static Observable getToken(String devId) { + return OkGo.get(BASE_URL + getToken) + .params("deviceId", devId) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable getBaseToken(String url, String devId) { + return OkGo.get(url + getToken) + .params("deviceId", devId) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable getServerAddress(String url, String devId) { + return OkGo.get(url + getServerAddress) + .params("equipmentCode", devId) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable appCheckUpdate(String versionNo, + String packageName) { + + return OkGo.get(BASE_URL + checkUpdate) + .params("appVersion", versionNo) + .params("appName", packageName) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable addDeviceInit(String devId, String packageName) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("status", 0);//0:不需要,1:需要分配虹软 + jsonObject.put("equipmentName", packageName); + jsonObject.put("equipmentCode", devId); + } catch (JSONException e) { + e.printStackTrace(); + } + + return OkGo.post(BASE_URL + addDeviceInit) + .upJson(jsonObject) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable getDeviceInfoByEquipmentId(String devId) { + return OkGo.get(BASE_URL + getDeviceInfoByEquipmentId) + .params("equipmentCode", devId) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + /** + * @param foodId 菜品ID + * @param type 是否开餐(0开餐,1加餐) + * @param totalWeight 菜品增重 + * @return + */ + public static Observable addFoodTotalWeight(String foodId, int type, double totalWeight, String deviceMac) { + return OkGo.post(BASE_URL + addFoodTotalWeight + "/" + foodId + "/" + type + "/" + totalWeight + "/" + deviceMac) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable getRestInfoByRestNo(String restNo) { + return OkGo.get(BASE_URL + getRestInfoByRestNo + restNo) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable onLineRenewal(String deviceMac) { + return OkGo.get(BASE_URL + onLineRenewal + "/" + deviceMac) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable uploadCrashLog(String url, String data) { + return OkGo.post(url) + .upJson(data) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + + public static Observable> getRecommendValue() { + return OkGo.>get(BASE_URL + getRecommendValue) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert>() { + }) + .adapt(new ObservableBody>()); + } + + public static Observable getGoodsUseList(String foodId, double foodWeight) { + return OkGo.get(BASE_URL + getGoodsUseList) + .params("foodId", foodId) + .params("foodWeight", foodWeight) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + + public static Observable saveFoodGoodsUseInfo(String foodId, double foodWeight, ArrayList goodsInfo) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("foodId", foodId); + jsonObject.put("foodWeight", foodWeight); + jsonObject.put("voList", goodsInfo); + } catch (JSONException e) { + e.printStackTrace(); + } + return OkGo.post(BASE_URL + saveFoodGoodsUseInfo) + .upJson(jsonObject) + .cacheMode(CacheMode.NO_CACHE) + .converter(new JsonConvert() { + }) + .adapt(new ObservableBody()); + } + +} diff --git a/app/src/main/java/com/sw/st/api/WeightApi.java b/app/src/main/java/com/sw/st/api/WeightApi.java new file mode 100644 index 0000000..6f26603 --- /dev/null +++ b/app/src/main/java/com/sw/st/api/WeightApi.java @@ -0,0 +1,152 @@ +//package com.sw.st.api; +// +//import com.xmjjdz.serialportapi.libweighingscale.API; +// +//import java.io.ByteArrayOutputStream; +//import java.io.File; +//import java.io.IOException; +//import java.io.InputStream; +//import java.util.Arrays; +//import java.util.Comparator; +// +//import android_serialport_api.SerialPort; +//import android_serialport_api.SerialPortFinder; +// +//public class WeightApi { +// private SerialPort serialPort; +// private ResponseListener responseListener; +// private ReceiveThread receiveThread; +// +// public WeightApi() { +// } +// +// public String[] getSerialPorts() { +// String[] result = new String[0]; +// +// try { +// SerialPortFinder serialPortFinder = new SerialPortFinder(); +// result = serialPortFinder.getAllDevicesPath(); +// Arrays.sort(result, new Comparator() { +// public int compare(String o1, String o2) { +// return o1.compareTo(o2); +// } +// }); +// } catch (Exception var3) { +// } +// +// return result; +// } +// +// public void openPort(String serialPortDevicePath, int baudrate, ResponseListener responseListener) throws IOException, SecurityException { +// if (this.serialPort != null) { +// this.closePort(); +// } +// +// this.responseListener = responseListener; +// this.serialPort = new SerialPort(new File(serialPortDevicePath), baudrate, 0); +// this.receiveThread = new ReceiveThread(this.serialPort.getInputStream()); +// this.receiveThread.start(); +// } +// +// public void closePort() { +// if (this.serialPort != null) { +// this.receiveThread.terminated = true; +// this.serialPort.close(); +// this.serialPort = null; +// } +// +// } +// +// public void sendCmd(byte[] cmdBytes) throws IOException { +// this.serialPort.getOutputStream().write(cmdBytes); +// } +// +// public void sendCmd(byte[] cmdBytes, int startPos, int length) throws IOException { +// this.serialPort.getOutputStream().write(Arrays.copyOfRange(cmdBytes, startPos, startPos + length)); +// } +// +// private class ReceiveThread extends Thread { +// boolean terminated = false; +// final InputStream inputStream; +// byte[] buffer = new byte[256]; +// +// ReceiveThread(InputStream inputStream) { +// this.inputStream = inputStream; +// } +// +// public void run() { +// super.run(); +// int lenTotalRead = 0; +// byte lenNeed = 2; +// +// long sleepMillis; +// try { +// +// for (; !this.terminated; Thread.sleep(sleepMillis)) { +// int lenAvailable; +// if ((lenAvailable = this.inputStream.available()) > 0) { +// int lenWillRead = Math.min(lenAvailable, lenNeed - lenTotalRead); +// lenTotalRead += this.inputStream.read(this.buffer, lenTotalRead, lenWillRead); +// } +// +// if (lenNeed == lenTotalRead) { +// String bufferString = new String(this.buffer, 0, lenTotalRead); +// if (lenTotalRead == 2) { +// if (!bufferString.equals("OK") && !bufferString.equals("ER")) { +// lenNeed = 50; +// sleepMillis = 5L; +// } else { +// if (WeightApi.this.responseListener != null) { +// WeightApi.this.responseListener.onGetCmdResult(Arrays.copyOf(this.buffer, lenTotalRead)); +// } +// +// lenTotalRead = 0; +// lenNeed = 2; +// sleepMillis = 5L;//50L +// } +// } else { +// if (WeightApi.this.responseListener != null) { +// WeightApi.this.responseListener.onGetWeightInfo(Arrays.copyOf(this.buffer, lenTotalRead)); +// +//// WeightApi.this.responseListener.onGetWeightInfo(readStream(inputStream)); +// } +// +// lenTotalRead = 0; +// lenNeed = 2; +// sleepMillis = 5L;//50L +// } +// } else { +// sleepMillis = lenTotalRead == 0 ? 5L : 5L;//sleepMillis = lenTotalRead == 0 ? 50L : 5L; +// } +// } +// } catch (IOException var7) { +// var7.printStackTrace(); +// this.terminated = true; +// } catch (InterruptedException var8) { +// var8.printStackTrace(); +// this.terminated = true; +// } catch (Exception e) { +// e.printStackTrace(); +// } +// +// } +// } +// +// public interface ResponseListener { +// void onGetCmdResult(byte[] var1); +// +// void onGetWeightInfo(byte[] var1); +// } +// +// public byte[] readStream(InputStream inStream) throws Exception { +// ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); +// byte[] buffer = new byte[1024]; +// int len = -1; +// while ((len = inStream.read(buffer)) != -1) { +// outSteam.write(buffer, 0, len); +// } +// outSteam.close(); +// inStream.close(); +// return outSteam.toByteArray(); +// } +//} diff --git a/app/src/main/java/com/sw/st/application/App.java b/app/src/main/java/com/sw/st/application/App.java new file mode 100644 index 0000000..bdd889e --- /dev/null +++ b/app/src/main/java/com/sw/st/application/App.java @@ -0,0 +1,101 @@ +package com.sw.st.application; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.graphics.Typeface; + +import com.lzy.okgo.OkGo; +import com.lzy.okgo.cache.CacheEntity; +import com.lzy.okgo.cache.CacheMode; +import com.lzy.okgo.cookie.CookieJarImpl; +import com.lzy.okgo.cookie.store.SPCookieStore; +import com.lzy.okgo.interceptor.HttpLoggingInterceptor; +import com.sw.st.utils.CrashHandler; + +import java.lang.reflect.Field; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; + +import okhttp3.OkHttpClient; + +public class App extends Application { + + public static List activities = new LinkedList<>(); + + private static Context mContext; + + @Override + public void onCreate() { + super.onCreate(); + mContext = this.getApplicationContext(); + + initOkGo(); +// activeEngine(); + +// JPushInterface.setDebugMode(true); +// JPushInterface.init(this); + +// //设置LOG开关,默认为false +// UMConfigure.setLogEnabled(true); +// +// //友盟预初始化 +// UMConfigure.preInit(getApplicationContext(), "611e20f11fee2e303c2a51e4", "Umeng"); +// +// UMConfigure.init(getApplicationContext(), "611e20f11fee2e303c2a51e4", "Umeng", UMConfigure.DEVICE_TYPE_PHONE, ""); +// // 选用AUTO页面采集模式 +// MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO); + + + // 测试默认打开红外灯,否者个别红外摄像头没画面 +// YNHAPI.init(getApplicationContext()); +// YNHAPI.setLightState(YNHAPI.Light.Light_InfraredLed, true); + + +// LogcatHelper.getInstance(this).start(); + CrashHandler.getInstance().init(mContext); + } + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); +// MultiDex.install(this); + } + + /** + * 初始化okgo + */ + private void initOkGo() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + //使用sp保持cookie,如果cookie不过期,则一直有效 + builder.cookieJar(new CookieJarImpl(new SPCookieStore(this))); + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor("OkGo"); + loggingInterceptor.setPrintLevel(HttpLoggingInterceptor.Level.BODY); + //log颜色级别,决定了log在控制台显示的颜色 + loggingInterceptor.setColorLevel(Level.INFO); + builder.addInterceptor(loggingInterceptor); +// builder.addInterceptor(new TokenInterceptor()); + + OkGo.getInstance() + .init(this) + .setOkHttpClient(builder.build()) //设置OkHttpClient,不设置将使用默认的 + .setCacheMode(CacheMode.NO_CACHE) + .setCacheTime(CacheEntity.CACHE_NEVER_EXPIRE) + .setRetryCount(0); + } + + public static Context getmContext() { + return mContext; + } + + /** + * 退出程序 + */ + public static void exit() { + for (Activity activity : activities) { + activity.finish(); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/application/AppConst.java b/app/src/main/java/com/sw/st/application/AppConst.java new file mode 100644 index 0000000..2ef78c8 --- /dev/null +++ b/app/src/main/java/com/sw/st/application/AppConst.java @@ -0,0 +1,20 @@ +package com.sw.st.application; + + +import android.os.Environment; + +public class AppConst { + + public static final String BASE_FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/sw"; + + public static final String BASE_IMG_URL = "https://zhst.rk-health.com/systemController/showOrDownByurlFTP.do?dbPath="; + public static final String WEB_BASE_URL = "http://47.105.49.67/"; + // public static final String WEB_BASE_URL = "http://192.168.6.76:8080/"; + public static final String BASE_URL = "http://zhst.rk-health.com/"; + // public static final String BASE_URL = "http://192.168.31.36:8089/zhst/"; + public static final String SOCKET_SERVICE_URL = "ws://192.168.5.110:22211"; + + public static final String ARCSOFT_APP_ID = "J6jt8Lgou3cTW9Y1k9T8Zx4nP51ZgcHRv668znCcUu5g"; + public static final String ARCSOFT_SDK_KEY = "8necG4J6MQeTnz4gvcZuaRUywJynZindJCt2geuBnYv9"; + +} diff --git a/app/src/main/java/com/sw/st/base/BaseActivity.java b/app/src/main/java/com/sw/st/base/BaseActivity.java new file mode 100644 index 0000000..7d71552 --- /dev/null +++ b/app/src/main/java/com/sw/st/base/BaseActivity.java @@ -0,0 +1,217 @@ +package com.sw.st.base; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.TextView; +import android.widget.Toast; + +import com.sw.st.R; +import com.sw.st.application.App; +import com.sw.st.view.CustomDialog; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import butterknife.ButterKnife; + + +public abstract class BaseActivity> extends AppCompatActivity { + + protected T mPresenter; + private CustomDialog mDialogWaiting; + + protected Context mContext; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + App.activities.add(this); + mContext = this; + init(); + + //判断是否使用MVP模式 + mPresenter = createPresenter(); + if (mPresenter != null) { + mPresenter.attachView((V) this);//因为之后所有的子类都要实现对应的View接口 + } + + //子类不再需要设置布局ID,也不再需要使用ButterKnife.bind() + setContentView(provideContentViewId()); + ButterKnife.bind(this); + +// excuteStatesBar(); + + initData(); + initView(); + initListener(); + } + + /** + * 解决4.4设置状态栏颜色之后,布局内容嵌入状态栏位置问题 + */ + private void excuteStatesBar() { + ViewGroup mContentView = (ViewGroup) getWindow().findViewById(Window.ID_ANDROID_CONTENT); + View mChildView = mContentView.getChildAt(0); + if (mChildView != null) { + //注意不是设置 ContentView 的 FitsSystemWindows, + // 而是设置 ContentView 的第一个子 View ,预留出系统 View 的空间. + mChildView.setFitsSystemWindows(true); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mPresenter != null) { + mPresenter.detachView(); + } + + } + + //在setContentView()调用之前调用,可以设置WindowFeature(如:this.requestWindowFeature(Window.FEATURE_NO_TITLE);) + public void init() { + } + + //得到当前界面的布局文件id(由子类实现) + protected abstract int provideContentViewId(); + + public abstract void initView(); + + public abstract void initData(); + + public abstract void initListener(); + + //用于创建Presenter和判断是否使用MVP模式(由子类实现) + protected abstract T createPresenter(); + + + /** + * 显示等待提示框 + */ + public Dialog showWaitingDialog(String tip) { + hideWaitingDialog(); + View view = View.inflate(this, R.layout.dialog_waiting, null); + if (!TextUtils.isEmpty(tip)) + ((TextView) view.findViewById(R.id.tvTip)).setText(tip); + mDialogWaiting = new CustomDialog(this, view, R.style.MyDialog); + mDialogWaiting.show(); + mDialogWaiting.setCancelable(true); + return mDialogWaiting; + } + + /** + * 隐藏等待提示框 + */ + public void hideWaitingDialog() { + if (mDialogWaiting != null) { + mDialogWaiting.dismiss(); + mDialogWaiting = null; + } + } + + + /** + * 权限检查 + * + * @param neededPermissions 需要的权限 + * @return 是否全部被允许 + */ + protected boolean checkPermissions(String[] neededPermissions) { + if (neededPermissions == null || neededPermissions.length == 0) { + return true; + } + boolean allGranted = true; + for (String neededPermission : neededPermissions) { + allGranted &= ContextCompat.checkSelfPermission(this, neededPermission) == PackageManager.PERMISSION_GRANTED; + } + return allGranted; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + boolean isAllGranted = true; + for (int grantResult : grantResults) { + isAllGranted &= (grantResult == PackageManager.PERMISSION_GRANTED); + } + afterRequestPermission(requestCode, isAllGranted); + } + + /** + * 请求权限的回调 + * + * @param requestCode 请求码 + * @param isAllGranted 是否全部被同意 + */ + protected abstract void afterRequestPermission(int requestCode, boolean isAllGranted); + + protected void showToast(String s) { + Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT).show(); + } + + protected void showLongToast(String s) { + Toast.makeText(getApplicationContext(), s, Toast.LENGTH_LONG).show(); + } + + + /** + * 系统弹框提示 + * + * @param s + */ + protected void showTipsDialog(String s) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + + builder.setTitle("提示"); + builder.setMessage(s); + builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { +// Intent wifiSettingsIntent = new Intent("android.settings.WIFI_SETTINGS"); +// startActivity(wifiSettingsIntent); + } + }); + + builder.show(); + } + + + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + hideStatusBar(); + } + + private void hideStatusBar() { + if (Build.VERSION.SDK_INT < 16) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } else { + View decorView = getWindow().getDecorView(); +// int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN; +// decorView.setSystemUiVisibility(uiOptions); + + int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + //布局位于状态栏下方 + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + //全屏 + View.SYSTEM_UI_FLAG_FULLSCREEN | + //隐藏导航栏 + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + uiOptions |= 0x00001000; + decorView.setSystemUiVisibility(uiOptions); + } + } +} diff --git a/app/src/main/java/com/sw/st/base/BaseFragment.java b/app/src/main/java/com/sw/st/base/BaseFragment.java new file mode 100644 index 0000000..7e4f56b --- /dev/null +++ b/app/src/main/java/com/sw/st/base/BaseFragment.java @@ -0,0 +1,74 @@ +package com.sw.st.base; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import butterknife.ButterKnife; + +public abstract class BaseFragment> extends Fragment { + + protected T mPresenter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + init(); + + //判断是否使用MVP模式 + mPresenter = createPresenter(); + if (mPresenter != null) { + mPresenter.attachView((V) this);//因为之后所有的子类都要实现对应的View接口 + } + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View rootView = inflater.inflate(provideContentViewId(), container, false); + ButterKnife.bind(this, rootView); + initView(rootView); + return rootView; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + initData(); + initListener(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mPresenter != null) { + mPresenter.detachView(); + } + } + + public void init() { + + } + + public void initView(View rootView) { + } + + public void initData() { + + } + + public void initListener() { + + } + + + //用于创建Presenter和判断是否使用MVP模式(由子类实现) + protected abstract T createPresenter(); + + //得到当前界面的布局文件id(由子类实现) + protected abstract int provideContentViewId(); +} diff --git a/app/src/main/java/com/sw/st/base/BasePresenter.java b/app/src/main/java/com/sw/st/base/BasePresenter.java new file mode 100644 index 0000000..4de69fa --- /dev/null +++ b/app/src/main/java/com/sw/st/base/BasePresenter.java @@ -0,0 +1,35 @@ +package com.sw.st.base; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; + + +public abstract class BasePresenter { + + protected Reference mViewRef; + + + public void attachView(V view) { + mViewRef = new WeakReference(view); + } + + protected V getView() { + if (mViewRef != null) { + return mViewRef.get(); + } else { + return null; + } + } + + public boolean isViewAttached() { + return mViewRef != null && mViewRef.get() != null; + } + + public void detachView() { + if (mViewRef != null) { + mViewRef.clear(); + mViewRef = null; + } + } + +} diff --git a/app/src/main/java/com/sw/st/base/BaseView.java b/app/src/main/java/com/sw/st/base/BaseView.java new file mode 100644 index 0000000..1420d77 --- /dev/null +++ b/app/src/main/java/com/sw/st/base/BaseView.java @@ -0,0 +1,10 @@ +package com.sw.st.base; + +public interface BaseView { + + void showProgress(String tipString); + + void hideProgress(); + + +} diff --git a/app/src/main/java/com/sw/st/model/DeviceMacModel.java b/app/src/main/java/com/sw/st/model/DeviceMacModel.java new file mode 100644 index 0000000..29bd44a --- /dev/null +++ b/app/src/main/java/com/sw/st/model/DeviceMacModel.java @@ -0,0 +1,13 @@ +package com.sw.st.model; + +public class DeviceMacModel { + private String deviceMac; + + public String getDeviceMac() { + return deviceMac; + } + + public void setDeviceMac(String deviceMac) { + this.deviceMac = deviceMac; + } +} diff --git a/app/src/main/java/com/sw/st/model/DinnerModel.java b/app/src/main/java/com/sw/st/model/DinnerModel.java new file mode 100644 index 0000000..ad6b69a --- /dev/null +++ b/app/src/main/java/com/sw/st/model/DinnerModel.java @@ -0,0 +1,27 @@ +package com.sw.st.model; + +public class DinnerModel { + private float dinnerMin; + private float dinnerMax; + + public DinnerModel(float dinnerMin, float dinnerMax) { + this.dinnerMin = dinnerMin; + this.dinnerMax = dinnerMax; + } + + public float getDinnerMin() { + return dinnerMin; + } + + public void setDinnerMin(float dinnerMin) { + this.dinnerMin = dinnerMin; + } + + public float getDinnerMax() { + return dinnerMax; + } + + public void setDinnerMax(float dinnerMax) { + this.dinnerMax = dinnerMax; + } +} diff --git a/app/src/main/java/com/sw/st/model/DinnerType.java b/app/src/main/java/com/sw/st/model/DinnerType.java new file mode 100644 index 0000000..23484f9 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/DinnerType.java @@ -0,0 +1,13 @@ +package com.sw.st.model; + +public class DinnerType { + private int dinnerType; + + public int getDinnerType() { + return dinnerType; + } + + public void setDinnerType(int dinnerType) { + this.dinnerType = dinnerType; + } +} diff --git a/app/src/main/java/com/sw/st/model/FoodGoodsInfo.java b/app/src/main/java/com/sw/st/model/FoodGoodsInfo.java new file mode 100644 index 0000000..25fb5a9 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/FoodGoodsInfo.java @@ -0,0 +1,67 @@ +package com.sw.st.model; + +public class FoodGoodsInfo { + private String foodId; + private String foodName; + private double foodWeight; + private String goodsId; + private String goodsName; + private double useWeight; + private double useRealWeight; + + public String getFoodId() { + return foodId; + } + + public void setFoodId(String foodId) { + this.foodId = foodId; + } + + public String getFoodName() { + return foodName; + } + + public void setFoodName(String foodName) { + this.foodName = foodName; + } + + public double getFoodWeight() { + return foodWeight; + } + + public void setFoodWeight(double foodWeight) { + this.foodWeight = foodWeight; + } + + public String getGoodsId() { + return goodsId; + } + + public void setGoodsId(String goodsId) { + this.goodsId = goodsId; + } + + public String getGoodsName() { + return goodsName; + } + + public void setGoodsName(String goodsName) { + this.goodsName = goodsName; + } + + public double getUseWeight() { + return useWeight; + } + + public void setUseWeight(double useWeight) { + this.useWeight = useWeight; + } + + public double getUseRealWeight() { + return useRealWeight; + } + + public void setUseRealWeight(double useRealWeight) { + this.useRealWeight = useRealWeight; + } +} diff --git a/app/src/main/java/com/sw/st/model/FoodInfoModel.java b/app/src/main/java/com/sw/st/model/FoodInfoModel.java new file mode 100644 index 0000000..85b326e --- /dev/null +++ b/app/src/main/java/com/sw/st/model/FoodInfoModel.java @@ -0,0 +1,235 @@ +package com.sw.st.model; + +import java.io.Serializable; + +public class FoodInfoModel implements Serializable { + private String id; + private String foodName; + private String imgUrl; + private String foodLabel; + private stFoodInfoSetting stFoodInfoSetting; + + private stFoodInfoMaterial stFoodInfoMaterial; + + private stFoodInfoPagodaAPPVO stFoodInfoPagodaAPPVO; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFoodName() { + return foodName; + } + + public void setFoodName(String foodName) { + this.foodName = foodName; + } + + public String getImgUrl() { + return imgUrl; + } + + public void setImgUrl(String imgUrl) { + this.imgUrl = imgUrl; + } + + public void setFoodLabel(String foodLabel) { + this.foodLabel = foodLabel; + } + + public String getFoodLabel() { + return foodLabel; + } + + public FoodInfoModel.stFoodInfoSetting getStFoodInfoSetting() { + return stFoodInfoSetting; + } + + public void setStFoodInfoSetting(FoodInfoModel.stFoodInfoSetting stFoodInfoSetting) { + this.stFoodInfoSetting = stFoodInfoSetting; + } + + public FoodInfoModel.stFoodInfoMaterial getStFoodInfoMaterial() { + return stFoodInfoMaterial; + } + + public void setStFoodInfoMaterial(FoodInfoModel.stFoodInfoMaterial stFoodInfoMaterial) { + this.stFoodInfoMaterial = stFoodInfoMaterial; + } + + public FoodInfoModel.stFoodInfoPagodaAPPVO getStFoodInfoPagodaAPPVO() { + return stFoodInfoPagodaAPPVO; + } + + public void setStFoodInfoPagodaAPPVO(FoodInfoModel.stFoodInfoPagodaAPPVO stFoodInfoPagodaAPPVO) { + this.stFoodInfoPagodaAPPVO = stFoodInfoPagodaAPPVO; + } + + public class stFoodInfoMaterial implements Serializable { + private float energyKcal;//热量 + private float fat;//脂肪 + private float protein;//蛋白质 + private float cho;//碳水化合物 + private float na;//钠 + private float sugar;//糖 + + public float getEnergyKcal() { + return energyKcal; + } + + public void setEnergyKcal(float energyKcal) { + this.energyKcal = energyKcal; + } + + public float getFat() { + return fat; + } + + public void setFat(float fat) { + this.fat = fat; + } + + public float getProtein() { + return protein; + } + + public void setProtein(float protein) { + this.protein = protein; + } + + public float getCho() { + return cho; + } + + public void setCho(float cho) { + this.cho = cho; + } + + public float getNa() { + return na; + } + + public void setNa(float na) { + this.na = na; + } + + public float getSugar() { + return sugar; + } + + public void setSugar(float sugar) { + this.sugar = sugar; + } + } + + public class stFoodInfoPagodaAPPVO implements Serializable { + private double oil; + private double salt; + private double meat; + private double vegetable; + private double fruits; + private double grain; + private double soya; + private double sugar; + + public double getOil() { + return oil; + } + + public void setOil(double oil) { + this.oil = oil; + } + + public double getSalt() { + return salt; + } + + public void setSalt(double salt) { + this.salt = salt; + } + + public double getMeat() { + return meat; + } + + public void setMeat(double meat) { + this.meat = meat; + } + + public double getVegetable() { + return vegetable; + } + + public void setVegetable(double vegetable) { + this.vegetable = vegetable; + } + + public double getFruits() { + return fruits; + } + + public void setFruits(double fruits) { + this.fruits = fruits; + } + + public double getGrain() { + return grain; + } + + public void setGrain(double grain) { + this.grain = grain; + } + + public double getSoya() { + return soya; + } + + public void setSoya(double soya) { + this.soya = soya; + } + + public double getSugar() { + return sugar; + } + + public void setSugar(double sugar) { + this.sugar = sugar; + } + } + + + public class stFoodInfoSetting { + private int addFoodWeight; + private boolean tablewareStatus; + private int tablewareWeight; + + public int getAddFoodWeight() { + return addFoodWeight; + } + + public void setAddFoodWeight(int addFoodWeight) { + this.addFoodWeight = addFoodWeight; + } + + public boolean isTablewareStatus() { + return tablewareStatus; + } + + public void setTablewareStatus(boolean tablewareStatus) { + this.tablewareStatus = tablewareStatus; + } + + public int getTablewareWeight() { + return tablewareWeight; + } + + public void setTablewareWeight(int tablewareWeight) { + this.tablewareWeight = tablewareWeight; + } + } +} + diff --git a/app/src/main/java/com/sw/st/model/MealRecordsInfo.java b/app/src/main/java/com/sw/st/model/MealRecordsInfo.java new file mode 100644 index 0000000..c74ac43 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/MealRecordsInfo.java @@ -0,0 +1,277 @@ +package com.sw.st.model; + +import java.io.Serializable; + +public class MealRecordsInfo implements Serializable { + + private NeedEnergyVo needEnergyVo; + private foodExtInfoVo foodExtInfoVo; + private float totalWeight; + + public void setNeedEnergyVo(NeedEnergyVo needEnergyVo) { + this.needEnergyVo = needEnergyVo; + } + + public NeedEnergyVo getNeedEnergyVo() { + return needEnergyVo; + } + + public MealRecordsInfo.foodExtInfoVo getFoodExtInfoVo() { + return foodExtInfoVo; + } + + public void setFoodExtInfoVo(MealRecordsInfo.foodExtInfoVo foodExtInfoVo) { + this.foodExtInfoVo = foodExtInfoVo; + } + + public float getTotalWeight() { + return totalWeight; + } + + public void setTotalWeight(float totalWeight) { + this.totalWeight = totalWeight; + } + + public class foodExtInfoVo implements Serializable { + private float energyKcal; + private float protein; + private float fat; + private float cho; + + public float getEnergyKcal() { + return energyKcal; + } + + public void setEnergyKcal(float energyKcal) { + this.energyKcal = energyKcal; + } + + public float getProtein() { + return protein; + } + + public void setProtein(float protein) { + this.protein = protein; + } + + public float getFat() { + return fat; + } + + public void setFat(float fat) { + this.fat = fat; + } + + public float getCho() { + return cho; + } + + public void setCho(float cho) { + this.cho = cho; + } + } + + + public class NeedEnergyVo implements Serializable { + + private double bmi; + private int energy; + private String min; + private String max; + private float dinner1Min; + private float dinner1Max; + private float dinner2Min; + private float dinner2Max; + private float dinner3Min; + private float dinner3Max; + private double proteinMin; + private double proteinMax; + private double fatMin; + private double fatMax; + private double choMin; + private double choMax; + private double proteinMinKcal; + private double proteinMaxKcal; + private double fatMinKcal; + private double fatMaxKcal; + private double choMinKcal; + private double choMaxKcal; + + public void setBmi(double bmi) { + this.bmi = bmi; + } + + public double getBmi() { + return bmi; + } + + public void setEnergy(int energy) { + this.energy = energy; + } + + public int getEnergy() { + return energy; + } + + public void setMin(String min) { + this.min = min; + } + + public String getMin() { + return min; + } + + public void setMax(String max) { + this.max = max; + } + + public String getMax() { + return max; + } + + public void setDinner1Min(float dinner1Min) { + this.dinner1Min = dinner1Min; + } + + public float getDinner1Min() { + return dinner1Min; + } + + public void setDinner1Max(float dinner1Max) { + this.dinner1Max = dinner1Max; + } + + public float getDinner1Max() { + return dinner1Max; + } + + public void setDinner2Min(float dinner2Min) { + this.dinner2Min = dinner2Min; + } + + public float getDinner2Min() { + return dinner2Min; + } + + public void setDinner2Max(float dinner2Max) { + this.dinner2Max = dinner2Max; + } + + public float getDinner2Max() { + return dinner2Max; + } + + public void setDinner3Min(float dinner3Min) { + this.dinner3Min = dinner3Min; + } + + public float getDinner3Min() { + return dinner3Min; + } + + public void setDinner3Max(float dinner3Max) { + this.dinner3Max = dinner3Max; + } + + public float getDinner3Max() { + return dinner3Max; + } + + public void setProteinMin(double proteinMin) { + this.proteinMin = proteinMin; + } + + public double getProteinMin() { + return proteinMin; + } + + public void setProteinMax(double proteinMax) { + this.proteinMax = proteinMax; + } + + public double getProteinMax() { + return proteinMax; + } + + public void setFatMin(double fatMin) { + this.fatMin = fatMin; + } + + public double getFatMin() { + return fatMin; + } + + public void setFatMax(double fatMax) { + this.fatMax = fatMax; + } + + public double getFatMax() { + return fatMax; + } + + public void setChoMin(double choMin) { + this.choMin = choMin; + } + + public double getChoMin() { + return choMin; + } + + public void setChoMax(double choMax) { + this.choMax = choMax; + } + + public double getChoMax() { + return choMax; + } + + public void setProteinMinKcal(double proteinMinKcal) { + this.proteinMinKcal = proteinMinKcal; + } + + public double getProteinMinKcal() { + return proteinMinKcal; + } + + public void setProteinMaxKcal(double proteinMaxKcal) { + this.proteinMaxKcal = proteinMaxKcal; + } + + public double getProteinMaxKcal() { + return proteinMaxKcal; + } + + public void setFatMinKcal(double fatMinKcal) { + this.fatMinKcal = fatMinKcal; + } + + public double getFatMinKcal() { + return fatMinKcal; + } + + public void setFatMaxKcal(double fatMaxKcal) { + this.fatMaxKcal = fatMaxKcal; + } + + public double getFatMaxKcal() { + return fatMaxKcal; + } + + public void setChoMinKcal(double choMinKcal) { + this.choMinKcal = choMinKcal; + } + + public double getChoMinKcal() { + return choMinKcal; + } + + public void setChoMaxKcal(double choMaxKcal) { + this.choMaxKcal = choMaxKcal; + } + + public double getChoMaxKcal() { + return choMaxKcal; + } + + } +} diff --git a/app/src/main/java/com/sw/st/model/MessageEvent.java b/app/src/main/java/com/sw/st/model/MessageEvent.java new file mode 100644 index 0000000..1556705 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/MessageEvent.java @@ -0,0 +1,44 @@ +package com.sw.st.model; + +import java.io.Serializable; + +public class MessageEvent implements Serializable { + + public static final int MESSAGE_UPDATE_FOOD = 1; + public static final int MESSAGE_EVENT_ZERO = 2;//完成清零 + public static final int MESSAGE_WEIGHT_INFO = 3;//重量信息 + + + private int type; + private int weight; + private String message; + + public MessageEvent(int type, String message) { + this.type = type; + this.message = message; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/app/src/main/java/com/sw/st/model/NewFoodInfoModel.java b/app/src/main/java/com/sw/st/model/NewFoodInfoModel.java new file mode 100644 index 0000000..449de19 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/NewFoodInfoModel.java @@ -0,0 +1,217 @@ +package com.sw.st.model; + +import java.io.Serializable; +import java.util.List; + +public class NewFoodInfoModel implements Serializable { + private String id; + private String foodName; + private String img; + private List labList; + private double price; + private double vipPrice; + private int priceUnit; + private List diseaseList; + private float energyKcal; + private double cho; + private double fat; + private double protein; + private String mainIngredientList; + private String assistIngredientList; + private String seasoningList; + + private int tablewareWeight; + + private double meal; + private double vegetable; + private double fruits; + private double grain; + private double mixedBeans; + private double soyaNew; + + private boolean isSelect = false; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getTablewareWeight() { + return tablewareWeight; + } + + public void setTablewareWeight(int tablewareWeight) { + this.tablewareWeight = tablewareWeight; + } + + public String getFoodName() { + return foodName; + } + + public void setFoodName(String foodName) { + this.foodName = foodName; + } + + public String getImg() { + return img; + } + + public void setImg(String img) { + this.img = img; + } + + public List getLabList() { + return labList; + } + + public void setLabList(List labList) { + this.labList = labList; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public double getVipPrice() { + return vipPrice; + } + + public void setVipPrice(double vipPrice) { + this.vipPrice = vipPrice; + } + + public int getPriceUnit() { + return priceUnit; + } + + public void setPriceUnit(int priceUnit) { + this.priceUnit = priceUnit; + } + + public List getDiseaseList() { + return diseaseList; + } + + public void setDiseaseList(List diseaseList) { + this.diseaseList = diseaseList; + } + + public float getEnergyKcal() { + return energyKcal; + } + + public void setEnergyKcal(float energyKcal) { + this.energyKcal = energyKcal; + } + + public double getCho() { + return cho; + } + + public void setCho(double cho) { + this.cho = cho; + } + + public double getFat() { + return fat; + } + + public void setFat(double fat) { + this.fat = fat; + } + + public double getProtein() { + return protein; + } + + public void setProtein(double protein) { + this.protein = protein; + } + + public String getMainIngredientList() { + return mainIngredientList; + } + + public void setMainIngredientList(String mainIngredientList) { + this.mainIngredientList = mainIngredientList; + } + + public String getAssistIngredientList() { + return assistIngredientList; + } + + public void setAssistIngredientList(String assistIngredientList) { + this.assistIngredientList = assistIngredientList; + } + + public String getSeasoningList() { + return seasoningList; + } + + public void setSeasoningList(String seasoningList) { + this.seasoningList = seasoningList; + } + + public double getMeal() { + return meal; + } + + public void setMeal(double meal) { + this.meal = meal; + } + + public double getVegetable() { + return vegetable; + } + + public void setVegetable(double vegetable) { + this.vegetable = vegetable; + } + + public double getFruits() { + return fruits; + } + + public void setFruits(double fruits) { + this.fruits = fruits; + } + + public double getGrain() { + return grain; + } + + public void setGrain(double grain) { + this.grain = grain; + } + + public double getMixedBeans() { + return mixedBeans; + } + + public void setMixedBeans(double mixedBeans) { + this.mixedBeans = mixedBeans; + } + + public double getSoyaNew() { + return soyaNew; + } + + public void setSoyaNew(double soyaNew) { + this.soyaNew = soyaNew; + } + + public boolean isSelect() { + return isSelect; + } + + public void setSelect(boolean select) { + isSelect = select; + } +} diff --git a/app/src/main/java/com/sw/st/model/RecommendValueModel.java b/app/src/main/java/com/sw/st/model/RecommendValueModel.java new file mode 100644 index 0000000..0b38cb6 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/RecommendValueModel.java @@ -0,0 +1,40 @@ +package com.sw.st.model; + +public class RecommendValueModel { + private String heat; + private String main; + private String fruits; + private String meatAndEggs; + + public String getHeat() { + return heat; + } + + public void setHeat(String heat) { + this.heat = heat; + } + + public String getMain() { + return main; + } + + public void setMain(String main) { + this.main = main; + } + + public String getFruits() { + return fruits; + } + + public void setFruits(String fruits) { + this.fruits = fruits; + } + + public String getMeatAndEggs() { + return meatAndEggs; + } + + public void setMeatAndEggs(String meatAndEggs) { + this.meatAndEggs = meatAndEggs; + } +} diff --git a/app/src/main/java/com/sw/st/model/ResponseData.java b/app/src/main/java/com/sw/st/model/ResponseData.java new file mode 100644 index 0000000..318dcd2 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/ResponseData.java @@ -0,0 +1,54 @@ +package com.sw.st.model; + +import java.io.Serializable; + +public class ResponseData implements Serializable { + + private static final long serialVersionUID = 5213230387175987834L; + + /** + * respCode : -1 + * ok : false + * message : 食堂编码不存在 + */ + private int code; + private boolean success; + private String msg; + private T data; + + public static long getSerialVersionUID() { + return serialVersionUID; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/model/ScalesFoodList.java b/app/src/main/java/com/sw/st/model/ScalesFoodList.java new file mode 100644 index 0000000..a7328f0 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/ScalesFoodList.java @@ -0,0 +1,78 @@ +package com.sw.st.model; + +import java.io.Serializable; + +public class ScalesFoodList implements Serializable { + private String userFoodIds; + private String foodId; + private String foodName; + private float price; + private float vipPrice; + private float priceSum; + private float vipPriceSum; + private int foodWeight; + + public String getUserFoodIds() { + return userFoodIds; + } + + public void setUserFoodIds(String userFoodIds) { + this.userFoodIds = userFoodIds; + } + + public String getFoodId() { + return foodId; + } + + public void setFoodId(String foodId) { + this.foodId = foodId; + } + + public String getFoodName() { + return foodName; + } + + public void setFoodName(String foodName) { + this.foodName = foodName; + } + + public float getVipPrice() { + return vipPrice; + } + + public void setVipPrice(float vipPrice) { + this.vipPrice = vipPrice; + } + + public float getPriceSum() { + return priceSum; + } + + public void setPriceSum(float priceSum) { + this.priceSum = priceSum; + } + + public float getVipPriceSum() { + return vipPriceSum; + } + + public void setVipPriceSum(float vipPriceSum) { + this.vipPriceSum = vipPriceSum; + } + + public float getPrice() { + return price; + } + + public void setPrice(float price) { + this.price = price; + } + + public int getFoodWeight() { + return foodWeight; + } + + public void setFoodWeight(int foodWeight) { + this.foodWeight = foodWeight; + } +} diff --git a/app/src/main/java/com/sw/st/model/UserFaceModel.java b/app/src/main/java/com/sw/st/model/UserFaceModel.java new file mode 100644 index 0000000..e358441 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/UserFaceModel.java @@ -0,0 +1,33 @@ +package com.sw.st.model; + +import java.io.Serializable; + +public class UserFaceModel implements Serializable { + String userFaceId; + String userId; + String faceFeature; + + public String getUserFaceId() { + return userFaceId; + } + + public void setUserFaceId(String userFaceId) { + this.userFaceId = userFaceId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getFaceFeature() { + return faceFeature; + } + + public void setFaceFeature(String faceFeature) { + this.faceFeature = faceFeature; + } +} diff --git a/app/src/main/java/com/sw/st/model/UserInfo.java b/app/src/main/java/com/sw/st/model/UserInfo.java new file mode 100644 index 0000000..d8688ce --- /dev/null +++ b/app/src/main/java/com/sw/st/model/UserInfo.java @@ -0,0 +1,88 @@ +package com.sw.st.model; + +import java.io.Serializable; + +public class UserInfo implements Serializable { + + private String userId; + private String secondDepartId; + private String secondDepartName; + private String thirdDepartId; + private String thirdDepartName; + private String realname; + private String empIdcard; + private String mobilePhone; + private String token; + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getSecondDepartId() { + return secondDepartId; + } + + public void setSecondDepartId(String secondDepartId) { + this.secondDepartId = secondDepartId; + } + + public String getSecondDepartName() { + return secondDepartName; + } + + public void setSecondDepartName(String secondDepartName) { + this.secondDepartName = secondDepartName; + } + + public String getThirdDepartId() { + return thirdDepartId; + } + + public void setThirdDepartId(String thirdDepartId) { + this.thirdDepartId = thirdDepartId; + } + + public String getThirdDepartName() { + return thirdDepartName; + } + + public void setThirdDepartName(String thirdDepartName) { + this.thirdDepartName = thirdDepartName; + } + + public String getRealname() { + return realname; + } + + public void setRealname(String realname) { + this.realname = realname; + } + + public String getEmpIdcard() { + return empIdcard; + } + + public void setEmpIdcard(String empIdcard) { + this.empIdcard = empIdcard; + } + + public String getMobilePhone() { + return mobilePhone; + } + + public void setMobilePhone(String mobilePhone) { + this.mobilePhone = mobilePhone; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/app/src/main/java/com/sw/st/model/UserModel.java b/app/src/main/java/com/sw/st/model/UserModel.java new file mode 100644 index 0000000..cb9a87b --- /dev/null +++ b/app/src/main/java/com/sw/st/model/UserModel.java @@ -0,0 +1,6 @@ +package com.sw.st.model; + + +public class UserModel { + +} diff --git a/app/src/main/java/com/sw/st/model/UserNutritionInfo.java b/app/src/main/java/com/sw/st/model/UserNutritionInfo.java new file mode 100644 index 0000000..c20600d --- /dev/null +++ b/app/src/main/java/com/sw/st/model/UserNutritionInfo.java @@ -0,0 +1,105 @@ +package com.sw.st.model; + +import java.io.Serializable; + +public class UserNutritionInfo implements Serializable { + private int recommendMax; + private int recommendMin; + private float foodWeight; + private int foodNum; + private float water; + private float energyKcal; + private float protein; + private float fat; + private float cho; + private float dietFiber; + private float na; + + public int getRecommendMax() { + return recommendMax; + } + + public void setRecommendMax(int recommendMax) { + this.recommendMax = recommendMax; + } + + public int getRecommendMin() { + return recommendMin; + } + + public void setRecommendMin(int recommendMin) { + this.recommendMin = recommendMin; + } + + public float getFoodWeight() { + return foodWeight; + } + + public void setFoodWeight(float foodWeight) { + this.foodWeight = foodWeight; + } + + public int getFoodNum() { + return foodNum; + } + + public void setFoodNum(int foodNum) { + this.foodNum = foodNum; + } + + public float getWater() { + return water; + } + + public void setWater(float water) { + this.water = water; + } + + public float getEnergyKcal() { + return energyKcal; + } + + public void setEnergyKcal(float energyKcal) { + this.energyKcal = energyKcal; + } + + public float getProtein() { + return protein; + } + + public void setProtein(float protein) { + this.protein = protein; + } + + public float getFat() { + return fat; + } + + public void setFat(float fat) { + this.fat = fat; + } + + public float getCho() { + return cho; + } + + public void setCho(float cho) { + this.cho = cho; + } + + public float getDietFiber() { + return dietFiber; + } + + public void setDietFiber(float dietFiber) { + this.dietFiber = dietFiber; + } + + public float getNa() { + return na; + } + + public void setNa(float na) { + this.na = na; + } +} diff --git a/app/src/main/java/com/sw/st/model/UserNutritionModel.java b/app/src/main/java/com/sw/st/model/UserNutritionModel.java new file mode 100644 index 0000000..ac64a25 --- /dev/null +++ b/app/src/main/java/com/sw/st/model/UserNutritionModel.java @@ -0,0 +1,172 @@ +package com.sw.st.model; + +import java.io.Serializable; +import java.util.ArrayList; + +public class UserNutritionModel implements Serializable { + + private float cho; + private float fat; + private float foodWeight; + private float fruits; + private String fruitsRecommend; + private float grain; + private String grainRecommend; + private float heatKcal; + private float meal; + private String mealRecommend; + private float price; + private float protein; + private float vegetable; + private String vegetableRecommend; + private float vipPrice; + private float soyaNew; + private float mixedBeans; + + private ArrayList scalesFoodOrderListVOList; + + public float getCho() { + return cho; + } + + public void setCho(float cho) { + this.cho = cho; + } + + public float getFat() { + return fat; + } + + public void setFat(float fat) { + this.fat = fat; + } + + public float getFoodWeight() { + return foodWeight; + } + + public void setFoodWeight(float foodWeight) { + this.foodWeight = foodWeight; + } + + public float getFruits() { + return fruits; + } + + public void setFruits(float fruits) { + this.fruits = fruits; + } + + public String getFruitsRecommend() { + return fruitsRecommend; + } + + public void setFruitsRecommend(String fruitsRecommend) { + this.fruitsRecommend = fruitsRecommend; + } + + public float getGrain() { + return grain; + } + + public void setGrain(float grain) { + this.grain = grain; + } + + public String getGrainRecommend() { + return grainRecommend; + } + + public void setGrainRecommend(String grainRecommend) { + this.grainRecommend = grainRecommend; + } + + public float getHeatKcal() { + return heatKcal; + } + + public void setHeatKcal(float heatKcal) { + this.heatKcal = heatKcal; + } + + public float getMeal() { + return meal; + } + + public void setMeal(float meal) { + this.meal = meal; + } + + public String getMealRecommend() { + return mealRecommend; + } + + public void setMealRecommend(String mealRecommend) { + this.mealRecommend = mealRecommend; + } + + public float getPrice() { + return price; + } + + public void setPrice(float price) { + this.price = price; + } + + public float getProtein() { + return protein; + } + + public void setProtein(float protein) { + this.protein = protein; + } + + public float getVegetable() { + return vegetable; + } + + public void setVegetable(float vegetable) { + this.vegetable = vegetable; + } + + public String getVegetableRecommend() { + return vegetableRecommend; + } + + public void setVegetableRecommend(String vegetableRecommend) { + this.vegetableRecommend = vegetableRecommend; + } + + public float getSoyaNew() { + return soyaNew; + } + + public void setSoyaNew(float soyaNew) { + this.soyaNew = soyaNew; + } + + public float getMixedBeans() { + return mixedBeans; + } + + public void setMixedBeans(float mixedBeans) { + this.mixedBeans = mixedBeans; + } + + public float getVipPrice() { + return vipPrice; + } + + public void setVipPrice(float vipPrice) { + this.vipPrice = vipPrice; + } + + public ArrayList getScalesFoodOrderListVOList() { + return scalesFoodOrderListVOList; + } + + public void setScalesFoodOrderListVOList(ArrayList scalesFoodOrderListVOList) { + this.scalesFoodOrderListVOList = scalesFoodOrderListVOList; + } + +} diff --git a/app/src/main/java/com/sw/st/model/WeightModel.java b/app/src/main/java/com/sw/st/model/WeightModel.java new file mode 100644 index 0000000..4e32e1a --- /dev/null +++ b/app/src/main/java/com/sw/st/model/WeightModel.java @@ -0,0 +1,40 @@ +package com.sw.st.model; + +public class WeightModel { + private String uuid; + private String ip; + private int weight; + private String skin; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public String getSkin() { + return skin; + } + + public void setSkin(String skin) { + this.skin = skin; + } +} diff --git a/app/src/main/java/com/sw/st/net/ImageLoaderManager.java b/app/src/main/java/com/sw/st/net/ImageLoaderManager.java new file mode 100644 index 0000000..de96463 --- /dev/null +++ b/app/src/main/java/com/sw/st/net/ImageLoaderManager.java @@ -0,0 +1,68 @@ +package com.sw.st.net; + +import android.content.Context; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.FutureTarget; +import com.bumptech.glide.request.RequestOptions; +import com.sw.st.R; + +import java.io.File; + +/** + * 图片加载管理 + */ +public class ImageLoaderManager { + public static void LoadRoundImage(Context context, String imgUrl, ImageView imageView, int roundingRadius) { + RequestOptions cropOptions = new RequestOptions(); + cropOptions.transform(new RoundedCorners(roundingRadius));//new CenterCrop() + + Glide.with(context) + .load(imgUrl) + .placeholder(R.mipmap.ic_launcher) + .dontAnimate() //解决圆形图显示占位图问题 + .error(R.mipmap.ic_launcher) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .apply(cropOptions) + .into(imageView); + } + + public static void LoadImage(Context context, String imgUrl, ImageView imageView) { + + Glide.with(context) + .load(imgUrl) + .placeholder(R.mipmap.ic_launcher) + .dontAnimate() //解决圆形图显示占位图问题 + .error(R.mipmap.ic_launcher) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .into(imageView); + +// Glide +// .with(myFragment) +// .load(url) +// .centerCrop() +// .placeholder(R.drawable.loading_spinner) +// .into(myImageView); + } + + + /** + * 缓存图片到本地 + */ + public static File CacheFile(Context context, String imgUrl) { + File cacheFile = null; + FutureTarget future = Glide.with(context) + .load(imgUrl) + .downloadOnly(500, 500); + try { + cacheFile = future.get(); + } catch (Exception e) { + e.printStackTrace(); + } + return cacheFile; + } + +} diff --git a/app/src/main/java/com/sw/st/net/WsListener.java b/app/src/main/java/com/sw/st/net/WsListener.java new file mode 100644 index 0000000..c278834 --- /dev/null +++ b/app/src/main/java/com/sw/st/net/WsListener.java @@ -0,0 +1,54 @@ +package com.sw.st.net; + +import androidx.annotation.Nullable; + +import com.sw.st.utils.L; + +import org.jetbrains.annotations.NotNull; + +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public abstract class WsListener extends WebSocketListener { + + public abstract void onWsDataChanged(String text); + + @Override + public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { + super.onClosed(webSocket, code, reason); + } + + @Override + public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) { + super.onClosing(webSocket, code, reason); + } + + @Override + public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) { + super.onFailure(webSocket, t, response); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { + super.onMessage(webSocket, text); + L.e("客户端收到消息:" + text); + onWsDataChanged(text); + //测试发消息 + webSocket.send("我是客户端,你好啊"); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) { + super.onMessage(webSocket, bytes); + } + + @Override + public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { + super.onOpen(webSocket, response); + L.e("连接成功!"); + } + +} + diff --git a/app/src/main/java/com/sw/st/net/helper/Convert.java b/app/src/main/java/com/sw/st/net/helper/Convert.java new file mode 100644 index 0000000..3ba269e --- /dev/null +++ b/app/src/main/java/com/sw/st/net/helper/Convert.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 jeasonlzy(廖子尧) + * + * 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.sw.st.net.helper; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.JsonReader; + +import java.io.Reader; +import java.lang.reflect.Type; + +/** + * ================================================ + * 作 者:jeasonlzy(廖子尧)Github地址:https://github.com/jeasonlzy + * 版 本:1.0 + * 创建日期:16/9/28 + * 描 述: Gson 数据转换工具类 + * 修订历史: + * ================================================ + */ +public class Convert { + + private static Gson create() { + return GsonHolder.gson; + } + + private static class GsonHolder { + private static Gson gson = new Gson(); + } + + public static T fromJson(String json, Class type) throws JsonIOException, JsonSyntaxException { + return create().fromJson(json, type); + } + + public static T fromJson(String json, Type type) { + return create().fromJson(json, type); + } + + public static T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException { + return create().fromJson(reader, typeOfT); + } + + public static T fromJson(Reader json, Class classOfT) throws JsonSyntaxException, JsonIOException { + return create().fromJson(json, classOfT); + } + + public static T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyntaxException { + return create().fromJson(json, typeOfT); + } + + public static String toJson(Object src) { + return create().toJson(src); + } + + public static String toJson(Object src, Type typeOfSrc) { + return create().toJson(src, typeOfSrc); + } + +} diff --git a/app/src/main/java/com/sw/st/net/helper/JsonConvert.java b/app/src/main/java/com/sw/st/net/helper/JsonConvert.java new file mode 100644 index 0000000..62cb97e --- /dev/null +++ b/app/src/main/java/com/sw/st/net/helper/JsonConvert.java @@ -0,0 +1,165 @@ +/* + * Copyright 2016 jeasonlzy(廖子尧) + * + * 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.sw.st.net.helper; + +import com.google.gson.stream.JsonReader; +import com.lzy.okgo.convert.Converter; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * ================================================ + * 作 者:jeasonlzy(廖子尧)Github地址:https://github.com/jeasonlzy + * 版 本:1.0 + * 创建日期:16/9/11 + * 描 述: + * 修订历史: + * ================================================ + */ +public class JsonConvert implements Converter { + + private Type type; + private Class clazz; + + public JsonConvert() { + } + + public JsonConvert(Type type) { + this.type = type; + } + + public JsonConvert(Class clazz) { + this.clazz = clazz; + } + + /** + * 该方法是子线程处理,不能做ui相关的工作 + * 主要作用是解析网络返回的 response 对象,生成onSuccess回调中需要的数据对象 + * 这里的解析工作不同的业务逻辑基本都不一样,所以需要自己实现,以下给出的时模板代码,实际使用根据需要修改 + */ + @Override + public T convertResponse(Response response) throws Throwable { + + // 重要的事情说三遍,不同的业务,这里的代码逻辑都不一样,如果你不修改,那么基本不可用 + // 重要的事情说三遍,不同的业务,这里的代码逻辑都不一样,如果你不修改,那么基本不可用 + // 重要的事情说三遍,不同的业务,这里的代码逻辑都不一样,如果你不修改,那么基本不可用 + + // 如果你对这里的代码原理不清楚,可以看这里的详细原理说明: https://github.com/jeasonlzy/okhttp-OkGo/wiki/JsonCallback + // 如果你对这里的代码原理不清楚,可以看这里的详细原理说明: https://github.com/jeasonlzy/okhttp-OkGo/wiki/JsonCallback + // 如果你对这里的代码原理不清楚,可以看这里的详细原理说明: https://github.com/jeasonlzy/okhttp-OkGo/wiki/JsonCallback + + if (type == null) { + if (clazz == null) { + // 如果没有通过构造函数传进来,就自动解析父类泛型的真实类型(有局限性,继承后就无法解析到) + Type genType = getClass().getGenericSuperclass(); + type = ((ParameterizedType) genType).getActualTypeArguments()[0]; + } else { + return parseClass(response, clazz); + } + } + + if (type instanceof ParameterizedType) { + return parseParameterizedType(response, (ParameterizedType) type); + } else if (type instanceof Class) { + return parseClass(response, (Class) type); + } else { + return parseType(response, type); + } + } + + private T parseClass(Response response, Class rawType) throws Exception { + if (rawType == null) return null; + ResponseBody body = response.body(); + if (body == null) return null; + JsonReader jsonReader = new JsonReader(body.charStream()); + + if (rawType == String.class) { + //noinspection unchecked + return (T) body.string(); + } else if (rawType == JSONObject.class) { + //noinspection unchecked + return (T) new JSONObject(body.string()); + } else if (rawType == JSONArray.class) { + //noinspection unchecked + return (T) new JSONArray(body.string()); + } else { + T t = Convert.fromJson(jsonReader, rawType); + response.close(); + return t; + } + } + + private T parseType(Response response, Type type) throws Exception { + if (type == null) return null; + ResponseBody body = response.body(); + if (body == null) return null; + JsonReader jsonReader = new JsonReader(body.charStream()); + + // 泛型格式如下: new JsonCallback<任意JavaBean>(this) + T t = Convert.fromJson(jsonReader, type); + response.close(); + return t; + } + + private T parseParameterizedType(Response response, ParameterizedType type) throws Exception { + if (type == null) return null; + ResponseBody body = response.body(); + if (body == null) return null; + JsonReader jsonReader = new JsonReader(body.charStream()); + + Type rawType = type.getRawType(); // 泛型的实际类型 + Type typeArgument = type.getActualTypeArguments()[0]; // 泛型的参数 + if (rawType != LzyResponse.class) { + // 泛型格式如下: new JsonCallback<外层BaseBean<内层JavaBean>>(this) + T t = Convert.fromJson(jsonReader, type); + response.close(); + return t; + } else { + if (typeArgument == Void.class) { + // 泛型格式如下: new JsonCallback>(this) + SimpleResponse simpleResponse = Convert.fromJson(jsonReader, SimpleResponse.class); + response.close(); + //noinspection unchecked + return (T) simpleResponse.toLzyResponse(); + } else { + // 泛型格式如下: new JsonCallback>(this) + LzyResponse lzyResponse = Convert.fromJson(jsonReader, type); + response.close(); + int code = lzyResponse.code; + //这里的0是以下意思 + //一般来说服务器会和客户端约定一个数表示成功,其余的表示失败,这里根据实际情况修改 + if (code == 0) { + //noinspection unchecked + return (T) lzyResponse; + } else if (code == 104) { + throw new IllegalStateException("用户授权信息无效"); + } else if (code == 105) { + throw new IllegalStateException("用户收取信息已过期"); + } else { + //直接将服务端的错误信息抛出,onError中可以获取 + throw new IllegalStateException("错误代码:" + code + ",错误信息:" + lzyResponse.msg); + } + } + } + } +} diff --git a/app/src/main/java/com/sw/st/net/helper/LzyResponse.java b/app/src/main/java/com/sw/st/net/helper/LzyResponse.java new file mode 100644 index 0000000..76c4689 --- /dev/null +++ b/app/src/main/java/com/sw/st/net/helper/LzyResponse.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 jeasonlzy(廖子尧) + * + * 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.sw.st.net.helper; + +import java.io.Serializable; + +/** + * ================================================ + * 作 者:jeasonlzy(廖子尧)Github地址:https://github.com/jeasonlzy + * 版 本:1.0 + * 创建日期:16/9/28 + * 描 述: + * 修订历史: + * ================================================ + */ +public class LzyResponse implements Serializable { + + private static final long serialVersionUID = 5213230387175987834L; + + public int code; + public String msg; + public T data; + + @Override + public String toString() { + return "LzyResponse{\n" +// + "\tcode=" + code + "\n" +// + "\tmsg='" + msg + "\'\n" +// + "\tdata=" + data + "\n" +// + '}'; + } +} diff --git a/app/src/main/java/com/sw/st/net/helper/SimpleResponse.java b/app/src/main/java/com/sw/st/net/helper/SimpleResponse.java new file mode 100644 index 0000000..8182ffc --- /dev/null +++ b/app/src/main/java/com/sw/st/net/helper/SimpleResponse.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 jeasonlzy(廖子尧) + * + * 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.sw.st.net.helper; + +import java.io.Serializable; + +/** + * ================================================ + * 作 者:jeasonlzy(廖子尧)Github地址:https://github.com/jeasonlzy + * 版 本:1.0 + * 创建日期:16/9/28 + * 描 述: + * 修订历史: + * ================================================ + */ +public class SimpleResponse implements Serializable { + + private static final long serialVersionUID = -1477609349345966116L; + + public int code; + public String msg; + + public LzyResponse toLzyResponse() { + LzyResponse lzyResponse = new LzyResponse(); + lzyResponse.code = code; + lzyResponse.msg = msg; + return lzyResponse; + } +} diff --git a/app/src/main/java/com/sw/st/net/helper/interceptor/TokenInterceptor.java b/app/src/main/java/com/sw/st/net/helper/interceptor/TokenInterceptor.java new file mode 100644 index 0000000..3eda304 --- /dev/null +++ b/app/src/main/java/com/sw/st/net/helper/interceptor/TokenInterceptor.java @@ -0,0 +1,95 @@ +package com.sw.st.net.helper.interceptor; + +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.nio.charset.Charset; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; + +/** + * user:lqm + * desc:token验证令牌失效拦截器,token过期时刷新token或弹dialog跳转到登录界面 + * src:https://www.jianshu.com/p/62ab11ddacc8 + */ + +public class TokenInterceptor implements Interceptor { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + // try the request + Response originalResponse = chain.proceed(request); + + /**通过如下的办法曲线取到请求完成的数据 + * + * 原本想通过 originalResponse.body().string() + * 去取到请求完成的数据,但是一直报错,不知道是okhttp的bug还是操作不当 + * + * 然后去看了okhttp的源码,找到了这个曲线方法,取到请求完成的数据后,根据特定的判断条件去判断token过期 + */ + ResponseBody responseBody = originalResponse.body(); + BufferedSource source = responseBody.source(); + source.request(Long.MAX_VALUE); // Buffer the entire body. + Buffer buffer = source.buffer(); + Charset charset = UTF8; + MediaType contentType = responseBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + String bodyString = buffer.clone().readString(charset); + Log.d("body---------->", bodyString); + + /***************************************/ + + JSONObject extrasJson = null; + try { + if (extrasJson == null){ + extrasJson = new JSONObject(bodyString); + } + } catch (JSONException e) { + e.printStackTrace(); + } + int code = Integer.parseInt(extrasJson.optString("errorCode")); //根据后台返回数据执行修改 + +// if (response shows expired token){//根据和服务端的约定判断token过期 + if (code == 401){ //假设服务端返回码401为token过期 + + // TODO 弹出全局的dialog或者用以下代码刷新token (全局dialog可以用WindowManager相应实现) + + //取出本地的refreshToken + String refreshToken = "sssgr122222222"; + // 通过一个特定的接口获取新的token,此处要用到同步的retrofit请求 +// ApiService service = ServiceManager.getService(ApiService.class); +// Call call = service.refreshToken(refreshToken); + //要用retrofit的同步方式 +// String newToken = call.execute().body(); + String newToken = "sssssssss ne wtoken"; + + // create a new request and modify it accordingly using the new token + Request newRequest = request.newBuilder().header("token", newToken) + .build(); + + // retry the request + + originalResponse.body().close(); + return chain.proceed(newRequest); + } + + // otherwise just pass the original response on + return originalResponse; + } + +} diff --git a/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxObserver.java b/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxObserver.java new file mode 100644 index 0000000..231e917 --- /dev/null +++ b/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxObserver.java @@ -0,0 +1,48 @@ +package com.sw.st.net.helper.rxjavahelper; + + +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; + +/** + * 自己的Observer,减少实现不必要的回调 + */ +public abstract class RxObserver implements Observer { + + @Override + public void onSubscribe(Disposable d) { + _onSubscribe(d); + } + + @Override + public void onNext(T t) { + _onNext(t); + } + + + @Override + public void onError(Throwable e) { + _onError(e.getMessage()); + } + + @Override + public void onComplete() { + _onComplete(); + } + + public void _onSubscribe(Disposable d) { + + } + + + + public void _onComplete() { + + } + + //抽象方法,必须实现 + public abstract void _onNext(T t); + + public abstract void _onError(String errorMessage); + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxResultHelper.java b/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxResultHelper.java new file mode 100644 index 0000000..8f22c27 --- /dev/null +++ b/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxResultHelper.java @@ -0,0 +1,73 @@ +package com.sw.st.net.helper.rxjavahelper; + +import com.sw.st.model.ResponseData; + +import org.jetbrains.annotations.NotNull; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.ObservableTransformer; +import io.reactivex.functions.Function; + +/** + * 服务器的返回的数据格式一般都是一致的,所有我们每个网络请求都可以使 + * 用compose(RxResultHelper.handleResult())来处理服务器返回,一般服务器返回成功码为200, + * 相应改一下返回码的判断就行了 + */ + +public class RxResultHelper { + + private static final int RESPONSE_SUCCESS_CODE = 200; //大部分为200 + private static final int RESPONSE_ERROR_CODE = -1; + + + public static ObservableTransformer, T> handleResult() { + return new ObservableTransformer, T>() { + @Override + public ObservableSource apply(Observable> tObservable) { + return tObservable.flatMap( + new Function, Observable>() { + @Override + public Observable apply(ResponseData tResponseData) { + //可以相应更改 + if (tResponseData.getCode() == RESPONSE_SUCCESS_CODE) { + + return Observable.just(tResponseData.getData()); + } else if (tResponseData.getCode() == RESPONSE_ERROR_CODE) { + return Observable.error(new Exception(tResponseData.getMsg())); + } else { + return Observable.empty(); + } + } + } + ); + } + + }; + } + + /** + * 返回原始json + * + * @return + */ + public static ObservableTransformer handleJsonResponse() { + return new ObservableTransformer() { + @NotNull + @Override + public ObservableSource apply(@NotNull Observable tObservable) { + return tObservable.flatMap( + new Function>() { + @Override + public Observable apply(@NotNull String tResponseData) throws Exception { + //可以相应更改 + + return Observable.just(tResponseData); + } + } + ); + } + + }; + } +} diff --git a/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxSchedulersHelper.java b/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxSchedulersHelper.java new file mode 100644 index 0000000..76f0ca6 --- /dev/null +++ b/app/src/main/java/com/sw/st/net/helper/rxjavahelper/RxSchedulersHelper.java @@ -0,0 +1,29 @@ +package com.sw.st.net.helper.rxjavahelper; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.ObservableTransformer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +/** + * compose()里接收一个Transformer对象,ObservableTransformer + * 可以通过它将一种类型的Observable转换成另一种类型的Observable。 + * 现在.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) + * 的地方可以用.compose(RxSchedulersHelper.io_main())代替。 + */ + +public class RxSchedulersHelper { + + public static ObservableTransformer io_main() { + return new ObservableTransformer() { + @Override + public ObservableSource apply(Observable upstream) { + return upstream + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + }; + } + +} diff --git a/app/src/main/java/com/sw/st/ui/addfood/AddFoodActivity.java b/app/src/main/java/com/sw/st/ui/addfood/AddFoodActivity.java new file mode 100644 index 0000000..407510b --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/addfood/AddFoodActivity.java @@ -0,0 +1,158 @@ +package com.sw.st.ui.addfood; + +import static com.sw.st.ui.setting.FoodSettingActivity.CURRENT_FOOD_DATA; +import static com.sw.st.ui.setting.FoodSettingActivity.REST_NUM; + +import android.view.View; +import android.widget.TextView; + +import com.google.gson.reflect.TypeToken; +import com.gyf.immersionbar.BarHide; +import com.gyf.immersionbar.ImmersionBar; +import com.sw.st.R; +import com.sw.st.base.BaseActivity; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.MessageEvent; +import com.sw.st.net.helper.Convert; +import com.sw.st.utils.L; +import com.sw.st.utils.PrefUtils; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import butterknife.BindView; +import butterknife.OnClick; + +public class AddFoodActivity extends BaseActivity implements AddFoodView { + + @BindView(R.id.foodName) + TextView foodName; + @BindView(R.id.currentWeight) + TextView currentWeight; + @BindView(R.id.addWeight) + TextView addWeight; + + private int laseWeight = 0; + private int originalWeight = 0; + private int tablewareWeight = 0; + private FoodInfoModel foodInfo; + + @Override + protected int provideContentViewId() { + return R.layout.activity_add_food; + } + + @Override + public void initView() { + ImmersionBar.with(this) + .hideBar(BarHide.FLAG_HIDE_BAR) + .statusBarAlpha(0f) + .statusBarDarkFont(true) + .statusBarColor(R.color.white) + .init(); + + + laseWeight = getIntent().getIntExtra("laseWeight", 0); + currentWeight.setText(laseWeight + ""); + EventBus.getDefault().register(this); + } + + @Override + public void initData() { + String foodInfoStr = PrefUtils.getString(mContext, CURRENT_FOOD_DATA, ""); + foodInfo = Convert.fromJson(foodInfoStr, new TypeToken() { + }.getType()); + foodName.setText(foodInfo.getFoodName()); + } + + @Override + public void initListener() { + } + + + @OnClick({R.id.back, R.id.start}) + public void onViewClicked(View view) { + switch (view.getId()) { + case R.id.back: + finish(); + break; + case R.id.start: + showProgress("处理中..."); + int totalWeight = originalWeight - laseWeight; + mPresenter.addFoodTotalWeight(foodInfo.getId(), + 2, + totalWeight, + PrefUtils.getString(mContext, REST_NUM, "")); + break; + } + } + + @Override + protected AddFoodPresenter createPresenter() { + return new AddFoodPresenter(); + } + + @Override + protected void afterRequestPermission(int requestCode, boolean isAllGranted) { + + } + + @Override + public void addFoodWeightSuccess(String info) { + hideProgress(); + finish(); + } + + @Override + public void addFoodWeightFail(String info) { + hideProgress(); + finish(); + } + + @Override + public void showProgress(String tipString) { + showWaitingDialog(tipString); + } + + @Override + public void hideProgress() { + hideWaitingDialog(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onReceiveMsg(MessageEvent eventModel) { + switch (eventModel.getType()) { + case MessageEvent.MESSAGE_WEIGHT_INFO: + if (foodInfo == null) { + return; + } + int weight = eventModel.getWeight(); + + if (foodInfo.getStFoodInfoSetting() != null + && foodInfo.getStFoodInfoSetting().isTablewareStatus()) { + tablewareWeight = foodInfo.getStFoodInfoSetting().getTablewareWeight(); + originalWeight = weight - tablewareWeight; + } else { + originalWeight = weight; + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + int totalWeight = originalWeight - laseWeight; + addWeight.setText(totalWeight + ""); + } + }); + break; + } + } + +} diff --git a/app/src/main/java/com/sw/st/ui/addfood/AddFoodPresenter.java b/app/src/main/java/com/sw/st/ui/addfood/AddFoodPresenter.java new file mode 100644 index 0000000..2bfef8b --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/addfood/AddFoodPresenter.java @@ -0,0 +1,63 @@ +package com.sw.st.ui.addfood; + + +import com.sw.st.api.SwService; +import com.sw.st.base.BasePresenter; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; + +import org.json.JSONException; +import org.json.JSONObject; + +import io.reactivex.disposables.Disposable; + + +public class AddFoodPresenter extends BasePresenter { + + /** + * @param foodId 菜品ID + * @param type 是否开餐1开餐,2加菜 + * @param totalWeight 菜品增重 + * @return + */ + public void addFoodTotalWeight(String foodId, int type, double totalWeight, String deviceMac) { + SwService.startMealService(foodId, type, totalWeight, deviceMac) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().addFoodWeightSuccess(data); + } else { + getView().addFoodWeightFail(jsonObject.optString("message")); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().addFoodWeightFail("数据解析异常"); + } + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().addFoodWeightFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + +} diff --git a/app/src/main/java/com/sw/st/ui/addfood/AddFoodView.java b/app/src/main/java/com/sw/st/ui/addfood/AddFoodView.java new file mode 100644 index 0000000..b6c253e --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/addfood/AddFoodView.java @@ -0,0 +1,16 @@ +package com.sw.st.ui.addfood; + + +import com.sw.st.base.BaseView; +import com.sw.st.ui.setting.FoodListModel; + +import java.util.ArrayList; + +public interface AddFoodView extends BaseView { + + + void addFoodWeightSuccess(String info); + + void addFoodWeightFail(String info); + +} diff --git a/app/src/main/java/com/sw/st/ui/device/BindDeviceModel.java b/app/src/main/java/com/sw/st/ui/device/BindDeviceModel.java new file mode 100644 index 0000000..354acc9 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/device/BindDeviceModel.java @@ -0,0 +1,96 @@ +package com.sw.st.ui.device; + +import java.util.Date; + +public class BindDeviceModel { + private int deviceNo; + private String deviceMac; + private String deviceDesc; + private String restId; + private String restName; + private String restNo; + private String createBy; + private String createDate; + private String updateBy; + private String updateDate; + + public int getDeviceNo() { + return deviceNo; + } + + public void setDeviceNo(int deviceNo) { + this.deviceNo = deviceNo; + } + + public String getDeviceMac() { + return deviceMac; + } + + public void setDeviceMac(String deviceMac) { + this.deviceMac = deviceMac; + } + + public String getDeviceDesc() { + return deviceDesc; + } + + public void setDeviceDesc(String deviceDesc) { + this.deviceDesc = deviceDesc; + } + + public String getRestId() { + return restId; + } + + public void setRestId(String restId) { + this.restId = restId; + } + + public String getRestName() { + return restName; + } + + public void setRestName(String restName) { + this.restName = restName; + } + + public String getRestNo() { + return restNo; + } + + public void setRestNo(String restNo) { + this.restNo = restNo; + } + + public String getCreateBy() { + return createBy; + } + + public void setCreateBy(String createBy) { + this.createBy = createBy; + } + + public String getCreateDate() { + return createDate; + } + + public void setCreateDate(String createDate) { + this.createDate = createDate; + } + + public String getUpdateBy() { + return updateBy; + } + + public void setUpdateBy(String updateBy) { + this.updateBy = updateBy; + } + + public String getUpdateDate() { + return updateDate; + } + + public void setUpdateDate(String updateDate) { + this.updateDate = updateDate; + } +} diff --git a/app/src/main/java/com/sw/st/ui/device/ChengListActivity.java b/app/src/main/java/com/sw/st/ui/device/ChengListActivity.java new file mode 100644 index 0000000..a91d181 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/device/ChengListActivity.java @@ -0,0 +1,187 @@ +package com.sw.st.ui.device; + +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.listener.OnItemClickListener; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.gyf.immersionbar.BarHide; +import com.gyf.immersionbar.ImmersionBar; +import com.scwang.smart.refresh.layout.SmartRefreshLayout; +import com.scwang.smart.refresh.layout.api.RefreshLayout; +import com.scwang.smart.refresh.layout.listener.OnRefreshListener; +import com.sw.st.R; +import com.sw.st.base.BaseActivity; +import com.sw.st.base.BasePresenter; +import com.sw.st.model.WeightModel; +import com.sw.st.net.WsListener; +import com.sw.st.utils.PrefUtils; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +import butterknife.BindView; +import butterknife.OnClick; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.WebSocket; + +import static com.sw.st.application.AppConst.SOCKET_SERVICE_URL; + +public class ChengListActivity extends BaseActivity { + + private final String CHENG_IP = "cheng_ip"; + + @BindView(R.id.title_name_tv) + TextView titleName; + @BindView(R.id.refreshLayout) + SmartRefreshLayout refreshLayout; + @BindView(R.id.recyclerView_content) + RecyclerView recyclerView; + + private ChengListAdapter chengListAdapter; + + private OkHttpClient mClient; + private WebSocket mWebSocket; + private Handler handler; + + @Override + protected int provideContentViewId() { + return R.layout.activity_cheng_list; + } + + @Override + public void initView() { + ImmersionBar.with(this) + .hideBar(BarHide.FLAG_HIDE_BAR) + .statusBarAlpha(0f) + .statusBarDarkFont(true) + .statusBarColor(R.color.white) + .init(); + titleName.setText("请选择"); + + refreshLayout.setOnRefreshListener(new OnRefreshListener() { + @Override + public void onRefresh(@NonNull RefreshLayout refreshLayout) { + + send("refreshChengs"); + } + }); + refreshLayout.setEnableLoadMore(false); + + chengListAdapter = new ChengListAdapter(chengDataList, ChengListActivity.this); + recyclerView.setAdapter(chengListAdapter); + } + + private ArrayList chengDataList = new ArrayList<>(); + + @Override + public void initData() { + initWebSocket(); + handler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case 1://返回socket数据 + refreshLayout.finishRefresh(); + chengDataList.clear(); + String msgStr = msg.obj.toString(); + Gson gson = new Gson(); + JsonParser parser = new JsonParser(); + JsonArray Jarray = parser.parse(msgStr).getAsJsonArray(); + for (JsonElement obj : Jarray) { + WeightModel cse = gson.fromJson(obj, WeightModel.class); + chengDataList.add(cse); + } + chengListAdapter.notifyDataSetChanged(); + break; + } + } + }; + } + + @Override + public void initListener() { + chengListAdapter.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(@NonNull @NotNull BaseQuickAdapter adapter, @NonNull @NotNull View view, int position) { + PrefUtils.setString(ChengListActivity.this, CHENG_IP, chengDataList.get(position).getIp()); + finish(); + } + }); + } + + @OnClick({R.id.title_back_iv}) + public void onViewClicked(View view) { + switch (view.getId()) { + case R.id.title_back_iv: + finish(); + break; + } + } + + + //初始化WebSocket + public void initWebSocket() { + mClient = new OkHttpClient.Builder() + .readTimeout(10, TimeUnit.SECONDS)//设置读取超时时间 + .writeTimeout(10, TimeUnit.SECONDS)//设置写的超时时间 + .connectTimeout(10, TimeUnit.SECONDS)//设置连接超时时间 + .build(); + Request request = new Request.Builder() + .url(SOCKET_SERVICE_URL) + .build(); + mWebSocket = mClient.newWebSocket(request, new WsListener() { + @Override + public void onWsDataChanged(String text) { + Message message = new Message(); + message.what = 1; + message.obj = text; + handler.sendMessage(message); + } + }); + send("refreshChengs"); + } + + //发送String消息 + public void send(final String message) { + if (mWebSocket != null) { + mWebSocket.send(message); + } + } + + //主动断开连接 + public void disconnect(int code, String reason) { + if (mWebSocket != null) + mWebSocket.close(code, reason); + } + + + @Override + protected BasePresenter createPresenter() { + return null; + } + + @Override + protected void afterRequestPermission(int requestCode, boolean isAllGranted) { + + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + disconnect(1001, null); + } +} diff --git a/app/src/main/java/com/sw/st/ui/device/ChengListAdapter.java b/app/src/main/java/com/sw/st/ui/device/ChengListAdapter.java new file mode 100644 index 0000000..2d8c7b7 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/device/ChengListAdapter.java @@ -0,0 +1,33 @@ +package com.sw.st.ui.device; + +import android.content.Context; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.sw.st.R; +import com.sw.st.model.WeightModel; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class ChengListAdapter extends BaseQuickAdapter { + + Context context; + + public ChengListAdapter(@Nullable List listModels, Context context) { + super(R.layout.item_cheng, listModels); + this.context = context; + addChildClickViewIds(R.id.item_cheng_open_com, R.id.item_cheng_close_com); + } + + + @Override + protected void convert(@NotNull BaseViewHolder baseViewHolder, WeightModel weightModel) { + baseViewHolder.setText(R.id.item_cheng_ip, "COM:" + weightModel.getIp()); +// baseViewHolder.setText(R.id.item_cheng_weight, "重量:" + weightModel.getWeight() + "g"); + } + + +} diff --git a/app/src/main/java/com/sw/st/ui/device/ComListActivity.java b/app/src/main/java/com/sw/st/ui/device/ComListActivity.java new file mode 100644 index 0000000..e000998 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/device/ComListActivity.java @@ -0,0 +1,225 @@ +//package com.sw.st.ui.device; +// +//import android.os.Handler; +//import android.os.Message; +//import android.view.View; +//import android.widget.TextView; +//import android.widget.Toast; +// +//import androidx.annotation.NonNull; +//import androidx.recyclerview.widget.RecyclerView; +// +//import com.chad.library.adapter.base.BaseQuickAdapter; +//import com.chad.library.adapter.base.listener.OnItemChildClickListener; +//import com.chad.library.adapter.base.listener.OnItemClickListener; +//import com.gyf.immersionbar.BarHide; +//import com.gyf.immersionbar.ImmersionBar; +//import com.scwang.smart.refresh.layout.SmartRefreshLayout; +//import com.scwang.smart.refresh.layout.api.RefreshLayout; +//import com.scwang.smart.refresh.layout.listener.OnRefreshListener; +//import com.sw.st.R; +//import com.sw.st.base.BaseActivity; +//import com.sw.st.base.BasePresenter; +//import com.sw.st.model.WeightModel; +//import com.sw.st.utils.L; +//import com.sw.st.utils.PrefUtils; +// +//import org.jetbrains.annotations.NotNull; +// +//import java.io.File; +//import java.util.ArrayList; +//import java.util.List; +// +//import aclasdriver.AclasScale; +//import butterknife.BindView; +//import butterknife.OnClick; +// +//public class ComListActivity extends BaseActivity { +// +// private final String COM_INFO = "com_info"; +// +// @BindView(R.id.title_name_tv) +// TextView titleName; +// @BindView(R.id.refreshLayout) +// SmartRefreshLayout refreshLayout; +// @BindView(R.id.recyclerView_content) +// RecyclerView recyclerView; +// @BindView(R.id.weight_info) +// TextView weightInfo; +// +// private ChengListAdapter chengListAdapter; +// +// +// @Override +// protected int provideContentViewId() { +// return R.layout.activity_cheng_list; +// } +// +// @Override +// public void initView() { +// ImmersionBar.with(this) +// .hideBar(BarHide.FLAG_HIDE_BAR) +// .statusBarAlpha(0f) +// .statusBarDarkFont(true) +// .statusBarColor(R.color.white) +// .init(); +// titleName.setText("请选择"); +// +// refreshLayout.setOnRefreshListener(new OnRefreshListener() { +// @Override +// public void onRefresh(@NonNull RefreshLayout refreshLayout) { +// +// } +// }); +// refreshLayout.setEnableLoadMore(false); +// +// chengListAdapter = new ChengListAdapter(chengDataList, ComListActivity.this); +// recyclerView.setAdapter(chengListAdapter); +// } +// +// private ArrayList chengDataList = new ArrayList<>(); +// +// @Override +// public void initData() { +// final List list = AclasScale.getAvailableUartList(); +// for (String com : list) { +// WeightModel weightModel = new WeightModel(); +// weightModel.setIp(com); +// chengDataList.add(weightModel); +// } +// +// handler = new Handler() { +// @Override +// public void handleMessage(Message msg) { +// switch (msg.what) { +// case 1: +// weightInfo.setText(msg.obj.toString()); +// break; +// } +// } +// }; +// +// initAclasScaleListener(); +// } +// +// @Override +// public void initListener() { +// chengListAdapter.setOnItemClickListener(new OnItemClickListener() { +// @Override +// public void onItemClick(@NonNull @NotNull BaseQuickAdapter adapter, @NonNull @NotNull View view, int position) { +// PrefUtils.setString(ComListActivity.this, COM_INFO, chengDataList.get(position).getIp()); +// finish(); +// } +// }); +// chengListAdapter.setOnItemChildClickListener(new OnItemChildClickListener() { +// @Override +// public void onItemChildClick(@NonNull @NotNull BaseQuickAdapter adapter, @NonNull @NotNull View view, int position) { +// switch (view.getId()) { +// case R.id.item_cheng_open_com: +// showToast("item_cheng_open_com" + "===" + position); +// openScale(chengDataList.get(position).getIp()); +// break; +// case R.id.item_cheng_close_com: +// showToast("item_cheng_close_com" + "===" + position); +// CloseDevice(); +// break; +// } +// } +// }); +// } +// +// @OnClick({R.id.title_back_iv}) +// public void onViewClicked(View view) { +// switch (view.getId()) { +// case R.id.title_back_iv: +// finish(); +// break; +// } +// } +// +// +// @Override +// protected BasePresenter createPresenter() { +// return null; +// } +// +// @Override +// protected void afterRequestPermission(int requestCode, boolean isAllGranted) { +// +// } +// +// @Override +// protected void onDestroy() { +// super.onDestroy(); +// +// CloseDevice(); +// } +// +// private Handler handler; +// +// private AclasScale scale = null; +// AclasScale.AclasScaleListener listener = null; +// +// private void initAclasScaleListener() { +// +// listener = new AclasScale.AclasScaleListener() { +// public void OnError(int code) { +// L.e("OnError!!!!" + code); +// } +// +// public void OnDataReceive(AclasScale.St_Data data) { +// if (data.m_iStatus == -1) { +// L.e("data error"); +// } else { +// L.e("data:" + (data.m_iStatus == 0 ? "Unstable" : "Stable") + " weight:" + String.format("%.3f", data.m_fWeight) +// + " price:" + String.format("%.3f", data.m_fPrice) + " total:" +// + data.m_fTotal + " key:" + data.m_stKey.m_iValue + " str:" + data.m_stKey.m_strKey); +// Message message = new Message(); +// message.what = 1; +// message.obj = (data.m_iStatus == 0 ? "U" : "S") + " " + data.m_fWeight + data.m_strUnit; +// handler.sendMessage(message); +// } +// } +// +// public void OnReadTare(float fVal, boolean bFlag) { +// String string = bFlag ? String.valueOf(fVal) : "Error"; +// L.d("data len OnReadTare:" + bFlag + " " + fVal); +// } +// }; +// } +// +// private void openScale(String strAdd) { +// try { +// CloseDevice(); +// int iType = 0;// 0:计重模式; 1:计价模式 +// scale = new AclasScale(new File(strAdd), iType, listener); +// scale.bLogFlag = true; +// scale.open(); +//// m_strDeviceId = scale.GetId(); +// } catch (SecurityException e) { +// // TODO Auto-generated catch block +// scale = null; +// Toast.makeText(this, "OpenScale exception", Toast.LENGTH_SHORT).show(); +// e.printStackTrace(); +// } catch (Exception e) { +// // TODO: handle exception +// scale = null; +// Toast.makeText(this, "OpenScale exception", Toast.LENGTH_SHORT).show(); +// e.printStackTrace(); +// } +// if (scale != null) { +// L.e("scale start run thread"); +// scale.StartRead(); +// } else { +// L.e("scale null!!!!!!!!!!!!!!!!!!!"); +// } +// } +// +// private void CloseDevice() { +// if (scale != null) { +// scale.StopRead(); +// scale.close(); +// scale = null; +// } +// } +//} diff --git a/app/src/main/java/com/sw/st/ui/device/DeviceActivity.java b/app/src/main/java/com/sw/st/ui/device/DeviceActivity.java new file mode 100644 index 0000000..03ac880 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/device/DeviceActivity.java @@ -0,0 +1,277 @@ +//package com.sw.st.ui.device; +// +//import android.app.AlertDialog; +//import android.app.Dialog; +//import android.content.Intent; +//import android.view.View; +//import android.widget.EditText; +//import android.widget.TextView; +//import android.widget.Toast; +// +//import com.gyf.immersionbar.BarHide; +//import com.gyf.immersionbar.ImmersionBar; +//import com.sw.st.R; +//import com.sw.st.base.BaseActivity; +//import com.sw.st.model.UserFaceModel; +//import com.sw.st.utils.AppUtil; +//import com.sw.st.utils.DeviceIdUtil; +//import com.sw.st.utils.PrefUtils; +//import com.sw.st.utils.faceserver.FaceServer; +// +//import java.util.ArrayList; +// +//import butterknife.BindView; +//import butterknife.OnClick; +// +//public class DeviceActivity extends BaseActivity implements DeviceView { +// +// private final String REST_NUM = "restId"; +// private final String COM_INFO = "com_info"; +// +// @BindView(R.id.title_name_tv) +// TextView titleName; +// @BindView(R.id.device_restnum) +// EditText restNumEdit; +// @BindView(R.id.device_id) +// EditText deviceIdEdit; +// @BindView(R.id.device_description) +// EditText descriptionEdit; +// @BindView(R.id.device_cheng_ip) +// TextView chengIpTv; +// +//// private WeightApi weightApi; +// +// @Override +// protected int provideContentViewId() { +// return R.layout.activity_device; +// } +// +// @Override +// public void initView() { +// +// ImmersionBar.with(this) +// .hideBar(BarHide.FLAG_HIDE_BAR) +// .statusBarAlpha(0f) +// .statusBarDarkFont(true) +// .statusBarColor(R.color.white) +// .init(); +// +// +// titleName.setText("食堂设置"); +// deviceIdEdit.setText(DeviceIdUtil.getDeviceId(this)); +// restNumEdit.setText(PrefUtils.getString(this, REST_NUM, null)); +// +//// weightApi = new WeightApi(); +// } +// +// +// @Override +// public void initData() { +// +// } +// +// @Override +// public void initListener() { +//// initAclasScaleListener(); +// } +// +// @Override +// protected void onResume() { +// super.onResume(); +// +// chengIpTv.setText(PrefUtils.getString(this, COM_INFO, "请选择串口")); +//// openScale(chengIpTv.getText().toString()); +// +// +// } +// +// @OnClick({R.id.title_back_iv, R.id.device_bind_ok, R.id.device_cheng_ip, R.id.button_zero, R.id.refreshFaceInfo}) +// public void onViewClicked(View view) { +// switch (view.getId()) { +// case R.id.title_back_iv: +// finish(); +// break; +// case R.id.device_bind_ok: +// String restNum = restNumEdit.getText().toString(); +// if (AppUtil.isEmpty(restNum)) { +// showToast("请输入食堂编号"); +// return; +// } +// PrefUtils.setString(this, REST_NUM, restNum); +// mPresenter.relRest(deviceIdEdit.getText().toString(), restNum, descriptionEdit.getText().toString()); +// break; +// case R.id.device_cheng_ip: +//// startActivity(new Intent(DeviceActivity.this, ComListActivity.class)); +// break; +// case R.id.button_zero: +//// scale.SetZero(); +// +//// try { +//// weightApi.openPort(chengIpTv.getText().toString(), 9600, responseListener); +//// } catch (Exception exception) { +//// exception.printStackTrace(); +//// Toast.makeText(DeviceActivity.this, "打开失败", +//// Toast.LENGTH_SHORT).show(); +//// } +//// +//// byte[] cmd = new byte[]{0x53, 0x57, 0x02, 0x00, 0x00, 0x00, (byte) 0xAC};//置0指令 +//// try { +//// weightApi.sendCmd(cmd); +//// } catch (Exception exception) { +//// exception.printStackTrace(); +//// Toast.makeText(DeviceActivity.this, "请连接电子秤", Toast.LENGTH_SHORT).show(); +//// } +// break; +// case R.id.refreshFaceInfo: +// mPresenter.getUserFace(); +// break; +// } +// } +// +// @Override +// protected DevicePresenter createPresenter() { +// return new DevicePresenter(); +// } +// +// @Override +// protected void afterRequestPermission(int requestCode, boolean isAllGranted) { +// +// } +// +// @Override +// protected void onDestroy() { +// super.onDestroy(); +//// CloseDevice(); +// } +// +// @Override +// public void relRestSuccess(String info) { +// showToast("绑定成功"); +// } +// +// @Override +// public void relRestFail(String info) { +// +// showToast(info); +// +// } +// +// @Override +// public void getFaceFeatureSuccess(ArrayList list) { +// if (list != null && list.size() > 0) { +// FaceServer.getInstance().clearAllFaces(this); +// +// for (int i = 0; i < list.size(); i++) { +// UserFaceModel faceModel = list.get(i); +// FaceServer.getInstance().saveFaceFeature(faceModel.getUserId() + ":" + faceModel.getUserFaceId(), faceModel.getFaceFeature()); +// } +// showToast("人脸数据更新成功"); +// +// Dialog alertDialog = new AlertDialog.Builder(this). +// setTitle("成功"). +// setMessage("人脸数据更新成功"). +// create(); +// alertDialog.show(); +// } else { +// showToast("人脸数据更新失败"); +// } +// } +// +// @Override +// public void getFaceFeatureFail(String info) { +// +// } +// +// @Override +// public void showProgress(String tipString) { +// +// } +// +// @Override +// public void hideProgress() { +// +// } +// +//// API.ResponseListener responseListener = new API.ResponseListener() { +//// @Override +//// public void onGetCmdResult(final byte[] result) { +//// +//// runOnUiThread(new Runnable() { +//// @Override +//// public void run() { +//// } +//// }); +//// } +//// +//// @Override +//// public void onGetWeightInfo(final byte[] weightInfo) { +//// runOnUiThread(new Runnable() { +//// @Override +//// public void run() { +//// } +//// }); +//// } +//// }; +//// private AclasScale scale = null; +//// AclasScale.AclasScaleListener listener = null; +//// +//// private void initAclasScaleListener() { +//// +//// listener = new AclasScale.AclasScaleListener() { +//// public void OnError(int code) { +//// L.e("OnError!!!!" + code); +//// } +//// +//// public void OnDataReceive(AclasScale.St_Data data) { +//// if (data.m_iStatus == -1) { +//// L.e("data error"); +//// } else { +//// L.e("data:" + (data.m_iStatus == 0 ? "Unstable" : "Stable") + " weight:" + String.format("%.3f", data.m_fWeight) +//// + " price:" + String.format("%.3f", data.m_fPrice) + " total:" +//// + data.m_fTotal + " key:" + data.m_stKey.m_iValue + " str:" + data.m_stKey.m_strKey); +//// +//// } +//// } +//// +//// public void OnReadTare(float fVal, boolean bFlag) { +//// String string = bFlag ? String.valueOf(fVal) : "Error"; +//// L.d("data len OnReadTare:" + bFlag + " " + fVal); +//// } +//// }; +//// } +//// +//// private void openScale(String strAdd) { +//// try { +//// CloseDevice(); +//// int iType = 0;// 0:计重模式; 1:计价模式 +//// scale = new AclasScale(new File(strAdd), iType, listener); +//// scale.bLogFlag = true; +//// scale.open(); +////// m_strDeviceId = scale.GetId(); +//// } catch (SecurityException e) { +//// // TODO Auto-generated catch block +//// scale = null; +//// Toast.makeText(this, "OpenScale exception", Toast.LENGTH_SHORT).show(); +//// e.printStackTrace(); +//// } catch (Exception e) { +//// // TODO: handle exception +//// scale = null; +//// Toast.makeText(this, "OpenScale exception", Toast.LENGTH_SHORT).show(); +//// e.printStackTrace(); +//// } +//// if (scale != null) { +//// L.e("scale start run thread"); +//// scale.StartRead(); +//// } else { +//// L.e("scale null!!!!!!!!!!!!!!!!!!!"); +//// } +//// } +//// +//// private void CloseDevice() { +//// if (scale != null) { +//// scale.StopRead(); +//// scale.close(); +//// scale = null; +//// } +//// } +//} diff --git a/app/src/main/java/com/sw/st/ui/device/DevicePresenter.java b/app/src/main/java/com/sw/st/ui/device/DevicePresenter.java new file mode 100644 index 0000000..00ca227 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/device/DevicePresenter.java @@ -0,0 +1,77 @@ +package com.sw.st.ui.device; + + +import com.sw.st.api.SwService; +import com.sw.st.base.BasePresenter; +import com.sw.st.model.UserFaceModel; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; + +import java.util.ArrayList; + +import io.reactivex.disposables.Disposable; + + +public class DevicePresenter extends BasePresenter { + + public void relRest(String deviceMac, String restNo, String deviceDesc) { + SwService.relRest(deviceMac, restNo, deviceDesc) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(BindDeviceModel info) { + if (getView() != null) + getView().relRestSuccess(info.getRestName()); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().relRestFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + }); + } + + public void getUserFace() { + SwService.getUserFace() + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver>() { + + @Override + public void _onSubscribe(Disposable d) { + getView().showProgress("加载中..."); + } + + @Override + public void _onNext(ArrayList list) { + getView().getFaceFeatureSuccess(list); + } + + @Override + public void _onError(String errorMessage) { + getView().getFaceFeatureFail(errorMessage); + + getView().hideProgress(); + } + + @Override + public void _onComplete() { + getView().hideProgress(); + } + + }); + } +} diff --git a/app/src/main/java/com/sw/st/ui/device/DeviceView.java b/app/src/main/java/com/sw/st/ui/device/DeviceView.java new file mode 100644 index 0000000..c8a3d50 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/device/DeviceView.java @@ -0,0 +1,18 @@ +package com.sw.st.ui.device; + + +import com.sw.st.base.BaseView; +import com.sw.st.model.UserFaceModel; + +import java.util.ArrayList; + +public interface DeviceView extends BaseView { + + void relRestSuccess(String info); + + void relRestFail(String info); + + void getFaceFeatureSuccess(ArrayList list); + + void getFaceFeatureFail(String info); +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/FoodDetailAdapter.java b/app/src/main/java/com/sw/st/ui/foodinfo/FoodDetailAdapter.java new file mode 100644 index 0000000..7bff30f --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/FoodDetailAdapter.java @@ -0,0 +1,40 @@ +package com.sw.st.ui.foodinfo; + +import android.content.Context; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.sw.st.R; +import com.sw.st.model.ScalesFoodList; +import com.sw.st.model.UserNutritionModel; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class FoodDetailAdapter extends BaseQuickAdapter { + + Context context; + + public FoodDetailAdapter(@Nullable List listModels, Context context) { + super(R.layout.item_food_detail, listModels); + this.context = context; + } + + + @Override + protected void convert(@NotNull BaseViewHolder baseViewHolder, ScalesFoodList info) { + + baseViewHolder.setText(R.id.nameTv, info.getFoodName()); + baseViewHolder.setText(R.id.weightTv, info.getFoodWeight() + ""); +// if (info.getVipPriceSum() > 0) { +// baseViewHolder.setText(R.id.priceTv, info.getVipPriceSum() + "元"); +// } else { + baseViewHolder.setText(R.id.priceTv, info.getPriceSum() + ""); +// } + + } + + +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoActivity.java b/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoActivity.java new file mode 100644 index 0000000..9763c65 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoActivity.java @@ -0,0 +1,1355 @@ +package com.sw.st.ui.foodinfo; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Build; +import android.os.CountDownTimer; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RatingBar; +import android.widget.TextView; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; +import com.github.mikephil.charting.formatter.PercentFormatter; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.gyf.immersionbar.BarHide; +import com.gyf.immersionbar.ImmersionBar; +import com.lxj.xpopup.XPopup; +import com.lxj.xpopup.interfaces.OnConfirmListener; +import com.sw.st.R; +import com.sw.st.api.SwService; +import com.sw.st.application.App; +import com.sw.st.base.BaseActivity; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.MessageEvent; +import com.sw.st.model.NewFoodInfoModel; +import com.sw.st.model.RecommendValueModel; +import com.sw.st.model.ScalesFoodList; +import com.sw.st.model.UserNutritionModel; +import com.sw.st.net.ImageLoaderManager; +import com.sw.st.net.helper.Convert; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; +import com.sw.st.ui.addfood.AddFoodActivity; +import com.sw.st.ui.setting.FoodSettingActivity; +import com.sw.st.utils.AppUtil; +import com.sw.st.utils.GpioUtils; +import com.sw.st.utils.L; +import com.sw.st.utils.OnDoubleClickListener; +import com.sw.st.utils.PrefUtils; +import com.sw.st.utils.ToastUtils; +import com.sw.st.utils.mqtt.MqttIn; +import com.sw.st.utils.mqtt.service.MqttActionListener; +import com.sw.st.utils.update.AppVersionInfoBean; +import com.sw.st.utils.update.UpdateVersionShowDialog; +import com.sw.st.view.CustomImageView; +import com.wabon.wbintelligenthardwaresdk.api.BarCodeScanner; +import com.wabon.wbintelligenthardwaresdk.api.SensorScale; + +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.OnClick; + +import static com.sw.st.ui.foodinfo.FoodInfoPresenter.FOOD_WEIGHT_THRESHOLD; +import static com.sw.st.ui.setting.FoodSettingActivity.CURRENT_FOOD_DATA; +import static com.sw.st.ui.setting.FoodSettingActivity.REST_NAME; +import static com.sw.st.ui.setting.FoodSettingActivity.REST_NUM; + +public class FoodInfoActivity extends BaseActivity implements FoodInfoView { + private final int TOTAL_COLUMN_HEIGHT = 420; + private final int OFFSET_COLUMN_HEIGHT = 20; + private final int OFFSET_DIV_HEIGHT = 4; + @BindView(R.id.restNameTv) + TextView restNameTv; + @BindView(R.id.foodImg) + ImageView foodImg; + @BindView(R.id.foodNameTv) + TextView foodNameTv; + @BindView(R.id.lableRecyclerView) + RecyclerView lableRecyclerView; + @BindView(R.id.warnLableRecyclerView) + RecyclerView warnLableRecyclerView; + @BindView(R.id.priceTv) + TextView priceTv; + @BindView(R.id.vipPriceTv) + TextView vipPriceTv; + @BindView(R.id.vipPriceLayout) + LinearLayout vipPriceLayout; + @BindView(R.id.priceUnitTv) + TextView priceUnitTv; + @BindView(R.id.vipPriceUnitTv) + TextView vipPriceUnitTv; + + @BindView(R.id.currentFoodInfoLayout) + LinearLayout currentFoodInfoLayout; + @BindView(R.id.energyKcalTv) + TextView energyKcalTv; + @BindView(R.id.energyKcalTipsTv) + TextView energyKcalTipsTv; + @BindView(R.id.proteinProgressBar) + ProgressBar proteinProgressBar; + @BindView(R.id.proteinTv) + TextView proteinTv; + @BindView(R.id.fatProgressBar) + ProgressBar fatProgressBar; + @BindView(R.id.fatTv) + TextView fatTv; + @BindView(R.id.choProgressBar) + ProgressBar choProgressBar; + @BindView(R.id.choTv) + TextView choTv; + @BindView(R.id.foodIngredientsMainTv) + TextView foodIngredientsMainTv; + @BindView(R.id.foodIngredientAassistTv) + TextView foodIngredientAassistTv; + @BindView(R.id.foodSeasoningTv) + TextView foodSeasoningTv; + @BindView(R.id.totalWeightTv) + TextView totalWeightTv; + @BindView(R.id.totalKcalTv) + TextView totalKcalTv; + @BindView(R.id.dishCodeTv) + TextView dishCodeTv; + @BindView(R.id.takeFoodLayout) + LinearLayout takeFoodLayout; + @BindView(R.id.currentWeightTv) + TextView currentWeightTv; + @BindView(R.id.divKcalView) + View divKcalView; + @BindView(R.id.divTotalFoodView) + View divTotalFoodView; + @BindView(R.id.divVegetableView) + View divVegetableView; + @BindView(R.id.divMeatView) + View divMeatView; + // @BindView(R.id.ImgDishFood1) +// ImageView ImgDishFood1; +// @BindView(R.id.ImgDishFood2) +// ImageView ImgDishFood2; +// @BindView(R.id.ImgDishFood3) +// ImageView ImgDishFood3; +// @BindView(R.id.ImgDishFood4) +// ImageView ImgDishFood4; + @BindView(R.id.tv_date) + TextView tv_date; + @BindView(R.id.currentPriceTv) + TextView currentPriceTv; + @BindView(R.id.userTotalWeightTv) + TextView userTotalWeightTv; + @BindView(R.id.totalPriceTv) + TextView totalPriceTv; + @BindView(R.id.vipTotalPriceTv) + TextView vipTotalPriceTv; + @BindView(R.id.foodDetailRecyclerView) + RecyclerView foodDetailRecyclerView; + @BindView(R.id.totalFoodTv) + TextView totalFoodTv; + @BindView(R.id.totalMeatTv) + TextView totalMeatTv; + @BindView(R.id.totalVegetableTv) + TextView totalVegetableTv; + @BindView(R.id.warningView) + ImageView warningView; + @BindView(R.id.customKcalColumn) + CustomImageView customKcalColumn; + @BindView(R.id.customFoodColumn) + CustomImageView customFoodColumn; + @BindView(R.id.customMeatColumn) + CustomImageView customMeatColumn; + @BindView(R.id.customVegetableColumn) + CustomImageView customVegetableColumn; + private FoodInfoAdapter foodInfoAdapter; + private NewFoodInfoModel foodInfo; + private String foodId; + + private Context mContext; + private String mRestNum; + + private boolean isCurrent = true;//是否在菜品界面 + + private FoodDetailAdapter foodDetailAdapter; + + @Override + protected int provideContentViewId() { + return R.layout.activity_food_info; + } + + private static SensorScale mSensorScale; + private BarCodeScanner mBarCodeScanner; + + @Override + public void init() { + super.init(); + + mContext = this; + + mMessageHandler = new MyHandler(this); + + mBarCodeScanner = new BarCodeScanner(); + try { + mBarCodeScanner.openBarCodeScanner("/dev/ttyS2"); + } catch (Exception e) { + + } + mBarCodeScanner.addScannerListener(listener); + + EventBus.getDefault().register(this); + } + + + @Override + public void initView() { + ImmersionBar.with(this) + .hideBar(BarHide.FLAG_HIDE_BAR) + .statusBarAlpha(0f) + .statusBarDarkFont(true) + .statusBarColor(R.color.white) + .init(); + + Typeface typeface = Typeface.createFromAsset(getAssets(), "fonts/IMPACT.TTF"); +// foodNameTv.setTypeface(typeface); + priceTv.setTypeface(typeface); + vipPriceTv.setTypeface(typeface); + totalPriceTv.setTypeface(typeface); + + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(FoodInfoActivity.this); + linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + lableRecyclerView.setLayoutManager(linearLayoutManager); + lableRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + if (parent.getChildPosition(view) != (lableRecyclerView.getAdapter().getItemCount() - 1)) { + outRect.right = -16; + } + + } + }); + + warnLableRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + if (parent.getChildPosition(view) != (warnLableRecyclerView.getAdapter().getItemCount() - 1)) { + outRect.bottom = -16; + } + + } + }); + + foodDetailAdapter = new FoodDetailAdapter(null, mContext); + foodDetailRecyclerView.setAdapter(foodDetailAdapter); + } + + @Override + public void initData() { + mqttConnect(); + mRestNum = PrefUtils.getString(mContext, REST_NUM, ""); + resetFood(); + + appCheckUpdate(AppUtil.getAppVersionCode(mContext) + "", + AppUtil.getAppPackageName(mContext)); + + initDownTimer(); + + initScale(); + + closeGreenLight(); + closeRedLight(); + + mPresenter.getRecommendValue(); + } + + private void resetFood() { + String foodInfoStr = PrefUtils.getString(mContext, CURRENT_FOOD_DATA, ""); + if (AppUtil.isEmpty(foodInfoStr)) { + return; + } + + foodInfo = Convert.fromJson(foodInfoStr, new TypeToken() { + }.getType()); + foodId = foodInfo.getId(); +// foodInfo = (FoodInfoModel) getIntent().getSerializableExtra("foodInfo"); + String restName = PrefUtils.getString(this, REST_NAME, ""); + restNameTv.setText("欢迎光临" + restName); + + float kcal = AppUtil.formatFloat1(foodInfo.getEnergyKcal()); + energyKcalTv.setText(kcal + ""); + + if (kcal < 100) {//低 + energyKcalTipsTv.setText("低热量"); + energyKcalTipsTv.setBackgroundResource(R.drawable.bg_text_green); + } else if (kcal >= 100 && kcal < 250) {//一般 + energyKcalTipsTv.setText("一般热量"); + energyKcalTipsTv.setBackgroundResource(R.drawable.bg_text_yellow1); + } else if (kcal >= 250 && kcal < 400) {//相对较高 + energyKcalTipsTv.setText("高热量"); + energyKcalTipsTv.setBackgroundResource(R.drawable.bg_text_yellow2); + } else {//高 + energyKcalTipsTv.setText("较高热量"); + energyKcalTipsTv.setBackgroundResource(R.drawable.bg_text_red); + } + + foodNameTv.setText(foodInfo.getFoodName()); +// ImageLoaderManager.LoadRoundImage(mContext, foodInfo.getImg(), foodImg, 16); + ImageLoaderManager.LoadImage(mContext, foodInfo.getImg(), foodImg); + + priceTv.setText(AppUtil.formatDouble(foodInfo.getPrice())); + priceUnitTv.setText("单价\n元/" + foodInfo.getPriceUnit() + "克"); + double vipPrice = foodInfo.getVipPrice(); + if (vipPrice == 0) { + vipPriceLayout.setVisibility(View.GONE); + vipPriceTv.setVisibility(View.GONE); + } else { + vipPriceLayout.setVisibility(View.VISIBLE); + vipPriceTv.setVisibility(View.VISIBLE); + vipPriceTv.setText(AppUtil.formatDouble(vipPrice)); + vipPriceUnitTv.setText("会员价\n元/" + foodInfo.getPriceUnit() + "克"); + } + + List labels = foodInfo.getLabList(); + if (labels != null && labels.size() > 0) { + if (labels.size() > 4) { + labels = labels.subList(0, 4); + } + FoodLableAdapter foodLableAdapter = new FoodLableAdapter(labels, FoodInfoActivity.this); + lableRecyclerView.setAdapter(foodLableAdapter); +// lableRecyclerView.setVisibility(View.VISIBLE); + } else { + lableRecyclerView.setVisibility(View.GONE); + } + + List diseaseList = foodInfo.getDiseaseList(); + if (diseaseList != null && diseaseList.size() > 0) { + if (diseaseList.size() > 3) { + diseaseList = diseaseList.subList(0, 3); + } + FoodWarnLableAdapter foodWarnLableAdapter = new FoodWarnLableAdapter(diseaseList, FoodInfoActivity.this); + warnLableRecyclerView.setAdapter(foodWarnLableAdapter); +// warnLableRecyclerView.setVisibility(View.VISIBLE); + } else { + warnLableRecyclerView.setVisibility(View.GONE); + } + + double totalNutritionInfo = foodInfo.getCho() + foodInfo.getFat() + foodInfo.getProtein(); + int proteinProgress = (int) (foodInfo.getProtein() / totalNutritionInfo * 100); + int fatProgress = (int) (foodInfo.getFat() / totalNutritionInfo * 100); + int choProgress = 100 - proteinProgress - fatProgress; + proteinProgressBar.setProgress(proteinProgress); + fatProgressBar.setProgress(fatProgress); + choProgressBar.setProgress(choProgress); + proteinTv.setText(proteinProgress + "%"); + fatTv.setText(fatProgress + "%"); + choTv.setText(choProgress + "%"); + + foodIngredientsMainTv.setText(foodInfo.getMainIngredientList()); + foodIngredientAassistTv.setText(foodInfo.getAssistIngredientList()); + foodSeasoningTv.setText(foodInfo.getSeasoningList()); + } + + + @Override + public void initListener() { + tv_date.setOnTouchListener(new OnDoubleClickListener(() -> { + resetDevice(); + })); + } + + private void resetDevice() { + new XPopup.Builder(mContext) + .isViewMode(true) + .asConfirm("提示", "确定清空数据进行设备初始化?", + new OnConfirmListener() { + @Override + public void onConfirm() { + PrefUtils.clearAllData(mContext); + finish(); + } + }) + .show(); + } + + @Override + protected FoodInfoPresenter createPresenter() { + return new FoodInfoPresenter(); + } + + + @OnClick({R.id.foodImg, R.id.foodNameTv, R.id.energyKcalTv, R.id.totalWeightTv}) + public void onViewClicked(View view) { + switch (view.getId()) { + case R.id.foodImg: + Intent i = new Intent(mContext, AddFoodActivity.class); + i.putExtra("laseWeight", originalWeight); + startActivity(i); + break; + case R.id.foodNameTv: + startActivity(new Intent(this, FoodSettingActivity.class)); + break; + case R.id.totalWeightTv: + break; + case R.id.energyKcalTv: + break; + } + } + + + @Override + protected void afterRequestPermission(int requestCode, boolean isAllGranted) { + + } + +// @Override +// public void getTokenSuccess(String info) { +// String token = info; +// if (!TextUtils.isEmpty(token)) { +// HttpHeaders httpHeaders = new HttpHeaders(); +// httpHeaders.headersMap.put("X-Access-Token", token); +// OkGo.getInstance().addCommonHeaders(httpHeaders); +// } +// } +// +// @Override +// public void getTokenFail(String info) { +// +// } + + + private CountDownTimer mCountDownTimer; + private final long MILLIS_IN_FUTURE = 1000 * 5; + private boolean isLight = false; + + private void initDownTimer() { + mCountDownTimer = new CountDownTimer(MILLIS_IN_FUTURE, 500) { + @Override + public void onTick(long millisUntilFinished) { + openRedLight(); + isLight = !isLight; + } + + @Override + public void onFinish() { + isLight = false; + closeRedLight(); + } + }; + } + + private int lastFoodPosition = 0;//标记自动滑动位置 + private int lastFoodWeight = -1; + + private void setWeightData(int firstWeight, int deviceWeight) { + + int weight = firstWeight - deviceWeight; + if (weight < 0) {//拿出的重量小于放回的重量时 + weight = 0; + } + currentWeightTv.setText(weight + ""); + if (foodInfo == null) { + return; + } +// foodNameTv.setText("" + deviceWeight); + float kcal = AppUtil.formatFloat1((foodInfo.getEnergyKcal() * weight / 100)); + + if (currentUserModel == null) { + if (weight > 10 && isCurrent && deviceWeight > 0) { + openRedLight(); + if (mCountDownTimer != null) { + mCountDownTimer.start(); + } + this.firstWeight = this.deviceWeight; + } + return; + } + if (lastFoodWeight == weight) {//防止数据重复,不停刷新界面,影响性能 + return; + } + try { + double vegetable = weight * foodInfo.getVegetable() / 100 + + currentUserModel.getVegetable(); + double meat = weight * foodInfo.getMeal() / 100 + weight * foodInfo.getSoyaNew() / 100 + + currentUserModel.getMeal() + currentUserModel.getSoyaNew(); + double fruits = weight * foodInfo.getFruits() / 100 + + currentUserModel.getFruits(); + double grain = weight * foodInfo.getGrain() / 100 + weight * foodInfo.getMixedBeans() / 100 + + currentUserModel.getGrain() + currentUserModel.getMixedBeans(); + double fruitsAndVeg = fruits + vegetable; + + totalFoodTv.setText(AppUtil.formatDouble1(grain) + "克"); + totalMeatTv.setText(AppUtil.formatDouble1(meat) + "克"); + totalVegetableTv.setText(AppUtil.formatDouble1(fruitsAndVeg) + "克"); + +// int mainFoodCommentMax = ; +// int meatCommentMax = ; +// int fruitsAndVegCommentMax = ; + + int columnMax = AppUtil.getMaxInt(Integer.parseInt(mainRecommendArr[mainRecommendArr.length - 1]), + Integer.parseInt(meatRecommendArr[meatRecommendArr.length - 1]), + Integer.parseInt(fruitsAndVegRecommendArr[fruitsAndVegRecommendArr.length - 1])); + + boolean isFoodWarning = false; + int foodTotal = (int) (grain / columnMax * 100 * TOTAL_COLUMN_HEIGHT / 100); + if (foodTotal > OFFSET_DIV_HEIGHT) { + divTotalFoodView.setVisibility(View.GONE); + } else { + divTotalFoodView.setVisibility(View.VISIBLE); + } + if (foodTotal > 0) { + foodTotal += OFFSET_COLUMN_HEIGHT; + } + customFoodColumn.setCustomHeight(foodTotal > TOTAL_COLUMN_HEIGHT ? TOTAL_COLUMN_HEIGHT : foodTotal, true); + if (grain < Integer.parseInt(mainRecommendArr[0])) { + customFoodColumn.setImageDrawable(getDrawable(R.drawable.img_gray_column)); + isFoodWarning = false; + } else if (grain > Integer.parseInt(mainRecommendArr[0]) && grain < Integer.parseInt(mainRecommendArr[1])) { + customFoodColumn.setImageDrawable(getDrawable(R.drawable.img_green_column)); + isFoodWarning = false; + } else if (grain > Integer.parseInt(mainRecommendArr[1]) && grain < Integer.parseInt(mainRecommendArr[2])) { + customFoodColumn.setImageDrawable(getDrawable(R.drawable.img_yellow_column)); + isFoodWarning = false; + } else if (grain > Integer.parseInt(mainRecommendArr[2]) && grain < Integer.parseInt(mainRecommendArr[3])) { + customFoodColumn.setImageDrawable(getDrawable(R.drawable.img_orange_column)); + isFoodWarning = true; + } else { + customFoodColumn.setImageDrawable(getDrawable(R.drawable.img_red_column)); + isFoodWarning = true; + } + + boolean isMeatWarning = false; + int meatTotal = (int) (meat / columnMax * 100 * TOTAL_COLUMN_HEIGHT / 100); + if (meatTotal > OFFSET_DIV_HEIGHT) { + divMeatView.setVisibility(View.GONE); + } else { + divMeatView.setVisibility(View.VISIBLE); + } + if (meatTotal > 0) { + meatTotal += OFFSET_COLUMN_HEIGHT; + } + customMeatColumn.setCustomHeight(meatTotal > TOTAL_COLUMN_HEIGHT ? TOTAL_COLUMN_HEIGHT : meatTotal, true); + if (meat < Integer.parseInt(meatRecommendArr[0])) { + customMeatColumn.setImageDrawable(getDrawable(R.drawable.img_gray_column)); + isMeatWarning = false; + } else if (meat > Integer.parseInt(meatRecommendArr[0]) && meat < Integer.parseInt(meatRecommendArr[1])) { + customMeatColumn.setImageDrawable(getDrawable(R.drawable.img_green_column)); + isMeatWarning = false; + } else if (meat > Integer.parseInt(meatRecommendArr[1]) && meat < Integer.parseInt(meatRecommendArr[2])) { + customMeatColumn.setImageDrawable(getDrawable(R.drawable.img_yellow_column)); + isMeatWarning = false; + } else if (meat > Integer.parseInt(meatRecommendArr[2]) && meat < Integer.parseInt(meatRecommendArr[3])) { + customMeatColumn.setImageDrawable(getDrawable(R.drawable.img_orange_column)); + isMeatWarning = true; + } else { + customMeatColumn.setImageDrawable(getDrawable(R.drawable.img_red_column)); + isMeatWarning = true; + } + + boolean isVegetableWarning = false; + int vegetableTotal = (int) (fruitsAndVeg / columnMax * 100 * TOTAL_COLUMN_HEIGHT / 100); + if (vegetableTotal > OFFSET_DIV_HEIGHT) { + divVegetableView.setVisibility(View.GONE); + } else { + divVegetableView.setVisibility(View.VISIBLE); + } + if (vegetableTotal > 0) { + vegetableTotal += OFFSET_COLUMN_HEIGHT; + } + customVegetableColumn.setCustomHeight(vegetableTotal > TOTAL_COLUMN_HEIGHT ? TOTAL_COLUMN_HEIGHT : vegetableTotal, true); + if (fruitsAndVeg < Integer.parseInt(fruitsAndVegRecommendArr[0])) { + customVegetableColumn.setImageDrawable(getDrawable(R.drawable.img_gray_column)); + isVegetableWarning = false; + } else if (fruitsAndVeg > Integer.parseInt(fruitsAndVegRecommendArr[0]) && fruitsAndVeg < Integer.parseInt(fruitsAndVegRecommendArr[1])) { + customVegetableColumn.setImageDrawable(getDrawable(R.drawable.img_green_column)); + isVegetableWarning = false; + } else if (fruitsAndVeg > Integer.parseInt(fruitsAndVegRecommendArr[1]) && fruitsAndVeg < Integer.parseInt(fruitsAndVegRecommendArr[2])) { + customVegetableColumn.setImageDrawable(getDrawable(R.drawable.img_yellow_column)); + isVegetableWarning = false; + } else if (fruitsAndVeg > Integer.parseInt(fruitsAndVegRecommendArr[2]) && fruitsAndVeg < Integer.parseInt(fruitsAndVegRecommendArr[3])) { + customVegetableColumn.setImageDrawable(getDrawable(R.drawable.img_orange_column)); + isVegetableWarning = true; + } else { + customVegetableColumn.setImageDrawable(getDrawable(R.drawable.img_red_column)); + isVegetableWarning = true; + } + + +// String vegetableRecommend = currentUserModel.getVegetableRecommend(); +// String[] vegetableRecommendArr = vegetableRecommend.split("-"); +// double vegetableRecommendMin = Double.parseDouble(vegetableRecommendArr[0]); +// double vegetableRecommendMax = Double.parseDouble(vegetableRecommendArr[1]); +// if (vegetable == 0) { +// ImgDishFood1.setImageResource(R.mipmap.img_dish_food1_normal); +// } else if (vegetable < vegetableRecommendMin) { +// ImgDishFood1.setImageResource(R.mipmap.img_dish_food1_normal); +// } else if (vegetable > vegetableRecommendMax) { +// ImgDishFood1.setImageResource(R.mipmap.img_dish_food1_highlight); +// } else { +// ImgDishFood1.setImageResource(R.mipmap.img_dish_food1); +// } +// +// String meatRecommend = currentUserModel.getMealRecommend(); +// String[] meatRecommendArr = meatRecommend.split("-"); +// double meatRecommendMin = Double.parseDouble(meatRecommendArr[0]); +// double meatRecommendMax = Double.parseDouble(meatRecommendArr[1]); + +// +// String fruitsRecommend = currentUserModel.getFruitsRecommend(); +// String[] fruitsRecommendArr = fruitsRecommend.split("-"); +// double fruitsRecommendMin = Double.parseDouble(fruitsRecommendArr[0]); +// double fruitsRecommendMax = Double.parseDouble(fruitsRecommendArr[1]); +// if (fruits == 0) { +// ImgDishFood3.setImageResource(R.mipmap.img_dish_food3_normal); +// } else if (fruits < fruitsRecommendMin) { +// ImgDishFood3.setImageResource(R.mipmap.img_dish_food3_normal); +// } else if (fruits > fruitsRecommendMax) { +// ImgDishFood3.setImageResource(R.mipmap.img_dish_food3_highlight); +// } else { +// ImgDishFood3.setImageResource(R.mipmap.img_dish_food3); +// } +// +// String grainRecommend = currentUserModel.getGrainRecommend(); +// String[] grainRecommendArr = grainRecommend.split("-"); +// double grainRecommendMin = Double.parseDouble(grainRecommendArr[0]); +// double grainRecommendMax = Double.parseDouble(grainRecommendArr[1]); + + + foodDetailAdapter.setList(currentUserModel.getScalesFoodOrderListVOList()); + + ScalesFoodList scalesFoodList = new ScalesFoodList(); + scalesFoodList.setFoodWeight(weight); + scalesFoodList.setFoodName(foodInfo.getFoodName()); + scalesFoodList.setFoodId(foodInfo.getId()); + + float totalKcal = kcal + currentUserModel.getHeatKcal(); + + boolean isKcalWarning = false; + totalKcalTv.setText(AppUtil.formatFloat1(totalKcal) + "千卡"); + int kcalCommentMax = Integer.parseInt(heatRecommendArr[heatRecommendArr.length - 1]); + int kcalHeight = (int) (totalKcal / kcalCommentMax * 100 * TOTAL_COLUMN_HEIGHT / 100); + if (kcalHeight > OFFSET_DIV_HEIGHT) { + divKcalView.setVisibility(View.GONE); + } else { + divKcalView.setVisibility(View.VISIBLE); + } + if (kcalHeight > 0) { + kcalHeight += OFFSET_COLUMN_HEIGHT; + } + customKcalColumn.setCustomHeight(kcalHeight > TOTAL_COLUMN_HEIGHT ? TOTAL_COLUMN_HEIGHT : kcalHeight, true); + if (totalKcal < Integer.parseInt(heatRecommendArr[0])) { + customKcalColumn.setImageDrawable(getDrawable(R.drawable.img_gray_column)); + isKcalWarning = false; + } else if (totalKcal > Integer.parseInt(heatRecommendArr[0]) && totalKcal < Integer.parseInt(heatRecommendArr[1])) { + customKcalColumn.setImageDrawable(getDrawable(R.drawable.img_green_column)); + isKcalWarning = false; + } else if (totalKcal > Integer.parseInt(heatRecommendArr[1]) && totalKcal < Integer.parseInt(heatRecommendArr[2])) { + customKcalColumn.setImageDrawable(getDrawable(R.drawable.img_yellow_column)); + isKcalWarning = false; + } else if (totalKcal > Integer.parseInt(heatRecommendArr[2]) && totalKcal < Integer.parseInt(heatRecommendArr[3])) { + customKcalColumn.setImageDrawable(getDrawable(R.drawable.img_orange_column)); + isKcalWarning = true; + } else { + customKcalColumn.setImageDrawable(getDrawable(R.drawable.img_red_column)); + isKcalWarning = true; + } + + if (isKcalWarning) { + showWarningView(); + } else { + hideWarningView(); + } + if (isVegetableWarning) { + customVegetableColumn.startAlphaAnimation(); + } else { + customVegetableColumn.cancelAlphaAnimation(); + } + if (isFoodWarning) { + customFoodColumn.startAlphaAnimation(); + } else { + customFoodColumn.cancelAlphaAnimation(); + } + if (isMeatWarning) { + customMeatColumn.startAlphaAnimation(); + } else { + customMeatColumn.cancelAlphaAnimation(); + } + + userTotalWeightTv.setText(weight + currentUserModel.getFoodWeight() + ""); + + float price = 0; + float vipPrice = 0; + if (weight < FOOD_WEIGHT_THRESHOLD) {//重量大于阈值计算费用 + currentPriceTv.setText(price + ""); + totalPriceTv.setText(AppUtil.formatFloat2(price + currentUserModel.getPrice()) + ""); + vipTotalPriceTv.setText(AppUtil.formatFloat2(vipPrice + currentUserModel.getVipPrice()) + ""); + } else { + price = AppUtil.formatFloat2((float) (foodInfo.getPrice() / foodInfo.getPriceUnit() * weight)); + vipPrice = AppUtil.formatFloat2((float) (foodInfo.getVipPrice() / foodInfo.getPriceUnit() * weight)); + currentPriceTv.setText(price + ""); + totalPriceTv.setText(AppUtil.formatFloat2(price + currentUserModel.getPrice()) + ""); + vipTotalPriceTv.setText(AppUtil.formatFloat2(vipPrice + currentUserModel.getVipPrice()) + ""); + + scalesFoodList.setPriceSum(price); + scalesFoodList.setVipPriceSum(vipPrice); + foodDetailAdapter.addData(scalesFoodList); + } + vipTotalPriceTv.setText(AppUtil.formatFloat2(price + currentUserModel.getPrice()) + ""); + + if (currentUserModel.getVipPrice() == 0 && vipPrice == 0) { + vipTotalPriceTv.setVisibility(View.GONE); + + } else { + if (vipPrice > 0 && currentUserModel.getVipPrice() == 0) { + vipTotalPriceTv.setText(AppUtil.formatFloat2(vipPrice + currentUserModel.getPrice()) + ""); + } +// vipTotalPriceTv.setVisibility(View.VISIBLE); + } + if (lastFoodPosition != foodDetailAdapter.getItemCount() - 1) { + lastFoodPosition = foodDetailAdapter.getItemCount() - 1; + foodDetailRecyclerView.scrollToPosition(lastFoodPosition); + } + } catch (Exception e) { + L.e(e.getMessage()); + } + } + + private UserNutritionModel currentUserModel; + + @Override + public void getUserNutritionDataSuccess(UserNutritionModel userModel) { + if (userModel == null) { + return; + } + openGreenLight(); + this.currentUserModel = userModel; + runOnUiThread(new Runnable() { + @Override + public void run() { + dishCodeTv.setText("餐盘码NO." + plateId); + takeFoodLayout.setVisibility(View.VISIBLE); + foodDetailRecyclerView.setVisibility(View.VISIBLE); + currentFoodInfoLayout.setVisibility(View.GONE); + + setWeightData(0, 0); + saveLog(restNameTv.getText().toString(), + "菜品重量:" + deviceWeight); + } + }); + } + + + private boolean isOpen = false; + + @Override + public void getUserNutritionDataFail(String info) { + if ("餐盘未绑定".equals(info)) { + openRedLight(); + new CountDownTimer(1000 * 2, 200) { + @Override + public void onTick(long millisUntilFinished) { + openRedLight(); + isOpen = !isOpen; + } + + @Override + public void onFinish() { + isOpen = false; + closeRedLight(); + } + }.start(); + } + ToastUtils.showToast(mContext, info, 2000); + } + + + /** + * 创建就餐记录 + */ + private void createRecord() { + isOut = true; + plateTime = 0; + autoSubmitCount = 0; + lastFoodPosition = 0; + lastFoodWeight = -1; + int takeWeight = firstWeight - deviceWeight; + + if (takeWeight > 0 + && currentUserModel != null) { + mPresenter.createRecord(plateId, + foodId, + mRestNum, + takeWeight, + deviceWeight); + } + + saveLog("out", + restNameTv.getText().toString(), + "菜品重量:" + deviceWeight, + "取菜量:" + takeWeight); + + runOnUiThread(() -> { + currentFoodInfoLayout.setVisibility(View.VISIBLE); + takeFoodLayout.setVisibility(View.GONE); + foodDetailRecyclerView.setVisibility(View.GONE); + resetCustomColumn(); + hideWarningView(); + }); + + plateId = ""; + firstWeight = deviceWeight; + currentUserModel = null; + + closeGreenLight(); + + } + + private void resetCustomColumn() { + customKcalColumn.setCustomHeight(0, false); + customFoodColumn.setCustomHeight(0, false); + customVegetableColumn.setCustomHeight(0, false); + customMeatColumn.setCustomHeight(0, false); + } + + @Override + public void createRecordSuccess(String info) { + + } + + @Override + public void createRecordFail(String info) { + + } + + @Override + public void addFoodWeightSuccess(String info) { + firstWeight = deviceWeight; + } + + @Override + public void addFoodWeightFail(String info) { + + } + + @Override + public void saveMarginWeightSuccess(String info) { + + } + + @Override + public void saveMarginWeightFail(String info) { + + } + + @Override + public void getFoodInfoSuccess(ArrayList info) { + if (info != null && info.size() > 0) { + String currentFoodInfoStr = Convert.toJson(info.get(0)); + L.e(currentFoodInfoStr); + PrefUtils.setString(FoodInfoActivity.this, CURRENT_FOOD_DATA, currentFoodInfoStr); + resetFood(); + } + } + + @Override + public void getFoodInfoFail(String info) { + + } + + private String[] heatRecommendArr; + private String[] mainRecommendArr; + private String[] fruitsAndVegRecommendArr; + private String[] meatRecommendArr; + + @Override + public void getRecommendValueSuccess(RecommendValueModel info) { + heatRecommendArr = info.getHeat().split(","); + mainRecommendArr = info.getMain().split(","); + fruitsAndVegRecommendArr = info.getFruits().split(","); + meatRecommendArr = info.getMeatAndEggs().split(","); + } + + @Override + public void getRecommendValueFail(String info) { + + } + + @Override + public void showProgress(String tipString) { + + } + + @Override + public void hideProgress() { + + } + + @Override + protected void onResume() { + super.onResume(); + isCurrent = true; + firstWeight = deviceWeight; + String currentFoodStr = PrefUtils.getString(mContext, CURRENT_FOOD_DATA, ""); + mRestNum = PrefUtils.getString(mContext, REST_NUM, ""); + if (!AppUtil.isEmpty(currentFoodStr)) { + resetFood(); + } else { + startActivity(new Intent(this, FoodSettingActivity.class)); + } + } + + @Override + protected void onPause() { + super.onPause(); + isCurrent = false; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + L.e("FoodInfoActivity onDestroy"); + try { + mSensorScale.closeScale(); + } catch (Exception e) { + e.printStackTrace(); + } + mHandler.removeCallbacks(heartBeatRunnable); + + App.exit(); + EventBus.getDefault().unregister(this); + } + + private long plateTime = 0;//餐盘读取时间,超过一段时间如果信息未提交,自动提交,防止结束标识未识别 + private String plateId = ""; + private boolean isOut = true;//是否已经离开,防止扫码获取用户信息之前直接离开 + BarCodeScanner.ScannerListener listener = new BarCodeScanner.ScannerListener() { + @Override + public void scanResult(String result) { + plateTime = java.lang.System.currentTimeMillis(); + + L.e("餐盘信息:" + result); + result = result.replaceAll("\r|\n", ""); + if (result.contains("left") || + (!AppUtil.isEmpty(plateId) && !result.equals(plateId))) { + createRecord(); + } else if (AppUtil.isEmpty(plateId)) { + saveLog("in", plateId, + "菜品重量:" + deviceWeight); + if (isOut) { + isOut = false; + + plateId = result; + firstWeight = deviceWeight; + mPresenter.getUserNutritionData(foodId, mRestNum, plateId); + + mPresenter.submitTakeFoodState(mRestNum, plateId); +// runOnUiThread(new Runnable() { +// @Override +// public void run() { +// dishCodeTv.setText("餐盘码NO." + plateId); +// takeFoodLayout.setVisibility(View.VISIBLE); +// currentFoodInfoLayout.setVisibility(View.GONE); +// +// setWeightData(0, 0); +// saveLog(restNameTv.getText().toString(), +// "菜品重量:" + deviceWeight); +// } +// }); + } + } + + } + + @Override + public void failed(String errMsg) { + L.e(errMsg); + } + }; + + private int deviceWeight = 0; + private int firstWeight = 0; + private int lastWeight = 0; + + private int originalWeight = 0; + private int autoSubmitCount = 0; + private int tablewareWeight = 0; + + private boolean isFirstReadWeight = true; + + public static void weightZero() { + mSensorScale.zero(() -> { + L.e("onZero=======================End"); + MessageEvent eventModel = new MessageEvent(MessageEvent.MESSAGE_EVENT_ZERO, "zero ok"); + EventBus.getDefault().post(eventModel); + L.e("zero ok"); + }); + } + + public static void weightTare() { + mSensorScale.tare(() -> { + }); + } + + /** + * 初始化称体 + */ + private void initScale() { + mSensorScale = new SensorScale(new SensorScale.OnScaleResult() { + @Override + public void readWeight(int state, double value) { + String stateStr = ""; + switch (state) { + case SensorScale.STATE_STABLE: + stateStr = "稳定"; + break; + case SensorScale.STATE_UNSTABLE: + stateStr = "不稳定"; + return; + case SensorScale.STATE_OVER_WEIGHT: + stateStr = "量程溢出"; + return; + } + int weight = (int) (value * 1000); + if (lastWeight != weight) { + lastWeight = weight; + return; + } + + if (!isCurrent) { + MessageEvent eventModel = new MessageEvent(MessageEvent.MESSAGE_WEIGHT_INFO, "重量信息"); + eventModel.setWeight(weight); + EventBus.getDefault().post(eventModel); + } + if (foodInfo == null) { + return; + } +// if (foodInfo.getStFoodInfoSetting() != null +// && foodInfo.getStFoodInfoSetting().isTablewareStatus()) { +// if (tablewareWeight != foodInfo.getStFoodInfoSetting().getTablewareWeight()) { +// tablewareWeight = foodInfo.getStFoodInfoSetting().getTablewareWeight(); +// originalWeight = weight - tablewareWeight; +// deviceWeight = originalWeight; +// return; +// } +// tablewareWeight = foodInfo.getStFoodInfoSetting().getTablewareWeight(); +// originalWeight = weight - tablewareWeight; +// } else { + originalWeight = weight; +// } + + if (currentUserModel == null && + foodInfo != null && + autoSubmitCount >= 5 * 10) {//10秒无变化,提交数据 + autoSubmitCount = 0; + mPresenter.saveMarginWeight(foodId, mRestNum, originalWeight); + } else { + autoSubmitCount++; + } + + if (plateTime != 0 && + java.lang.System.currentTimeMillis() - plateTime > 2000) {//餐盘离开超过2秒数据未提交,自动提交 + L.e("超过上传时间阈值"); + if ("您好,欢迎就餐".equals(restNameTv.getText().toString())) { + plateTime = 0; + return; + } + L.e("上传数据"); + createRecord(); + } + + +// if (isCurrent +// && (originalWeight - deviceWeight) > +// (foodInfo.getStFoodInfoSetting().getAddFoodWeight() > 0 ? foodInfo.getStFoodInfoSetting().getAddFoodWeight() : 200) +// && currentUserModel == null) {//大于200 认为是加菜 +// mPresenter.addFoodTotalWeight(foodInfo.getId(), +// 2, originalWeight - deviceWeight, mRestNum); +// } + deviceWeight = originalWeight; + runOnUiThread(() -> { + totalWeightTv.setText(formatWeight(originalWeight)); + setWeightData(firstWeight, deviceWeight); + }); + } + + @Override + public void readIdentify(int rate) { + L.e("读取鉴别率成功=" + String.valueOf(rate)); + } + + @Override + public void fail(int errCode) { + String str = ""; + switch (errCode) { + case SensorScale.ERR_001: + str = "电子称未初始化"; + try { + Thread.sleep(300); + mSensorScale.startContinuousRead(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + break; + case SensorScale.ERR_002: + str = "开机零位异常"; + break; + case SensorScale.ERR_003: + str = "传感器故障"; + break; + case SensorScale.ERR_004: + str = "鉴别率超出范围"; + break; + } + L.e(str + "===" + errCode); + } + + }); + + + mMessageHandler.sendEmptyMessageDelayed(MESSAGE_OPEN_SCALE, 300); + + } + + private String formatWeight(int weight) { +// weight = weight < 0 ? 0 : weight; + if (weight > 1000) { + return "余量 " + AppUtil.formatFloat2(weight / (float) 1000) + " 千克"; + } else { + return "余量 " + weight + " 克"; + } + } + + + private void appCheckUpdate(String versionNo, + String packageName) { + SwService.appCheckUpdate(versionNo, packageName) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onNext(String responseData) { + try { + JSONObject jsonObject = new JSONObject(responseData); + int code = jsonObject.optInt("code"); + + if (code == 200) { + String result = jsonObject.optString("result"); + + JSONObject jsonData = new JSONObject(result); + int type = jsonData.optInt("type");//0不升级1升级2强制升级 + + if (type == 0) { + return; + } + + UpdateVersionShowDialog.show((FoodInfoActivity.this), + AppVersionInfoBean.parse(result)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public void _onError(String errorMessage) { + } + }); + } + + private void saveLog(String... info) { +// StringBuffer stringBuffer = new StringBuffer(); +// for (String s : info) { +// stringBuffer.append(s).append("==="); +// } +// stringBuffer.append((AppUtil.formatDateGetCurrentTime())).append("\n"); +// FileUtil.saveStrFile(stringBuffer.toString(), +// "log" + AppUtil.formatDateGetDay(java.lang.System.currentTimeMillis()) + ".txt", FILE_STR_PATH, true); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onMessageEvent(MessageEvent event) { + switch (event.getType()) { + case MessageEvent.MESSAGE_UPDATE_FOOD://更新菜品信息 + try { + JSONArray jsonArray = new JSONArray(event.getMessage()); + if (jsonArray != null && jsonArray.length() > 0) { + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String foodId = jsonObject.optString("foodId"); + if (foodInfo != null && + foodId.equals(foodInfo.getId())) { + int updateFlag = jsonObject.optInt("updateFlag"); + if (updateFlag == 0) {//删除 + PrefUtils.setString(mContext, CURRENT_FOOD_DATA, ""); + if (isCurrent) + startActivity(new Intent(this, FoodSettingActivity.class)); + } else if (updateFlag == 1) {//更新 + mPresenter.getRestInfoFoodsByFoodId(PrefUtils.getString(mContext, REST_NUM, ""), foodId); + } + } + } + } + } catch (JSONException e) { + e.printStackTrace(); + } + break; + case MESSAGE_GET_SCALE: + L.e("获取重量信息"); + mSensorScale.startContinuousRead(); + break; + } + } + + private boolean isStartHeart = false; + + private void mqttConnect() { + MqttIn.getInstance(this).connect(new MqttActionListener(MqttActionListener.TYPE.CONNECTMQTT) { + @Override + public void onSuccess(IMqttToken iMqttToken) { + super.onSuccess(iMqttToken);//,"send_to_weightscale_a20170e2-72aa-3230-89ae-86911ff9f74c" + MqttIn.getInstance(mContext).subscribe(new String[]{"sw_test"}, new int[]{0 }, + null, + new MqttActionListener(MqttActionListener.TYPE.SUBCRIBE) { + @Override + public void onSuccess(IMqttToken iMqttToken) { + super.onSuccess(iMqttToken); + L.e("订阅成功"); + if (!isStartHeart) { + mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE); + isStartHeart = true; + } + } + }); + } + }); + } + + private void openGreenLight() { + L.e("openGreenLight"); + GpioUtils.writeGpioValue(41, "1"); + } + + private void closeGreenLight() { + L.e("closeGreenLight"); + GpioUtils.writeGpioValue(41, "0"); + } + + private void openRedLight() { + L.e("openRedLight"); + GpioUtils.writeGpioValue(40, "1"); + } + + private void closeRedLight() { + L.e("closeRedLight"); + GpioUtils.writeGpioValue(40, "0"); + } + + + //-------------------------------------Handler消息传递------------------------------------------------ + private static MyHandler mMessageHandler; + + private static final int MESSAGE_OPEN_SCALE = 0;//连接智能秤 + private static final int MESSAGE_GET_SCALE = 1001;//读取重量 + + private class MyHandler extends Handler { + + //弱引用持有HandlerActivity , GC 回收时会被回收掉 + private WeakReference weakReference; + + public MyHandler(FoodInfoActivity activity) { + this.weakReference = new WeakReference(activity); + } + + @Override + public void handleMessage(Message msg) { + FoodInfoActivity activity = weakReference.get(); + super.handleMessage(msg); + if (null != activity) { + //执行业务逻辑 + switch (msg.arg1) { + case MESSAGE_OPEN_SCALE: + mSensorScale.openScale("/dev/ttyS7", 115200, open -> { + L.e("openScale===" + open); + if (open) { + try { + Thread.sleep(300); + } catch (InterruptedException e) { + e.printStackTrace(); + } + EventBus.getDefault().post(new MessageEvent(MESSAGE_GET_SCALE, + "startContinuousRead")); +// mSensorScale.startContinuousRead(); + } + }); + break; + } + } + } + } + + // -------------------------------------显示报警框------------------------------------------------ + private boolean isShowWarningView = false; + private AlphaAnimation alphaAnimation; + + private void showWarningView() { + if (!isShowWarningView) { + warningView.setVisibility(View.VISIBLE); + isShowWarningView = true; + alphaAnimation = new AlphaAnimation(0.6f, 0.1f); // 从不透明渐变到完全透明 + alphaAnimation.setDuration(500); // 设置动画持续时间,单位为毫秒 + alphaAnimation.setRepeatCount(AlphaAnimation.INFINITE); // 设置动画重复次数为无限(可选) + warningView.startAnimation(alphaAnimation); + customKcalColumn.startAlphaAnimation(); + } + } + + private void hideWarningView() { + isShowWarningView = false; + warningView.setVisibility(View.GONE); + customKcalColumn.cancelAlphaAnimation(); + if (alphaAnimation != null) { + alphaAnimation.cancel(); + } + } + + // -------------------------------------心跳检测------------------------------------------------ + private static final long HEART_BEAT_RATE = 10 * 1000;//每隔10秒进行一次对长连接的心跳检测 + private Handler mHandler = new Handler(); + private Runnable heartBeatRunnable = new Runnable() { + @Override + public void run() { + L.e("MQTT Service", "心跳包检测MQTT连接状态" + MqttIn.getInstance(mContext).isConnect()); + if (!MqttIn.getInstance(mContext).isConnect()) { + MqttIn.getInstance(mContext).reconnect(); + } + + //每隔一定的时间,对长连接进行一次心跳检测 + mHandler.postDelayed(this, HEART_BEAT_RATE); + } + }; +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoAdapter.java b/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoAdapter.java new file mode 100644 index 0000000..d647f7c --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoAdapter.java @@ -0,0 +1,39 @@ +package com.sw.st.ui.foodinfo; + +import android.content.Context; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.sw.st.R; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class FoodInfoAdapter extends BaseQuickAdapter { + + Context context; + + public FoodInfoAdapter(@Nullable List listModels, Context context) { + super(R.layout.item_food_info, listModels); + this.context = context; + } + + + @Override + protected void convert(@NotNull BaseViewHolder baseViewHolder, FoodNutritionModel info) { + if (getItemPosition(info) % 2 == 0) + baseViewHolder.setBackgroundColor(R.id.foodLayout, context.getResources().getColor(R.color.foodNutritionItemBg)); + String name = info.getName(); + baseViewHolder.setText(R.id.nameTv, name); + if (name.equals("钠")) { + baseViewHolder.setText(R.id.weightTv, info.getWeight() + "(mg)"); + } else { + baseViewHolder.setText(R.id.weightTv, info.getWeight() + "(g)"); + } + baseViewHolder.setText(R.id.nrvTv, info.getNrv()); + } + + +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoPresenter.java b/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoPresenter.java new file mode 100644 index 0000000..40fa174 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoPresenter.java @@ -0,0 +1,275 @@ +package com.sw.st.ui.foodinfo; + + +import com.google.gson.Gson; +import com.sw.st.api.SwService; +import com.sw.st.base.BasePresenter; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.RecommendValueModel; +import com.sw.st.model.ResponseData; +import com.sw.st.model.UserInfo; +import com.sw.st.model.UserNutritionModel; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; +import com.sw.st.utils.AppUtil; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import io.reactivex.disposables.Disposable; + +public class FoodInfoPresenter extends BasePresenter { +// public void getToken(String devId) { +// SwService.getToken(devId) +// .compose(RxSchedulersHelper.io_main()) +// .compose(RxResultHelper.handleJsonResponse()) +// .subscribe(new RxObserver() { +// @Override +// public void _onSubscribe(Disposable d) { +// } +// +// @Override +// public void _onNext(String info) { +// try { +// JSONObject jsonObject = new JSONObject(info); +// if (jsonObject.optInt("code") == 200) { +// String data = jsonObject.optString("result"); +// if (getView() != null) +// getView().getTokenSuccess(data); +// } +// } catch (JSONException e) { +// e.printStackTrace(); +// if (getView() != null) +// getView().getTokenFail("数据解析异常"); +// } +// +// } +// +// @Override +// public void _onError(String errorMessage) { +// if (getView() != null) +// getView().getTokenFail(errorMessage); +// } +// +// @Override +// public void _onComplete() { +// } +// }); +// } + + public void getUserNutritionData(String foodId, + String eaId, + String plateNumber) { + + SwService.getUserNutritionData(foodId, eaId, plateNumber) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + + @Override + public void _onSubscribe(Disposable d) { + if (getView() != null) + getView().showProgress("加载中..."); + } + + @Override + public void _onNext(String info) { + if (AppUtil.isEmpty(info)) { + if (getView() != null) + getView().getUserNutritionDataFail("数据为空"); + return; + } + try { + JSONObject jsonObject = new JSONObject(info); + int code = jsonObject.optInt("code"); + if (code == 200) { + UserNutritionModel userModel = new Gson().fromJson(jsonObject.optString("data"), + UserNutritionModel.class); + if (getView() != null) + getView().getUserNutritionDataSuccess(userModel); + } else { + if (getView() != null) + getView().getUserNutritionDataFail(jsonObject.optString("msg")); + } + } catch (JSONException e) { + e.printStackTrace(); + } + + + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) { + getView().getUserNutritionDataFail(errorMessage); + + getView().hideProgress(); + } + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + + }); + } + + private double lastWeight = 0; + + public void saveMarginWeight(String foodId, + String eaId, + double marginWeight) { + if (lastWeight == marginWeight || marginWeight < 0) { + return; + } + lastWeight = marginWeight; + SwService.saveMarginWeight(foodId, eaId, marginWeight) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + if (getView() != null) + getView().saveMarginWeightSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().saveMarginWeightFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + + public static final int FOOD_WEIGHT_THRESHOLD = 5;//取餐阈值,包含设置值 + + public void createRecord(String plateNumber, String foodId, String eaId, float intake, double residueWeight) { + if (intake < FOOD_WEIGHT_THRESHOLD) {//小于5g忽略 + return; + } + SwService.userEatFood(plateNumber, foodId, eaId, intake, residueWeight) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + if (getView() != null) + getView().createRecordSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().createRecordFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + + + public void getRestInfoFoodsByFoodId(String restId, String foodId) { + SwService.getRestInfoFoodsByFoodId(restId, foodId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver>() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(ArrayList info) { + if (getView() != null) + getView().getFoodInfoSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getFoodInfoFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + }); + } + + public void getRecommendValue() { + SwService.getRecommendValue() + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(RecommendValueModel info) { + if (getView() != null) + getView().getRecommendValueSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getRecommendValueFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + }); + } + + + public void submitTakeFoodState(String eaId, + String plateNumber) { + SwService.submitTakeFoodState(eaId, plateNumber) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + } + + @Override + public void _onError(String errorMessage) { + + } + + @Override + public void _onComplete() { + } + + }); + } +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoView.java b/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoView.java new file mode 100644 index 0000000..f334157 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/FoodInfoView.java @@ -0,0 +1,43 @@ +package com.sw.st.ui.foodinfo; + + +import com.sw.st.base.BaseView; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.RecommendValueModel; +import com.sw.st.model.UserInfo; +import com.sw.st.model.UserNutritionModel; +import com.sw.st.ui.setting.FoodListModel; + +import java.util.ArrayList; + +public interface FoodInfoView extends BaseView { + +// void getTokenSuccess(String info); +// +// void getTokenFail(String info); + + void getUserNutritionDataSuccess(UserNutritionModel userModel); + + void getUserNutritionDataFail(String info); + + void createRecordSuccess(String info); + + void createRecordFail(String info); + + void addFoodWeightSuccess(String info); + + void addFoodWeightFail(String info); + + void saveMarginWeightSuccess(String info); + + void saveMarginWeightFail(String info); + + void getFoodInfoSuccess(ArrayList info); + + void getFoodInfoFail(String info); + + void getRecommendValueSuccess(RecommendValueModel info); + + void getRecommendValueFail(String info); + +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/FoodLableAdapter.java b/app/src/main/java/com/sw/st/ui/foodinfo/FoodLableAdapter.java new file mode 100644 index 0000000..f851ec0 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/FoodLableAdapter.java @@ -0,0 +1,31 @@ +package com.sw.st.ui.foodinfo; + +import android.content.Context; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.sw.st.R; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class FoodLableAdapter extends BaseQuickAdapter { + + Context context; + + public FoodLableAdapter(@Nullable List strs, Context context) { + super(R.layout.item_lable_tv, strs); + this.context = context; + } + + + @Override + protected void convert(@NotNull BaseViewHolder baseViewHolder, String info) { + + baseViewHolder.setText(R.id.tv, info); + } + + +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/FoodNutritionModel.java b/app/src/main/java/com/sw/st/ui/foodinfo/FoodNutritionModel.java new file mode 100644 index 0000000..8b4106b --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/FoodNutritionModel.java @@ -0,0 +1,40 @@ +package com.sw.st.ui.foodinfo; + +public class FoodNutritionModel { + private String name; + private float weight; + private float nutritionWeight; + private String nrv; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public float getWeight() { + return weight; + } + + public void setWeight(float weight) { + this.weight = weight; + } + + public float getNutritionWeight() { + return nutritionWeight; + } + + public void setNutritionWeight(float nutritionWeight) { + this.nutritionWeight = nutritionWeight; + } + + public String getNrv() { + return nrv; + } + + public void setNrv(String nrv) { + this.nrv = nrv; + } +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/FoodWarnLableAdapter.java b/app/src/main/java/com/sw/st/ui/foodinfo/FoodWarnLableAdapter.java new file mode 100644 index 0000000..d6efbfe --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/FoodWarnLableAdapter.java @@ -0,0 +1,36 @@ +package com.sw.st.ui.foodinfo; + +import android.content.Context; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.sw.st.R; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class FoodWarnLableAdapter extends BaseQuickAdapter { + + Context context; + + public FoodWarnLableAdapter(@Nullable List strs, Context context) { + super(R.layout.item_food_warn_lable_tv, strs); + this.context = context; + } + +// @Override +// public int getItemCount() { +// return getItemCount() > 3 ? 3 : getItemCount(); +// } + + @Override + protected void convert(@NotNull BaseViewHolder baseViewHolder, String info) { + String arr[] = info.split("#"); + baseViewHolder.setText(R.id.lable, arr[0]); + baseViewHolder.setText(R.id.text, arr[1]); + } + + +} diff --git a/app/src/main/java/com/sw/st/ui/foodinfo/LimitedQueue.java b/app/src/main/java/com/sw/st/ui/foodinfo/LimitedQueue.java new file mode 100644 index 0000000..b45f7f9 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/foodinfo/LimitedQueue.java @@ -0,0 +1,21 @@ +package com.sw.st.ui.foodinfo; + +import java.util.LinkedList; + +public class LimitedQueue extends LinkedList { + private static final long serialVersionUID = 1L; + private final int size; + + public LimitedQueue(int size) { + this.size = size; + } + + @Override + public boolean add(E o) { + super.add(o); + while (size() > size) { + super.remove(); + } + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/ui/init/InitActivity.java b/app/src/main/java/com/sw/st/ui/init/InitActivity.java new file mode 100644 index 0000000..fb705ef --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/init/InitActivity.java @@ -0,0 +1,420 @@ +package com.sw.st.ui.init; + +import android.app.Dialog; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.os.CountDownTimer; +import android.os.SystemClock; +import android.text.TextUtils; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.gyf.immersionbar.BarHide; +import com.gyf.immersionbar.ImmersionBar; +import com.lxj.xpopup.XPopup; +import com.lxj.xpopup.core.BasePopupView; +import com.lxj.xpopup.interfaces.OnConfirmListener; +import com.lxj.xpopup.interfaces.XPopupCallback; +import com.lzy.okgo.OkGo; +import com.lzy.okgo.model.HttpHeaders; +import com.sw.st.R; +import com.sw.st.base.BaseActivity; +import com.sw.st.ui.foodinfo.FoodInfoActivity; +import com.sw.st.utils.AppUtil; +import com.sw.st.utils.BatteryChangeReceiver; +import com.sw.st.utils.FileUtil; +import com.sw.st.utils.InputMethod; +import com.sw.st.utils.L; +import com.sw.st.utils.OnDoubleClickListener; +import com.sw.st.utils.PrefUtils; +import com.sw.st.utils.SystemCtrlUtil; +import com.sw.st.utils.mqtt.MqttIn; +import com.sw.st.view.BindDiningRoomPopup; +import com.sw.st.view.CustomDialog; +import com.sw.st.view.IPEditText; + +import org.json.JSONException; +import org.json.JSONObject; + +import butterknife.BindView; +import butterknife.OnClick; +import cn.bingoogolapple.qrcode.zxing.QRCodeEncoder; + + +public class InitActivity extends BaseActivity + implements InitView { + // private final String LOCAL_SERVICE_ADDRESS = "http://kuaijiexi1234.gnway.cc"; + private final String LOCAL_SERVICE_ADDRESS = "http://192.168.10.173:9092"; +// private final String LOCAL_SERVICE_ADDRESS = "https://vip.shuziweidao.com"; + // private final String LOCAL_SERVICE_ADDRESS = "http://192.168.1.250/gateway/local"; +// private final String LOCAL_SERVICE_ADDRESS = "http://192.168.10.6/gateway/local"; + + private final String INTERNET_SERVICE_ADDRESS = "http://device.shuziweidao.com:8889"; + + private String TAG = "InitActivity"; + + @BindView(R.id.radioGroup) + RadioGroup radioGroup; + @BindView(R.id.welcomeLayout) + LinearLayout welcomeLayout; + @BindView(R.id.imgQr) + ImageView imgQr; + @BindView(R.id.radioButton1) + RadioButton radioButton1; + @BindView(R.id.radioButton2) + RadioButton radioButton2; + + public static String BASE_URL = ""; + + public static final String REST_NUM = "restId"; + private BindDiningRoomPopup customPopup; + + @Override + protected int provideContentViewId() { + return R.layout.activity_init; + } + + @Override + public void init() { + super.init(); + } + + + @Override + public void initView() { + ImmersionBar.with(InitActivity.this) + .hideBar(BarHide.FLAG_HIDE_BAR) + .init(); + + int netType = PrefUtils.getInt(mContext, NET_TYPE, 1); + switch (netType) { + case 1: + radioButton1.setChecked(true); + break; + case 2: + radioButton2.setChecked(true); + break; + } + } + + private CountDownTimer checkNetworkCountDownTimer; + private int checkNetworkCount = 0; + + @Override + public void initData() { + showProgress("网络连接中..."); + checkNetworkCountDownTimer = new CountDownTimer(1000 * 3, 1000) { + @Override + public void onTick(long millisUntilFinished) { + + } + + @Override + public void onFinish() { + L.e("检测网络连接状态" + AppUtil.isNetworkConnected(mContext)); + if (AppUtil.isNetworkConnected(mContext)) { + BASE_URL = PrefUtils.getString(mContext, "ip", ""); + if (!AppUtil.isEmpty(BASE_URL)) { + mPresenter.getToken(AppUtil.getUDID(mContext)); + } else { + welcomeLayout.setVisibility(View.GONE); + } + hideProgress(); + } else { + if (checkNetworkCount >= 10) { + showErrorDialog("网络出现错误!", R.mipmap.img_error_net); + welcomeLayout.setVisibility(View.GONE); + hideProgress(); + } else { + checkNetworkCount++; + checkNetworkCountDownTimer.start(); + } + } + + } + }; + checkNetworkCountDownTimer.start(); + + customPopup = new BindDiningRoomPopup(InitActivity.this); + Bitmap qrcode = QRCodeEncoder.syncEncodeQRCode("" + AppUtil.getUDID(mContext), 100); + imgQr.setImageBitmap(qrcode); + imgQr.setOnTouchListener(new OnDoubleClickListener(() -> { + resetDevice(); + })); + + String url = PrefUtils.getString(mContext, "crashUrl", ""); + String data = PrefUtils.getString(mContext, "crashData", ""); + L.e(url + "===" + data); + mPresenter.uploadCrashLog(mContext, url, data); + + + //当前无配置,默认写死 + +// HttpHeaders httpHeaders = new HttpHeaders(); +// httpHeaders.headersMap.put("Authorization", "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiZDU0M2E3ODI1ZjJhZThhYjJmMWQ3MWJmYTNjZjYzMGY2ZmRiM2JiOCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiOTgxNmM0ZWEtMzJmYi00ODIwLWE0NGQtOGM0ZmQ5NDU5Zjk2In0.HCeetHT7Z9GwmA1kgfYN41iS4ELw_eyv7J6FxK6EbXRfsx-2rA4ZQIJ9vi8nrAD97qouA3072v2PjBJ2ybiZbA"); +// OkGo.getInstance().addCommonHeaders(httpHeaders); +// +// +// BASE_URL = LOCAL_SERVICE_ADDRESS; +// PrefUtils.setString(mContext, "restName", "数味餐厅"); +// PrefUtils.setString(mContext, "restId", "99"); +// startActivity(new Intent(this, FoodInfoActivity.class)); + } + + private void resetDevice() { + new XPopup.Builder(mContext) + .isViewMode(true) + .asConfirm("提示", "确定清空数据进行设备初始化?", + new OnConfirmListener() { + @Override + public void onConfirm() { + new Thread() { + @Override + public void run() { + super.run(); + PrefUtils.clearAllData(mContext); + Glide.get(mContext).clearDiskCache(); + } + }.start(); + Glide.get(mContext).clearMemory(); + } + }) + .show(); + } + + + @Override + public void onStart() { + super.onStart(); + } + + @Override + public void onStop() { + super.onStop(); + } + + @Override + public void initListener() { + welcomeLayout.setOnClickListener(v -> { + welcomeLayout.setVisibility(View.GONE); + }); + } + + private final String NET_TYPE = "netType";//1内网 2外网 + + @OnClick({R.id.okButton}) + public void onViewClicked(View view) { + switch (view.getId()) { + case R.id.okButton: + if (!AppUtil.isNetworkConnected(mContext)) { + showErrorDialog("网络出现错误!", R.mipmap.img_error_net); + return; + } + switch (radioGroup.getCheckedRadioButtonId()) { + case R.id.radioButton1: + showProgress("加载中..."); + PrefUtils.setInt(mContext, NET_TYPE, 1); + BASE_URL = LOCAL_SERVICE_ADDRESS; + + mPresenter.getToken(AppUtil.getUDID(mContext)); + break; + case R.id.radioButton2: + showProgress("加载中..."); + PrefUtils.setInt(mContext, NET_TYPE, 2); + mPresenter.getBaseToken(INTERNET_SERVICE_ADDRESS, AppUtil.getUDID(mContext)); + break; + case -1: + showToast("请选择服务器"); + break; + } + break; + } + } + + + @Override + protected void onResume() { + super.onResume(); + } + + + @Override + protected InitPresenter createPresenter() { + return new InitPresenter(); + } + + @Override + protected void afterRequestPermission(int requestCode, boolean isAllGranted) { + + } + + + @Override + public void getTokenSuccess(String token) { + FileUtil.saveLog("getTokenSuccess", token, BASE_URL); + PrefUtils.setString(mContext, "ip", BASE_URL); + if (!TextUtils.isEmpty(token)) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.headersMap.put("Authorization", token); + OkGo.getInstance().addCommonHeaders(httpHeaders); + } +//=============================测试用=============================================== +// BASE_URL = LOCAL_SERVICE_ADDRESS; +// PrefUtils.setString(mContext, "restName", "数味餐厅"); +// PrefUtils.setString(mContext, "restId", "99"); +// startActivity(new Intent(this, FoodInfoActivity.class)); +//============================================================================ + + String devInfo = PrefUtils.getString(mContext, "devInfo", null); + if (AppUtil.isEmpty(devInfo)) { + mPresenter.getDeviceInfoByEquipmentId(AppUtil.getUDID(mContext)); + } else { + initDevice(devInfo); + } + + } + + @Override + public void getTokenFail(String info) { + FileUtil.saveLog("getTokenFail", info); + if (!AppUtil.isEmpty(info)) { + showErrorDialog("系统故障!", R.mipmap.img_error_system); + welcomeLayout.setVisibility(View.GONE); + } + hideProgress(); + } + + @Override + public void addDeviceInitSuccess(String result) { + mPresenter.getDeviceInfoByEquipmentId(AppUtil.getUDID(mContext)); + } + + @Override + public void addDeviceInitFail(String info) { + + } + + + @Override + public void getDeviceInfoSuccess(String info) { + L.e("mzf", info); + + PrefUtils.setString(mContext, "devInfo", info); + initDevice(info); + } + + @Override + public void getDeviceInfoFail(String info) { + showToast(info); + hideProgress(); + } + + + @Override + public void getBaseTokenSuccess(String token) { + if (!TextUtils.isEmpty(token)) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.headersMap.put("X-Access-Token", token); + OkGo.getInstance().addCommonHeaders(httpHeaders); + } + mPresenter.getServiceAddress(INTERNET_SERVICE_ADDRESS, AppUtil.getUDID(mContext)); + } + + @Override + public void getBaseTokenFail(String info) { + if (!AppUtil.isEmpty(info)) + showToast(info); + hideProgress(); + } + + @Override + public void getServiceAddressSuccess(String info) { + PrefUtils.setString(mContext, "devInfo", info); + + try { + JSONObject jsonObject = new JSONObject(info); + BASE_URL = jsonObject.optString("appPackageUrl"); + + mPresenter.getToken(AppUtil.getUDID(mContext)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public void getServiceAddressFail(String info) { + + } + + @Override + public void showProgress(String tipString) { + showWaitingDialog(tipString); + } + + @Override + public void hideProgress() { + hideWaitingDialog(); + } + + private void initDevice(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + PrefUtils.setString(mContext, "restName", jsonObject.optString("canteenName")); + PrefUtils.setString(mContext, "restId", jsonObject.optString("canteenId")); + + String mqttIp = jsonObject.optString("clientServerIp"); + String mqttPort = jsonObject.optString("zhstServerIp"); + MqttIn.getInstance(mContext).init(mqttIp, mqttPort); + + } catch (JSONException e) { + e.printStackTrace(); + } + + hideProgress(); + PrefUtils.setBoolean(mContext, "isInit", true); + startActivity(new Intent(this, FoodInfoActivity.class)); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + MqttIn.getInstance(mContext).destroy(); + } + + @Override + protected void onPause() { + super.onPause(); + } + + /** + * 显示错误提示框 + * + * @param tip + * @param resId + * @return + */ + public Dialog showErrorDialog(String tip, int resId) { + View view = View.inflate(this, R.layout.dialog_error, null); + TextView tvTip = view.findViewById(R.id.tvTip); + tvTip.setText(tip); + ImageView img = view.findViewById(R.id.img); + img.setImageResource(resId); + TextView tvBack = view.findViewById(R.id.tvBack); + + CustomDialog customDialog = new CustomDialog(this, view, R.style.MyDialog); + customDialog.show(); + customDialog.setCancelable(false); + tvBack.setOnClickListener(v -> { + customDialog.dismiss(); + }); + return customDialog; + } + +} diff --git a/app/src/main/java/com/sw/st/ui/init/InitPresenter.java b/app/src/main/java/com/sw/st/ui/init/InitPresenter.java new file mode 100644 index 0000000..ca1d5ac --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/init/InitPresenter.java @@ -0,0 +1,251 @@ +package com.sw.st.ui.init; + + +import android.content.Context; + +import com.sw.st.api.SwService; +import com.sw.st.application.App; +import com.sw.st.base.BasePresenter; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; +import com.sw.st.utils.AppUtil; +import com.sw.st.utils.PrefUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import io.reactivex.disposables.Disposable; + +public class InitPresenter extends BasePresenter { + public void getToken(String devId) { + SwService.getToken(devId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().getTokenSuccess(data); + } else { + if (getView() != null) + getView().getTokenFail(jsonObject.optString("msg")); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().getTokenFail("数据解析异常"); + } + + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getTokenFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().getTokenFail(null); + } + }); + } + + public void addDeviceInit(String devId, String packageName) { + SwService.addDeviceInit(devId, packageName) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().addDeviceInitSuccess(data); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().addDeviceInitFail("数据解析异常"); + } + + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().addDeviceInitFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + + + public void getDeviceInfoByEquipmentId(String devId) { + SwService.getDeviceInfoByEquipmentId(devId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().getDeviceInfoSuccess(data); + } else { + if (getView() != null) + getView().getDeviceInfoFail(jsonObject.optString("msg")); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().getDeviceInfoFail("数据解析异常"); + } + + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getDeviceInfoFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + + + public void getBaseToken(String url, String devId) { + SwService.getBaseToken(url, devId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().getBaseTokenSuccess(data); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().getBaseTokenFail("数据解析异常"); + } + + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getBaseTokenFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().getTokenFail(null); + } + }); + } + + public void getServiceAddress(String url, String devId) { + SwService.getServerAddress(url, devId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().getServiceAddressSuccess(data); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().getServiceAddressFail("数据解析异常"); + } + + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getServiceAddressFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + + public void uploadCrashLog(Context context, String url, String data) { + if (AppUtil.isEmpty(url) || AppUtil.isEmpty(data)) { + return; + } + SwService.uploadCrashLog(url, data) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + PrefUtils.setString(context, "crashUrl", ""); + PrefUtils.setString(context, "crashData", ""); + } + + @Override + public void _onError(String errorMessage) { + + } + + @Override + public void _onComplete() { + } + }); + } +} diff --git a/app/src/main/java/com/sw/st/ui/init/InitView.java b/app/src/main/java/com/sw/st/ui/init/InitView.java new file mode 100644 index 0000000..a3df173 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/init/InitView.java @@ -0,0 +1,27 @@ +package com.sw.st.ui.init; + + +import com.sw.st.base.BaseView; + +public interface InitView extends BaseView { + + void getTokenSuccess(String token); + + void getTokenFail(String info); + + void addDeviceInitSuccess(String token); + + void addDeviceInitFail(String info); + + void getDeviceInfoSuccess(String info); + + void getDeviceInfoFail(String info); + + void getBaseTokenSuccess(String token); + + void getBaseTokenFail(String info); + + void getServiceAddressSuccess(String info); + + void getServiceAddressFail(String info); +} diff --git a/app/src/main/java/com/sw/st/ui/login/FoodTagAdapter.java b/app/src/main/java/com/sw/st/ui/login/FoodTagAdapter.java new file mode 100644 index 0000000..0000aca --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/FoodTagAdapter.java @@ -0,0 +1,30 @@ +package com.sw.st.ui.login; + +import android.content.Context; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.sw.st.R; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class FoodTagAdapter extends BaseQuickAdapter { + + Context context; + + public FoodTagAdapter(@Nullable List listModels, Context context) { + super(R.layout.item_food_tag_tv, listModels); + this.context = context; + } + + + @Override + protected void convert(@NotNull BaseViewHolder baseViewHolder, String info) { + baseViewHolder.setText(R.id.tv, "#" + info); + } + + +} diff --git a/app/src/main/java/com/sw/st/ui/login/LoginActivity.java b/app/src/main/java/com/sw/st/ui/login/LoginActivity.java new file mode 100644 index 0000000..0a2df55 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/LoginActivity.java @@ -0,0 +1,1798 @@ +//package com.sw.st.ui.login; +// +//import android.Manifest; +//import android.app.AlertDialog; +//import android.app.Dialog; +//import android.content.Intent; +//import android.graphics.Color; +//import android.graphics.Point; +//import android.graphics.Rect; +//import android.graphics.Typeface; +//import android.hardware.Camera; +//import android.os.CountDownTimer; +//import android.os.Handler; +//import android.os.Looper; +//import android.os.Message; +//import android.os.SystemClock; +//import android.text.TextUtils; +//import android.util.Log; +//import android.view.View; +//import android.view.ViewTreeObserver; +//import android.widget.FrameLayout; +//import android.widget.ImageView; +//import android.widget.LinearLayout; +//import android.widget.SeekBar; +//import android.widget.Switch; +//import android.widget.TextView; +//import android.widget.Toast; +// +//import androidx.annotation.Nullable; +//import androidx.core.app.ActivityCompat; +//import androidx.recyclerview.widget.RecyclerView; +// +//import com.arcsoft.face.AgeInfo; +//import com.arcsoft.face.ErrorInfo; +//import com.arcsoft.face.FaceEngine; +//import com.arcsoft.face.FaceFeature; +//import com.arcsoft.face.FaceInfo; +//import com.arcsoft.face.GenderInfo; +//import com.arcsoft.face.LivenessInfo; +//import com.arcsoft.face.enums.DetectFaceOrientPriority; +//import com.arcsoft.face.enums.DetectMode; +//import com.bumptech.glide.Glide; +//import com.bumptech.glide.load.resource.bitmap.CenterCrop; +//import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +//import com.bumptech.glide.request.RequestOptions; +//import com.github.mikephil.charting.charts.PieChart; +//import com.github.mikephil.charting.components.Legend; +//import com.github.mikephil.charting.data.Entry; +//import com.github.mikephil.charting.data.PieData; +//import com.github.mikephil.charting.data.PieDataSet; +//import com.github.mikephil.charting.data.PieEntry; +//import com.github.mikephil.charting.formatter.PercentFormatter; +//import com.github.mikephil.charting.highlight.Highlight; +//import com.github.mikephil.charting.listener.OnChartValueSelectedListener; +//import com.github.mikephil.charting.utils.MPPointF; +//import com.gyf.immersionbar.BarHide; +//import com.gyf.immersionbar.ImmersionBar; +//import com.innohi.YNHAPI; +//import com.lzy.okgo.OkGo; +//import com.lzy.okgo.model.HttpHeaders; +//import com.sw.st.R; +//import com.sw.st.api.SwService; +//import com.sw.st.api.WeightApi; +//import com.sw.st.application.AppConst; +//import com.sw.st.base.BaseActivity; +//import com.sw.st.model.DeviceMacModel; +//import com.sw.st.model.DinnerModel; +//import com.sw.st.model.DinnerType; +//import com.sw.st.model.FoodInfoModel; +//import com.sw.st.model.MealRecordsInfo; +//import com.sw.st.model.UserFaceModel; +//import com.sw.st.model.UserInfo; +//import com.sw.st.ui.device.DeviceActivity; +//import com.sw.st.ui.setting.FoodSettingActivity; +//import com.sw.st.ui.setting.MyFlowAdapters; +//import com.sw.st.utils.AppUtil; +//import com.sw.st.utils.DeviceIdUtil; +//import com.sw.st.utils.L; +//import com.sw.st.utils.PrefUtils; +//import com.sw.st.utils.T; +//import com.sw.st.utils.arcface.ConfigUtil; +//import com.sw.st.utils.arcface.DrawHelper; +//import com.sw.st.utils.arcface.DrawInfo; +//import com.sw.st.utils.arcface.camera.CameraListener; +//import com.sw.st.utils.arcface.camera.DualCameraHelper; +//import com.sw.st.utils.arcface.face.FaceHelper; +//import com.sw.st.utils.arcface.face.FaceListener; +//import com.sw.st.utils.arcface.face.LivenessType; +//import com.sw.st.utils.arcface.face.RecognizeColor; +//import com.sw.st.utils.arcface.face.RequestFeatureStatus; +//import com.sw.st.utils.arcface.face.RequestLivenessStatus; +//import com.sw.st.utils.arcface.model.FacePreviewInfo; +//import com.sw.st.utils.faceserver.CompareResult; +//import com.sw.st.utils.faceserver.FaceServer; +//import com.sw.st.view.CustomSeekBar; +//import com.sw.st.view.FaceRectView; +//import com.xmjjdz.serialportapi.libweighingscale.API; +//import com.zhy.view.flowlayout.FlowLayout; +//import com.zhy.view.flowlayout.TagFlowLayout; +// +//import org.json.JSONException; +//import org.json.JSONObject; +// +//import java.io.File; +//import java.io.UnsupportedEncodingException; +//import java.net.URLDecoder; +//import java.net.URLEncoder; +//import java.util.ArrayList; +//import java.util.Enumeration; +//import java.util.List; +//import java.util.Map; +//import java.util.Timer; +//import java.util.TimerTask; +//import java.util.concurrent.ConcurrentHashMap; +//import java.util.concurrent.TimeUnit; +//import java.util.regex.Matcher; +//import java.util.regex.Pattern; +// +//import aclasdriver.AclasScale; +//import butterknife.BindView; +//import butterknife.OnClick; +//import io.reactivex.Observable; +//import io.reactivex.ObservableEmitter; +//import io.reactivex.ObservableOnSubscribe; +//import io.reactivex.Observer; +//import io.reactivex.android.schedulers.AndroidSchedulers; +//import io.reactivex.disposables.CompositeDisposable; +//import io.reactivex.disposables.Disposable; +//import io.reactivex.schedulers.Schedulers; +// +///** +// * desc:登录注册界面 +// */ +// +//public class LoginActivity extends BaseActivity +// implements LoginRegistView, ViewTreeObserver.OnGlobalLayoutListener, OnChartValueSelectedListener { +// private static final String TAG = "LoginActivity"; +// +// private final String COM_INFO = "com_info"; +// private final String REST_NUM = "restId"; +// +// private final int OFFSET_WEIGHT = 2; +// +// private static final int MAX_DETECT_NUM = 3; +// /** +// * 当FR成功,活体未成功时,FR等待活体的时间 +// */ +// private static final int WAIT_LIVENESS_INTERVAL = 100; +// /** +// * 失败重试间隔时间(ms) +// */ +// private static final long FAIL_RETRY_INTERVAL = 1000; +// /** +// * 出错重试最大次数 +// */ +// private static final int MAX_RETRY_TIME = 3; +// +// private DualCameraHelper cameraHelper; +// private DrawHelper drawHelper; +// private Camera.Size previewSize; +// /** +// * 优先打开的摄像头,本界面主要用于单目RGB摄像头设备,因此默认打开前置 +// */ +//// private Integer rgbCameraID = Camera.CameraInfo.CAMERA_FACING_FRONT; +// private Integer rgbCameraID = Camera.CameraInfo.CAMERA_FACING_BACK; +// private Integer cameraIrId = Camera.CameraInfo.CAMERA_FACING_FRONT; +// +// private DualCameraHelper cameraHelperIr; +// +// private volatile byte[] irData; +// +// /** +// * VIDEO模式人脸检测引擎,用于预览帧人脸追踪 +// */ +// private FaceEngine ftEngine; +// /** +// * 用于特征提取的引擎 +// */ +// private FaceEngine frEngine; +// /** +// * IMAGE模式活体检测引擎,用于预览帧人脸活体检测 +// */ +// private FaceEngine flEngine; +// +// private int ftInitCode = -1; +// private int frInitCode = -1; +// private int flInitCode = -1; +// private FaceHelper faceHelper; +// private List compareResultList; +// /** +// * 活体检测的开关 +// */ +// private boolean livenessDetect = true; +// /** +// * 注册人脸状态码,准备注册 +// */ +// private static final int REGISTER_STATUS_READY = 0; +// /** +// * 注册人脸状态码,注册中 +// */ +// private static final int REGISTER_STATUS_PROCESSING = 1; +// /** +// * 注册人脸状态码,注册结束(无论成功失败) +// */ +// private static final int REGISTER_STATUS_DONE = 2; +// +// private int registerStatus = REGISTER_STATUS_DONE; +// /** +// * 用于记录人脸识别相关状态 +// */ +// private ConcurrentHashMap requestFeatureStatusMap = new ConcurrentHashMap<>(); +// /** +// * 用于记录人脸特征提取出错重试次数 +// */ +// private ConcurrentHashMap extractErrorRetryMap = new ConcurrentHashMap<>(); +// /** +// * 用于存储活体值 +// */ +// private ConcurrentHashMap livenessMap = new ConcurrentHashMap<>(); +// /** +// * 用于存储活体检测出错重试次数 +// */ +// private ConcurrentHashMap livenessErrorRetryMap = new ConcurrentHashMap<>(); +// +// private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable(); +// private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable(); +// /** +// * 相机预览显示的控件,可为SurfaceView或TextureView +// */ +// private View previewView; +// private View previewViewIr; +// /** +// * 绘制人脸框的控件 +// */ +// private FaceRectView faceRectView; +// +// private Switch switchLivenessDetect; +// +// private static final int ACTION_REQUEST_PERMISSIONS = 0x001; +// /** +// * 识别阈值 +// */ +// private static final float SIMILAR_THRESHOLD = 0.8F; +// /** +// * 所需的所有权限信息 +// */ +// private static final String[] NEEDED_PERMISSIONS = new String[]{ +// Manifest.permission.CAMERA, +// Manifest.permission.READ_PHONE_STATE, +// Manifest.permission.WRITE_EXTERNAL_STORAGE, +// Manifest.permission.READ_EXTERNAL_STORAGE +// +// }; +// +// private String mUid = null; +// +// @BindView(R.id.camera_layout) +// FrameLayout cameraLayout; +// @BindView(R.id.food_img) +// ImageView foodImg; +// @BindView(R.id.main_camera_status_info) +// TextView cameraStatusInfo; +// @BindView(R.id.main_user_info_layout) +// LinearLayout userInfoLayout; +// @BindView(R.id.main_user_name_tv) +// TextView userNameTv; +// @BindView(R.id.main_user_company_tv) +// TextView userCompanyTv; +// @BindView(R.id.main_foodname_tv) +// TextView foodNameTv; +// @BindView(R.id.main_foodname_take_tv) +// TextView foodTakeTv; +// @BindView(R.id.main_foodname_take_kcal_tv) +// TextView foodTakeKcalTv; +// @BindView(R.id.main_form_calorie_num_tv) +// TextView calorieNumTv; +// @BindView(R.id.main_dinner_min_tv) +// TextView dinnerMinTv; +// @BindView(R.id.main_dinner_max_tv) +// TextView dinnerMaxTv; +// @BindView(R.id.seekBar) +// SeekBar mSeekBar; +// @BindView(R.id.title_content_fat) +// TextView titleContentFat; +// @BindView(R.id.title_content_protein) +// TextView titleContentProtein; +// @BindView(R.id.title_content_cho) +// TextView titleContentCho; +// @BindView(R.id.title_flowlayout) +// TagFlowLayout titleFlowLayout; +// @BindView(R.id.middle_fat) +// TextView middleFat; +// @BindView(R.id.middle_protein) +// TextView middleProtein; +// @BindView(R.id.middle_cho) +// TextView middleCho; +// @BindView(R.id.bottom_weight_tv) +// TextView bottomWeight; +// @BindView(R.id.bottom_fat_tv) +// TextView bottomFat; +// @BindView(R.id.bottom_protein_tv) +// TextView bottomProtein; +// @BindView(R.id.bottom_cho_tv) +// TextView bottomCho; +// @BindView(R.id.bottom_kcal_tv) +// TextView bottomKcal; +// @BindView(R.id.rfid) +// TextView rfidTv; +// +// +// private boolean isWhileGetData = true; +// private float firstWeight = 0; +// private float currentWeight = 0; +// private float intake = 0; +// +// private WeightApi weightApi; +// +// @Override +// protected LoginRegistPresenter createPresenter() { +// return new LoginRegistPresenter(); +// } +// +// @Override +// protected int provideContentViewId() { +// return R.layout.activity_main; +// } +// +// @Override +// protected void afterRequestPermission(int requestCode, boolean isAllGranted) { +// if (requestCode == ACTION_REQUEST_PERMISSIONS) { +// if (isAllGranted) { +// initEngine(); +// initCamera(); +// initIrCamera(); +// } else { +// showToast(getString(R.string.permission_denied)); +// } +// } +// +// } +// +// @Override +// public void init() { +// super.init(); +// +// //本地人脸库初始化 +// FaceServer.getInstance().init(this); +// +// weightApi = new WeightApi(); +// try { +// String strAdd = PrefUtils.getString(this, COM_INFO, "/dev/ttyS4"); +// weightApi.openPort(strAdd, 9600, responseListener); +// } catch (Exception exception) { +// exception.printStackTrace(); +// Toast.makeText(LoginActivity.this, "打开失败", +// Toast.LENGTH_SHORT).show(); +// } +// +// initTimer(); +// startThreadGetData(); +// +// //未设置餐厅或IP,自动跳转设置界面 +// String restNum = PrefUtils.getString(LoginActivity.this, REST_NUM, null); +// String comInfo = PrefUtils.getString(LoginActivity.this, COM_INFO, null); +// //AppUtil.isEmpty(deviceIp) || +//// if (AppUtil.isEmpty(comInfo) || AppUtil.isEmpty(restNum)) { +//// startActivity(new Intent(LoginActivity.this, DeviceActivity.class)); +//// return; +//// } +// } +// +// @Override +// public void initView() { +// +// ImmersionBar.with(this) +// .hideBar(BarHide.FLAG_HIDE_BAR) +// .statusBarAlpha(0f) +// .statusBarDarkFont(true) +// .statusBarColor(R.color.white) +// .init(); +// +// previewView = findViewById(R.id.single_camera_texture_preview); +// //在布局结束后才做初始化操作 +// previewView.getViewTreeObserver().addOnGlobalLayoutListener(this); +// +// faceRectView = findViewById(R.id.single_camera_face_rect_view); +// compareResultList = new ArrayList<>(); +// +// +// previewViewIr = findViewById(R.id.dual_camera_texture_previewIr); +// +//// Typeface typeface = Typeface.createFromAsset(getAssets(), "fonts/FZYTK.TTF"); +//// bottomKcal.setTypeface(typeface); +// +//// mPresenter.getToken(); +// } +// +// +// //设置数据 +// private void setData(int weight, boolean isSetUserInfo) { +// if (currentFoodInfo == null) { +// return; +// } +// float kcal = AppUtil.formatFloat2((currentFoodInfo.getExtInfo().getEnergyKcal() / currentFoodInfo.getUnitWeight() * weight)); +// +// float fat = AppUtil.formatFloat2(currentFoodInfo.getExtInfo().getFat() / currentFoodInfo.getUnitWeight() * weight); +// float protein = AppUtil.formatFloat2(currentFoodInfo.getExtInfo().getProtein() / currentFoodInfo.getUnitWeight() * weight); +// float cho = AppUtil.formatFloat2(currentFoodInfo.getExtInfo().getCho() / currentFoodInfo.getUnitWeight() * weight); +// float na = AppUtil.formatFloat2(currentFoodInfo.getExtInfo().getNa() / 1000 / currentFoodInfo.getUnitWeight() * weight); +// if (isSetUserInfo) { +//// foodTakeTv.setText("取用量" + weight + "g(" +//// + kcal +//// + "Kcal)"); +// foodTakeTv.setText(weight + ""); +// foodTakeKcalTv.setText(kcal + ""); +// setEnergyKcal(energyKcal + kcal); +//// formWeightTv.setText("(" + weight + "g)"); +// middleFat.setText("脂肪/" + fat + "g"); +// middleProtein.setText("蛋白质/" + protein + "g"); +// middleCho.setText("碳水/" + cho + "g"); +// +// setBottomData(weight, fat, protein, cho); +// } else { +//// foodTakeTv.setText("取用量" + 0 + "g(" +//// + 0 +//// + "Kcal)"); +// foodTakeTv.setText("0"); +// foodTakeKcalTv.setText("0"); +// setEnergyKcal(energyKcal); +// middleFat.setText("脂肪/--"); +// middleProtein.setText("蛋白质/--"); +// middleCho.setText("碳水/--"); +// } +// } +// +// @Override +// protected void onResume() { +// super.onResume(); +// +// mPresenter.getFoodInfoByMac(DeviceIdUtil.getDeviceId(this)); +// +// try { +// if (cameraHelper != null) { +// cameraHelper.start(); +// } +// if (cameraHelperIr != null) { +// cameraHelperIr.start(); +// } +// } catch (RuntimeException e) { +// showToast(e.getMessage() + getString(R.string.camera_error_notice)); +// } +// } +// +// private void setBottomData(int weight, float fat, float protein, float cho) { +// if (foodExtInfoVo != null) { +// bottomFat.setText(AppUtil.formatFloat2(foodExtInfoVo.getFat() + fat) + "g"); +// bottomProtein.setText(AppUtil.formatFloat2(foodExtInfoVo.getProtein() + protein) + "g"); +// bottomCho.setText(AppUtil.formatFloat2(foodExtInfoVo.getCho() + cho) + "g"); +// } else { +// bottomFat.setText(AppUtil.formatFloat2(fat) + "g"); +// bottomProtein.setText(AppUtil.formatFloat2(protein) + "g"); +// bottomCho.setText(AppUtil.formatFloat2(cho) + "g"); +// } +// bottomWeight.setText(AppUtil.formatFloat2(totalWeight + weight) + "g"); +// } +// +// private Handler handler; +// +// @Override +// public void initData() { +// handler = new Handler(Looper.getMainLooper()) { +// @Override +// public void handleMessage(Message msg) { +// switch (msg.what) { +// case 1://返回socket数据 +// rfidTv.setText("RFID:" + rfId); +// //====================================== +//// if (firstWeight == 0) {//保存首次重量 +//// firstWeight = (int) deviceWeight; +//// } +//// currentWeight = (int) deviceWeight; +// if (firstWeight - currentWeight > 0) { +// intake = firstWeight - currentWeight; +// setData((int) intake, true); +// if (AppUtil.isEmpty(mUid) && intake > OFFSET_WEIGHT) { +// L.e("onLineRenewal-light===" + intake); +//// openLedLight(YNHAPI.Light.Light_Red); +// } +// } else if (firstWeight - currentWeight == 0) { +// intake = firstWeight - currentWeight; +// setData(100, false); +// setBottomData(0, 0, 0, 0); +// } +// break; +// case 2://获取本餐信息 +// mPresenter.getMealRecordsInfo(mUid); +// break; +// } +// } +// }; +// +// mPresenter.getUserFace(); +// mPresenter.getFoodInfoByMac(DeviceIdUtil.getDeviceId(this)); +// } +// +// private void openLedLight(YNHAPI.Light index) { +// if (YNHAPI.getLightState(index)) { +// return; +// } +// YNHAPI.setLightState(index, true); +// new Handler().postDelayed(new Runnable() { +// public void run() { +// YNHAPI.setLightState(index, false); +// } +// }, 5000); //5秒 +// } +// +// private final long CLICK_INTERVAL_TIME = 300; +// private long lastClickTime = 0; +// +// @Override +// public void initListener() { +// userInfoLayout.setOnClickListener(new View.OnClickListener() { +// @Override +// public void onClick(View v) { +// long currentTimeMillis = SystemClock.uptimeMillis(); +// if (currentTimeMillis - lastClickTime < CLICK_INTERVAL_TIME) { +// Intent intent = new Intent(LoginActivity.this, DeviceActivity.class); +// startActivity(intent); +// return; +// } +// lastClickTime = currentTimeMillis; +// } +// }); +// foodNameTv.setOnClickListener(new View.OnClickListener() { +// @Override +// public void onClick(View v) { +// long currentTimeMillis = SystemClock.uptimeMillis(); +// if (currentTimeMillis - lastClickTime < CLICK_INTERVAL_TIME) { +// Intent intent = new Intent(LoginActivity.this, FoodSettingActivity.class); +// intent.putExtra("current_food", foodNameTv.getText().toString()); +// startActivity(intent); +// finish(); +// return; +// } +// lastClickTime = currentTimeMillis; +// } +// }); +// } +// +// @Override +// public void onGlobalLayout() { +// previewView.getViewTreeObserver().removeOnGlobalLayoutListener(this); +// if (!checkPermissions(NEEDED_PERMISSIONS)) { +// ActivityCompat.requestPermissions(this, NEEDED_PERMISSIONS, ACTION_REQUEST_PERMISSIONS); +// } else { +// initEngine(); +// initCamera(); +// initIrCamera(); +// } +// } +// +// @OnClick({R.id.main_foodname_take_tv}) +// public void onViewClicked(View view) { +// switch (view.getId()) { +// case R.id.main_foodname_take_tv: +//// createRecord(); +//// setEnergyKcal(800); +// +//// openLedLight(YNHAPI.Light.Light_Red); +// break; +// } +// } +// +// +// @Override +// protected void onPause() { +// super.onPause(); +// if (cameraHelperIr != null) { +// cameraHelperIr.stop(); +// } +// } +// +// @Override +// protected void onDestroy() { +// isWhileGetData = false; +// if (uploadOnLinetimer != null) { +// uploadOnLinetimer.cancel(); +// } +// if (cameraHelper != null) { +// cameraHelper.release(); +// cameraHelper = null; +// } +// if (cameraHelperIr != null) { +// cameraHelperIr.release(); +// cameraHelperIr = null; +// } +// +// unInitEngine(); +// if (getFeatureDelayedDisposables != null) { +// getFeatureDelayedDisposables.clear(); +// } +// if (delayFaceTaskCompositeDisposable != null) { +// delayFaceTaskCompositeDisposable.clear(); +// } +// if (faceHelper != null) { +// ConfigUtil.setTrackedFaceCount(this, faceHelper.getTrackedFaceCount()); +// faceHelper.release(); +// faceHelper = null; +// } +// +// FaceServer.getInstance().unInit(); +// +// super.onDestroy(); +// } +// +// @Override +// public void showProgress(String tipString) { +//// showWaitingDialog(tipString); +// } +// +// @Override +// public void hideProgress() { +//// hideWaitingDialog(); +// } +// +// @Override +// public void upFaceFeatureSuccess() { +// finish(); +// startActivity(new Intent(this, SelectIdentityActivity.class)); +// } +// +// @Override +// public void upFaceFeatureFail(String info) { +// T.showShort(this, info); +// finish(); +// startActivity(new Intent(this, SelectIdentityActivity.class)); +// } +// +// @Override +// public void getFaceFeatureSuccess(ArrayList list) { +// if (list != null && list.size() > 0) { +// FaceServer.getInstance().clearAllFaces(this); +// +// for (int i = 0; i < list.size(); i++) { +// UserFaceModel faceModel = list.get(i); +// FaceServer.getInstance().saveFaceFeature(faceModel.getUserId() + ":" + faceModel.getUserFaceId(), faceModel.getFaceFeature()); +// } +// } else { +// showToast("人脸数据获取失败"); +// } +// } +// +// @Override +// public void getFaceFeatureFail(String info) { +// +// } +// +// +// @Override +// public void getUserInfoSuccess(UserInfo userModel) { +// if (userModel == null || userModel.getSlUserVo().getTsUser() == null) { +// showToast("获取用户信息失败"); +// return; +// } +// firstWeight = 0; +// UserInfo.tsUser tsUser = userModel.getSlUserVo().getTsUser(); +// mUid = tsUser.getId(); +// +// cameraStatusInfo.setVisibility(View.GONE); +// userInfoLayout.setVisibility(View.VISIBLE); +// userNameTv.setText(tsUser.getRealName()); +// userCompanyTv.setText(tsUser.getDepartName()); +// mPresenter.bindDeviceMac(mUid, DeviceIdUtil.getDeviceId(this)); +// +// Message message = new Message(); +// message.what = 2; +// handler.sendMessageDelayed(message, 700); +// +// +// if (mCountDownTimer != null) { +// mCountDownTimer.cancel(); +// mCountDownTimer.start(); +// } +// } +// +// @Override +// public void getUserInfoFail(String info) { +// +// } +// +// +// @Override +// public void getDeviceMacSuccess(DeviceMacModel deviceMacModel) { +// if (!deviceMacModel.getDeviceMac().equals(DeviceIdUtil.getDeviceId(this))) {//绑定设备已切换 +// createRecord(); +// } +// } +// +// private double anonymousLastWeight = 0; +// private long lastTimeMillis = 0; +// private final long ANONYMOUS_SUBMIT_INTERVAL = 1000 * 60; +// +// private void createAnonymousRecord() { +// long currentTime = System.currentTimeMillis(); +// if (lastTimeMillis == 0) { +// lastTimeMillis = currentTime; +// } +// if (anonymousLastWeight == 0) { +// anonymousLastWeight = deviceWeight; +// } +// if (currentTime - lastTimeMillis > ANONYMOUS_SUBMIT_INTERVAL) { +// lastTimeMillis = currentTime; +// double currentIntake = anonymousLastWeight - deviceWeight; +// Log.e("mzf", "createAnonymousRecord===" + currentIntake + ""); +// if (AppUtil.isEmpty(mUid) +// && currentIntake > OFFSET_WEIGHT) { +// String restNo = PrefUtils.getString(LoginActivity.this, REST_NUM, ""); +// String anonymousId = "anonymous_" + restNo + "_user_id_000"; +// mPresenter.createRecord(anonymousId, +// foodNameTv.getTag().toString(), (int) currentIntake, deviceWeight); +// +// runOnUiThread(new Runnable() { +// @Override +// public void run() { +// resetData(); +// } +// }); +// } +// anonymousLastWeight = deviceWeight; +// } +// +// } +// +// private void createRecord() { +// if (!AppUtil.isEmpty(mUid) +// && intake > OFFSET_WEIGHT +// && !AppUtil.isEmpty(foodNameTv.getTag().toString())) { +// mPresenter.createRecord(mUid, foodNameTv.getTag().toString(), intake, deviceWeight); +// anonymousLastWeight = deviceWeight; +// } +// if (!AppUtil.isEmpty(mUid)) { +// runOnUiThread(new Runnable() { +// @Override +// public void run() { +// resetData(); +// } +// }); +// } +// } +// +// private void resetData() { +// //--------------初始化状态------------------------ +// mUid = null; +// cameraStatusInfo.setVisibility(View.VISIBLE); +// userInfoLayout.setVisibility(View.GONE); +// userNameTv.setText(null); +// userCompanyTv.setText(null); +// +// firstWeight = 0; +// currentWeight = 0; +// intake = 0; +// energyKcal = 0; +// foodExtInfoVo = null; +// totalWeight = 0; +// setBottomData(0, 0, 0, 0); +// setEnergyKcal(0); +// setData(100, false); +// clearLeftFace(null); +// currentUid = null; +// +// if (mCountDownTimer != null) { +// mCountDownTimer.cancel(); +// } +// } +// +// @Override +// public void getDeviceMacFail(String info) { +// +// } +// +// private FoodInfoModel currentFoodInfo; +// +// @Override +// public void getFoodInfoByMacSuccess(FoodInfoModel foodInfoModel) { +// foodNameTv.setText(foodInfoModel.getFoodName()); +// foodNameTv.setTag(foodInfoModel.getId()); +// +// try { +// RequestOptions cropOptions = new RequestOptions(); +// cropOptions.transform(new RoundedCorners(12));//new CenterCrop() +// +// Glide.with(this).load(AppConst.BASE_IMG_URL + URLDecoder.decode(foodInfoModel.getFoodImg(), "UTF-8")) +// .apply(cropOptions) +// .into(foodImg); +// } catch (UnsupportedEncodingException e) { +// e.printStackTrace(); +// } +// +// ArrayList tagArr = new ArrayList<>(); +// String foodFeelName = foodInfoModel.getFoodFeelName(); +// String foodEffectName = foodInfoModel.getFoodEffectName(); +// if (!AppUtil.isEmpty(foodFeelName)) { +// String[] feedNameArr = foodFeelName.split(","); +// for (String feedName : feedNameArr) { +// tagArr.add(feedName); +// } +// } +// if (!AppUtil.isEmpty(foodEffectName)) { +// String[] feedNameArr = foodEffectName.split(","); +// for (String feedName : feedNameArr) { +// tagArr.add(feedName); +// } +// } +// if (tagArr.size() > 0) { +// MyFoodTagAdapters myFlowAdapters = new MyFoodTagAdapters(this, tagArr.size() > 4 ? tagArr.subList(0, 4) : tagArr); +// titleFlowLayout.setAdapter(myFlowAdapters); +// myFlowAdapters.notifyDataChanged(); +// titleFlowLayout.setVisibility(View.VISIBLE); +// } else { +// titleFlowLayout.setVisibility(View.GONE); +// } +// +// currentFoodInfo = foodInfoModel; +//// setData(100, false); +// +// //初始化营养信息 +// float kcal = AppUtil.formatFloat2((currentFoodInfo.getExtInfo().getEnergyKcal() / currentFoodInfo.getUnitWeight() * 100)); +// float fat = AppUtil.formatFloat2(currentFoodInfo.getExtInfo().getFat() / currentFoodInfo.getUnitWeight() * 100); +// float protein = AppUtil.formatFloat2(currentFoodInfo.getExtInfo().getProtein() / currentFoodInfo.getUnitWeight() * 100); +// float cho = AppUtil.formatFloat2(currentFoodInfo.getExtInfo().getCho() / currentFoodInfo.getUnitWeight() * 100); +// float na = AppUtil.formatFloat2(currentFoodInfo.getExtInfo().getNa() / 1000 / currentFoodInfo.getUnitWeight() * 100); +// +// calorieNumTv.setText(kcal + ""); +// titleContentFat.setText("脂肪/" + fat + "g"); +// titleContentCho.setText("碳水/" + cho + "g"); +// titleContentProtein.setText("蛋白质/" + protein + "g"); +// } +// +// @Override +// public void getFoodInfoByMacFail(String info) { +// +// } +// +// private ArrayList dinnerList; +// private float energyKcal = 0; +// private MealRecordsInfo.foodExtInfoVo foodExtInfoVo; +// private float totalWeight = 0; +// +// @Override +// public void getMealRecordsInfoSuccess(MealRecordsInfo mealRecordsInfo) { +// if (mealRecordsInfo != null && mealRecordsInfo.getNeedEnergyVo() != null) { +// MealRecordsInfo.NeedEnergyVo needEnergyVo = mealRecordsInfo.getNeedEnergyVo(); +// dinnerList = new ArrayList<>(); +// dinnerList.add(new DinnerModel(needEnergyVo.getDinner1Min(), needEnergyVo.getDinner1Max())); +// dinnerList.add(new DinnerModel(needEnergyVo.getDinner2Min(), needEnergyVo.getDinner2Max())); +// dinnerList.add(new DinnerModel(needEnergyVo.getDinner3Min(), needEnergyVo.getDinner3Max())); +// if (mealRecordsInfo.getFoodExtInfoVo() != null) { +// foodExtInfoVo = mealRecordsInfo.getFoodExtInfoVo(); +// energyKcal = foodExtInfoVo.getEnergyKcal(); +// } +// totalWeight = mealRecordsInfo.getTotalWeight(); +// mPresenter.getDinnerType(); +// } +// } +// +// @Override +// public void getMealRecordsInfoFail(String info) { +// +// } +// +// private DinnerModel currentDinner; +// +// @Override +// public void getDinnerTypeSuccess(DinnerType dinnerType) { +// DinnerModel dinnerModel = dinnerList.get(dinnerType.getDinnerType() - 1); +// +// if (dinnerModel == null) { +// return; +// } +// +// currentDinner = dinnerModel; +// setEnergyKcal(energyKcal); +// dinnerMinTv.setText(currentDinner.getDinnerMin() + "kcal"); +// dinnerMaxTv.setText(currentDinner.getDinnerMax() + "kcal"); +// setBottomData(0, 0, 0, 0); +// } +// +// private void setEnergyKcal(float kcal) { +// kcal = AppUtil.formatFloat2(kcal); +// if (currentDinner == null) { +// return; +// } +// +// mSeekBar.setProgress((int) (kcal / currentDinner.getDinnerMax() * 100)); +// bottomKcal.setText(kcal + ""); +//// mSeekBar.setTextInfo(kcal + "kcal"); +// +//// if (kcal > currentDinner.getDinnerMax()) { +//// kcalYellowTv.setVisibility(View.INVISIBLE); +//// kcalBlueTv.setVisibility(View.INVISIBLE); +//// kcalRedTv.setVisibility(View.VISIBLE); +//// kcalRedTv.setText("能量:" + kcal + "kcal"); +//// } else if (kcal > currentDinner.getDinnerMin()) { +//// kcalYellowTv.setVisibility(View.INVISIBLE); +//// kcalBlueTv.setVisibility(View.VISIBLE); +//// kcalRedTv.setVisibility(View.INVISIBLE); +//// kcalBlueTv.setText("能量:" + kcal + "kcal"); +//// } else { +//// kcalYellowTv.setVisibility(View.VISIBLE); +//// kcalBlueTv.setVisibility(View.INVISIBLE); +//// kcalRedTv.setVisibility(View.INVISIBLE); +//// kcalYellowTv.setText("能量:" + kcal + "kcal"); +//// } +// } +// +// @Override +// public void getDinnerTypeFail(String info) { +// +// } +// +// @Override +// public void createRecordSuccess(String info) { +// +// } +// +// @Override +// public void createRecordFail(String info) { +// +// } +// +// @Override +// public void getTokenSuccess(String info) { +// try { +// JSONObject jsonObject = new JSONObject(info); +// String token = jsonObject.optString("token"); +// +// if (!TextUtils.isEmpty(token)) { +// HttpHeaders httpHeaders = new HttpHeaders(); +// httpHeaders.headersMap.put("X-AUTH-TOKEN", token); +// OkGo.getInstance().addCommonHeaders(httpHeaders); +// +// mPresenter.getUserFace(); +// mPresenter.getFoodInfoByMac(DeviceIdUtil.getDeviceId(this)); +// } +// } catch (JSONException e) { +// e.printStackTrace(); +// } +// +// } +// +// @Override +// public void getTokenFail(String info) { +// +// } +// +// @Override +// public void addFoodWeightSuccess(String info) { +// +// } +// +// @Override +// public void addFoodWeightFail(String info) { +// +// } +// +// private static int REFRESH_DATA_INTERVAL_TIME = 1000 * 60 * 3; +// Timer uploadOnLinetimer = new Timer(); +// +// private void startThreadGetData() { +// class MyTimerTask extends TimerTask { +// public void run() { +// if (mPresenter != null) { +//// mPresenter.getDeviceMac(mUid); +// mPresenter.onLineRenewal(DeviceIdUtil.getDeviceId(LoginActivity.this)); +// } +// } +// } +// uploadOnLinetimer.schedule(new MyTimerTask(), 0, REFRESH_DATA_INTERVAL_TIME); +// } +// +// private final long COUNT_TIME = 60000 * 5; //5分钟倒计时提交数据 +// private CountDownTimer mCountDownTimer; +// +// private void initTimer() { +// mCountDownTimer = new CountDownTimer(COUNT_TIME, 1000) { +// @Override +// public void onTick(long millisUntilFinished) { +// L.e("mCountDownTimer", millisUntilFinished / 1000 + "s"); +// } +// +// @Override +// public void onFinish() { +// L.e("mCountDownTimer", "onFinish"); +// createRecord(); +// } +// }; +// } +// +// +// private double deviceWeight = 0; +// private double lastWeight = 0; +// private String rfId = ""; +// +// WeightApi.ResponseListener responseListener = new WeightApi.ResponseListener() { +// @Override +// public void onGetCmdResult(final byte[] result) { +// +// //Called when receiving response to the sent command. +// //If you want to display the result, you must display it on the UI thread. Currently not in the UI thread.Available runOnUiThread or Handler or other suitable method. +//// runOnUiThread(new Runnable() { +//// @Override +//// public void run() { +//// } +//// }); +// +// } +// +// @Override +// public void onGetWeightInfo(final byte[] weightInfo) { +// //Called when receiving weight information sent by the electronic scale. +// //If you want to display the weight information, you must display it on the UI thread. Currently not in the UI thread.Available runOnUiThread or Handler or other suitable method. +// boolean isSuccess = false; +// String bufferString = null; +// try { +// bufferString = new String(weightInfo,"GB2312"); +// } catch (UnsupportedEncodingException e) { +// e.printStackTrace(); +// } +// L.e(bufferString); +// if (bufferString.indexOf("s") > -1 +// || bufferString.indexOf("u") > -1 +// || bufferString.indexOf("rf") > -1) { +//// bufferString = bufferString.replaceAll("\\n", ""); +//// bufferString = bufferString.replaceAll("\\r", ""); +//// Log.e("mzf", bufferString); +// String[] tem = bufferString.split(","); +// for (int i = 1; i < tem.length - 1; i++) { +// if ((tem[i].startsWith("rf"))) { +// rfId = tem[i]; +//// Log.e("mzf1", bufferString); +// if (handler != null) { +// handler.sendEmptyMessage(1); +// } +// break; +// } +// } +// for (int i = 1; i < tem.length - 1; i++) { +// if ((tem[i].startsWith("s") || tem[i].startsWith("u")) && tem[i].endsWith("kg")) { +// bufferString = tem[i]; +//// Log.e("mzf1", bufferString); +// isSuccess = true; +// break; +// } +// } +// if (isSuccess) { +// Pattern pattern = Pattern.compile("\\d*\\.\\d*|0\\.\\d*[1-9]\\d*$"); +// Matcher matcher = pattern.matcher(bufferString); +// if (matcher.find()) { +// bufferString = matcher.group(0); +// } +// deviceWeight = AppUtil.convertToFloat(bufferString, 0.00f) * 1000; +// if (lastWeight > 0 && deviceWeight - lastWeight > 200) { +// mPresenter.addFoodTotalWeight(foodNameTv.getTag().toString(), 1, deviceWeight - lastWeight, DeviceIdUtil.getDeviceId(LoginActivity.this)); +// anonymousLastWeight = deviceWeight;//加菜后同步重量信息 +// } +// createAnonymousRecord(); +// lastWeight = deviceWeight; +// if (firstWeight == 0) {//保存首次重量 +// firstWeight = (int) deviceWeight; +// } +// currentWeight = (int) deviceWeight; +// if (handler != null) { +// handler.sendEmptyMessage(1); +// } +// } +// } +// } +// }; +//// API.ResponseListener responseListener = new API.ResponseListener() { +//// @Override +//// public void onGetCmdResult(final byte[] result) { +//// //Called when receiving response to the sent command. +//// //If you want to display the result, you must display it on the UI thread. Currently not in the UI thread.Available runOnUiThread or Handler or other suitable method. +////// runOnUiThread(new Runnable() { +////// @Override +////// public void run() { +////// +////// } +////// }); +//// +//// } +//// +//// @Override +//// public void onGetWeightInfo(final byte[] weightInfo) { +//// //Called when receiving weight information sent by the electronic scale. +//// //If you want to display the weight information, you must display it on the UI thread. Currently not in the UI thread.Available runOnUiThread or Handler or other suitable method. +//// runOnUiThread(new Runnable() { +//// @Override +//// public void run() { +//// String bufferString = new String(weightInfo); +//// if (bufferString.indexOf("S") > -1 || bufferString.indexOf("U") > -1) { +//// Pattern pattern = Pattern.compile("\\d*\\.\\d*|0\\.\\d*[1-9]\\d*$"); +//// Matcher matcher = pattern.matcher(bufferString); +//// if (matcher.find()) { +//// bufferString = matcher.group(0); +//// } +//// if (bufferString.length() > 5 && bufferString.indexOf(".") > -1) { +//// Log.e("mzf3333333333", bufferString); +//// +////// tv_weight_info.setText("重量信息:" + convertToFloat(bufferString, 0.00f) * 1000 + "g"); +//// deviceWeight = convertToFloat(bufferString, 0.00f) * 1000; +//// +//// +//// if (firstWeight == 0) {//保存首次重量 +//// firstWeight = (int) deviceWeight; +//// } +//// currentWeight = (int) deviceWeight; +//// if (firstWeight - currentWeight >= 0) { +//// intake = firstWeight - currentWeight; +//// setData(intake, true); +//// } +//// } +//// } +//// +//// } +//// }); +//// } +//// }; +// +// /** +// * 初始化引擎 +// */ +// private void initEngine() { +// ftEngine = new FaceEngine(); +// ftInitCode = ftEngine.init(this, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(this), +// 16, MAX_DETECT_NUM, FaceEngine.ASF_FACE_DETECT); +// +// frEngine = new FaceEngine(); +// frInitCode = frEngine.init(this, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, +// 16, MAX_DETECT_NUM, FaceEngine.ASF_FACE_RECOGNITION); +// +// flEngine = new FaceEngine(); +// //使用普通摄像头检测活体 +//// flInitCode = flEngine.init(this, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, +//// 16, MAX_DETECT_NUM, FaceEngine.ASF_LIVENESS); +// //使用红外摄像头检测活体 +// flInitCode = flEngine.init(this, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, +// 16, MAX_DETECT_NUM, FaceEngine.ASF_IR_LIVENESS); +// +// L.i("initEngine: init: " + ftInitCode); +// +// if (ftInitCode != ErrorInfo.MOK) { +// String error = getString(R.string.specific_engine_init_failed, "ftEngine", ftInitCode); +// L.i("initEngine: " + error); +// showToast(error); +// } +// if (frInitCode != ErrorInfo.MOK) { +// String error = getString(R.string.specific_engine_init_failed, "frEngine", frInitCode); +// L.i("initEngine: " + error); +// showToast(error); +// } +// if (flInitCode != ErrorInfo.MOK) { +// String error = getString(R.string.specific_engine_init_failed, "flEngine", flInitCode); +// L.i("initEngine: " + error); +// showToast(error); +// } +// } +// +// /** +// * 销毁引擎,faceHelper中可能会有特征提取耗时操作仍在执行,加锁防止crash +// */ +// private void unInitEngine() { +// if (ftInitCode == ErrorInfo.MOK && ftEngine != null) { +// synchronized (ftEngine) { +// int ftUnInitCode = ftEngine.unInit(); +// L.i("unInitEngine: " + ftUnInitCode); +// } +// } +// if (frInitCode == ErrorInfo.MOK && frEngine != null) { +// synchronized (frEngine) { +// int frUnInitCode = frEngine.unInit(); +// L.i("unInitEngine: " + frUnInitCode); +// } +// } +// if (flInitCode == ErrorInfo.MOK && flEngine != null) { +// synchronized (flEngine) { +// int flUnInitCode = flEngine.unInit(); +// L.i("unInitEngine: " + flUnInitCode); +// } +// } +// } +// +// private boolean isHasFace = false; +// +// private void initCamera() { +//// DisplayMetrics metrics = new DisplayMetrics(); +//// getWindowManager().getDefaultDisplay().getMetrics(metrics); +// +// final FaceListener faceListener = new FaceListener() { +// @Override +// public void onFail(Exception e) { +// L.e("onFail: " + e.getMessage()); +// createRecord(); +// } +// +// //请求FR的回调 +// @Override +// public void onFaceFeatureInfoGet(@Nullable final FaceFeature faceFeature, final Integer requestId, final Integer errorCode) { +// //FR成功 +// if (faceFeature != null) { +//// L.i( "onPreview: fr end = " + System.currentTimeMillis() + " trackId = " + requestId); +// Integer liveness = livenessMap.get(requestId); +// //不做活体检测的情况,直接搜索 +// if (!livenessDetect) { +// searchFace(faceFeature, requestId); +// } +// //活体检测通过,搜索特征 +// else if (liveness != null && liveness == LivenessInfo.ALIVE) { +// searchFace(faceFeature, requestId); +// } +// //活体检测未出结果,或者非活体,延迟执行该函数 +// else { +// if (requestFeatureStatusMap.containsKey(requestId)) { +// Observable.timer(WAIT_LIVENESS_INTERVAL, TimeUnit.MILLISECONDS) +// .subscribe(new Observer() { +// Disposable disposable; +// +// @Override +// public void onSubscribe(Disposable d) { +// disposable = d; +// getFeatureDelayedDisposables.add(disposable); +// } +// +// @Override +// public void onNext(Long aLong) { +// onFaceFeatureInfoGet(faceFeature, requestId, errorCode); +// } +// +// @Override +// public void onError(Throwable e) { +// +// } +// +// @Override +// public void onComplete() { +// getFeatureDelayedDisposables.remove(disposable); +// } +// }); +// } +// } +// +// } +// //特征提取失败 +// else { +// if (increaseAndGetValue(extractErrorRetryMap, requestId) > MAX_RETRY_TIME) { +// extractErrorRetryMap.put(requestId, 0); +// +// String msg; +// // 传入的FaceInfo在指定的图像上无法解析人脸,此处使用的是RGB人脸数据,一般是人脸模糊 +// if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) { +// msg = getString(R.string.low_confidence_level); +// } else { +// msg = "ExtractCode:" + errorCode; +// } +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, msg)); +// // 在尝试最大次数后,特征提取仍然失败,则认为识别未通过 +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); +// retryRecognizeDelayed(requestId); +// } else { +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.TO_RETRY); +// } +// } +// } +// +// @Override +// public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, final Integer requestId, Integer errorCode) { +// if (livenessInfo != null) { +// int liveness = livenessInfo.getLiveness(); +// livenessMap.put(requestId, liveness); +// // 非活体,重试 +// if (liveness == LivenessInfo.NOT_ALIVE) { +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, "NOT_ALIVE")); +// // 延迟 FAIL_RETRY_INTERVAL 后,将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 +// retryLivenessDetectDelayed(requestId); +// } +// } else { +// if (increaseAndGetValue(livenessErrorRetryMap, requestId) > MAX_RETRY_TIME) { +// livenessErrorRetryMap.put(requestId, 0); +// String msg; +// // 传入的FaceInfo在指定的图像上无法解析人脸,此处使用的是RGB人脸数据,一般是人脸模糊 +// if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) { +// msg = getString(R.string.low_confidence_level); +// } else { +// msg = "ProcessCode:" + errorCode; +// } +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, msg)); +// retryLivenessDetectDelayed(requestId); +// } else { +// livenessMap.put(requestId, LivenessInfo.UNKNOWN); +// } +// } +// } +// +// +// }; +// +// +// CameraListener cameraListener = new CameraListener() { +// @Override +// public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) { +// Camera.Size lastPreviewSize = previewSize; +// previewSize = camera.getParameters().getPreviewSize(); +// drawHelper = new DrawHelper(previewSize.width, previewSize.height, previewView.getWidth(), previewView.getHeight(), displayOrientation +// , cameraId, isMirror, false, false); +// L.i("onCameraOpened: " + drawHelper.toString()); +// // 切换相机的时候可能会导致预览尺寸发生变化 +// if (faceHelper == null || +// lastPreviewSize == null || +// lastPreviewSize.width != previewSize.width || lastPreviewSize.height != previewSize.height) { +// Integer trackedFaceCount = null; +// // 记录切换时的人脸序号 +// if (faceHelper != null) { +// trackedFaceCount = faceHelper.getTrackedFaceCount(); +// faceHelper.release(); +// } +// faceHelper = new FaceHelper.Builder() +// .ftEngine(ftEngine) +// .frEngine(frEngine) +// .flEngine(flEngine) +// .frQueueSize(MAX_DETECT_NUM) +// .flQueueSize(MAX_DETECT_NUM) +// .previewSize(previewSize) +// .faceListener(faceListener) +// .trackedFaceCount(trackedFaceCount == null ? ConfigUtil.getTrackedFaceCount(LoginActivity.this.getApplicationContext()) : trackedFaceCount) +// .build(); +// } +// } +// +// +// @Override +// public void onPreview(final byte[] nv21, Camera camera) { +// if (faceRectView != null) { +// faceRectView.clearFaceInfo(); +// } +// List facePreviewInfoList = faceHelper.onPreviewFrame(nv21); +// if (facePreviewInfoList != null && faceRectView != null && drawHelper != null) { +// drawPreviewInfo(facePreviewInfoList); +// } +// registerFace(nv21, facePreviewInfoList); +// clearLeftFace(facePreviewInfoList); +// +// if (facePreviewInfoList != null && facePreviewInfoList.size() > 0 && previewSize != null) { +// isHasFace = true; +// for (int i = 0; i < facePreviewInfoList.size(); i++) { +// Rect faceRect = facePreviewInfoList.get(i).getFaceInfo().getRect(); +//// L.e("mzf", "faceRect=" + i + "---" + faceRect.width() + "===" + faceRect.height()); +// Integer status = requestFeatureStatusMap.get(facePreviewInfoList.get(i).getTrackId()); +// /** +// * 在活体检测开启,在人脸识别状态不为成功或人脸活体状态不为处理中(ANALYZING)且不为处理完成(ALIVE、NOT_ALIVE)时重新进行活体检测 +// */ +// if (livenessDetect && (status == null || status != RequestFeatureStatus.SUCCEED)) { +// Integer liveness = livenessMap.get(facePreviewInfoList.get(i).getTrackId()); +// if (liveness == null +// || (liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE && liveness != RequestLivenessStatus.ANALYZING)) { +// livenessMap.put(facePreviewInfoList.get(i).getTrackId(), RequestLivenessStatus.ANALYZING); +// if (irData != null) { +// FaceInfo faceInfo = facePreviewInfoList.get(i).getFaceInfo().clone(); +// faceInfo.getRect().offset(0, 0); +// faceHelper.requestFaceLiveness(irData.clone(), faceInfo, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId(), LivenessType.IR); +// } else { +// faceHelper.requestFaceLiveness(nv21, facePreviewInfoList.get(i).getFaceInfo(), previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId(), LivenessType.RGB); +// } +// } +// } +// /** +// * 对于每个人脸,若状态为空或者为失败,则请求特征提取(可根据需要添加其他判断以限制特征提取次数), +// * 特征提取回传的人脸特征结果在{@link FaceListener#onFaceFeatureInfoGet(FaceFeature, Integer, Integer)}中回传 +// */ +// if (status == null +// || status == RequestFeatureStatus.TO_RETRY) { +// requestFeatureStatusMap.put(facePreviewInfoList.get(i).getTrackId(), RequestFeatureStatus.SEARCHING); +// faceHelper.requestFaceFeature(nv21, facePreviewInfoList.get(i).getFaceInfo(), previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId()); +//// L.i( "onPreview: fr start = " + System.currentTimeMillis() + " trackId = " + facePreviewInfoList.get(i).getTrackedFaceCount()); +// } +// } +// } else { +// if (isHasFace) { +//// showToast("无人脸信息"); +// createRecord(); +// isHasFace = false; +// } +// } +// } +// +// @Override +// public void onCameraClosed() { +// L.i("onCameraClosed: "); +// } +// +// @Override +// public void onCameraError(Exception e) { +// L.i("onCameraError: " + e.getMessage()); +// } +// +// @Override +// public void onCameraConfigurationChanged(int cameraID, int displayOrientation) { +// if (drawHelper != null) { +// drawHelper.setCameraDisplayOrientation(displayOrientation); +// } +// L.i("onCameraConfigurationChanged: " + cameraID + " " + displayOrientation); +// } +// }; +// +//// cameraHelper = new CameraHelper.Builder() +//// .previewViewSize(new Point(previewView.getMeasuredWidth(), previewView.getMeasuredHeight())) +//// .rotation(getWindowManager().getDefaultDisplay().getRotation()) +//// .specificCameraId(rgbCameraID != null ? rgbCameraID : Camera.CameraInfo.CAMERA_FACING_FRONT) +//// .isMirror(false) +//// .previewOn(previewView) +//// .cameraListener(cameraListener) +//// .build(); +// cameraHelper = new DualCameraHelper.Builder() +// .previewViewSize(new Point(previewView.getMeasuredWidth(), previewView.getMeasuredHeight())) +// .rotation(getWindowManager().getDefaultDisplay().getRotation()) +// .specificCameraId(rgbCameraID != null ? rgbCameraID : Camera.CameraInfo.CAMERA_FACING_FRONT) +// .previewOn(previewView) +// .cameraListener(cameraListener) +// .isMirror(rgbCameraID != null && Camera.CameraInfo.CAMERA_FACING_FRONT == rgbCameraID) +// .build(); +// cameraHelper.init(); +// cameraHelper.start(); +// +// } +// +// private void initIrCamera() { +// CameraListener irCameraListener = new CameraListener() { +// @Override +// public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) { +// +// } +// +// +// @Override +// public void onPreview(final byte[] nv21, Camera camera) { +// irData = nv21; +// } +// +// @Override +// public void onCameraClosed() { +// Log.i(TAG, "onCameraClosed: "); +// } +// +// @Override +// public void onCameraError(Exception e) { +// Log.i(TAG, "onCameraError: " + e.getMessage()); +// } +// +// @Override +// public void onCameraConfigurationChanged(int cameraID, int displayOrientation) { +// Log.i(TAG, "onCameraConfigurationChanged: " + cameraID + " " + displayOrientation); +// } +// }; +// +// cameraHelperIr = new DualCameraHelper.Builder() +// .previewViewSize(new Point(previewViewIr.getMeasuredWidth(), previewViewIr.getMeasuredHeight())) +// .rotation(getWindowManager().getDefaultDisplay().getRotation()) +// .specificCameraId(cameraIrId != null ? cameraIrId : Camera.CameraInfo.CAMERA_FACING_FRONT) +// .previewOn(previewViewIr) +// .cameraListener(irCameraListener) +// .isMirror(false) +//// .previewSize(new Point(1280, 960)) //相机预览大小设置,RGB与IR需使用相同大小 +//// .additionalRotation(270) //额外旋转角度 +// .build(); +// cameraHelperIr.init(); +// try { +// cameraHelperIr.start(); +// } catch (RuntimeException e) { +// showToast(e.getMessage() + getString(R.string.camera_error_notice)); +// } +// } +// +// +// private void registerFace(final byte[] nv21, +// final List facePreviewInfoList) { +// if (registerStatus == REGISTER_STATUS_READY && facePreviewInfoList != null && facePreviewInfoList.size() > 0) { +// registerStatus = REGISTER_STATUS_PROCESSING; +// Observable.create(new ObservableOnSubscribe() { +// @Override +// public void subscribe(ObservableEmitter emitter) { +// +// boolean success = FaceServer.getInstance().registerNv21(LoginActivity.this, nv21.clone(), previewSize.width, previewSize.height, +// facePreviewInfoList.get(0).getFaceInfo(), "registered " + faceHelper.getTrackedFaceCount()); +// emitter.onNext(success); +// } +// }) +// .subscribeOn(Schedulers.computation()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe(new Observer() { +// @Override +// public void onSubscribe(Disposable d) { +// +// } +// +// @Override +// public void onNext(Boolean success) { +// String result = success ? "register success!" : "register failed!"; +// showToast(result); +// registerStatus = REGISTER_STATUS_DONE; +// } +// +// @Override +// public void onError(Throwable e) { +// e.printStackTrace(); +// showToast("register failed!"); +// registerStatus = REGISTER_STATUS_DONE; +// } +// +// @Override +// public void onComplete() { +// +// } +// }); +// } +// } +// +// private void drawPreviewInfo(List facePreviewInfoList) { +// List drawInfoList = new ArrayList<>(); +// for (int i = 0; i < facePreviewInfoList.size(); i++) { +// String name = faceHelper.getName(facePreviewInfoList.get(i).getTrackId()); +// Integer liveness = livenessMap.get(facePreviewInfoList.get(i).getTrackId()); +// Integer recognizeStatus = requestFeatureStatusMap.get(facePreviewInfoList.get(i).getTrackId()); +// +// // 根据识别结果和活体结果设置颜色 +// int color = RecognizeColor.COLOR_UNKNOWN; +// if (recognizeStatus != null) { +// if (recognizeStatus == RequestFeatureStatus.FAILED) { +// color = RecognizeColor.COLOR_FAILED; +// } +// if (recognizeStatus == RequestFeatureStatus.SUCCEED) { +// color = RecognizeColor.COLOR_SUCCESS; +// } +// } +// if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) { +// color = RecognizeColor.COLOR_FAILED; +// } +// +// drawInfoList.add(new DrawInfo(drawHelper.adjustRect(facePreviewInfoList.get(i).getFaceInfo().getRect()), +// GenderInfo.UNKNOWN, AgeInfo.UNKNOWN_AGE, liveness == null ? LivenessInfo.UNKNOWN : liveness, color, +// name == null ? String.valueOf(facePreviewInfoList.get(i).getTrackId()) : name)); +// } +// drawHelper.draw(faceRectView, drawInfoList); +// } +// +// +// /** +// * 删除已经离开的人脸 +// * +// * @param facePreviewInfoList 人脸和trackId列表 +// */ +// private void clearLeftFace(List facePreviewInfoList) { +// if (compareResultList != null) { +// for (int i = compareResultList.size() - 1; i >= 0; i--) { +// if (!requestFeatureStatusMap.containsKey(compareResultList.get(i).getTrackId())) { +// compareResultList.remove(i); +//// adapter.notifyItemRemoved(i); +// } +// } +// } +// if (facePreviewInfoList == null || facePreviewInfoList.size() == 0) { +// requestFeatureStatusMap.clear(); +// livenessMap.clear(); +// livenessErrorRetryMap.clear(); +// extractErrorRetryMap.clear(); +// if (getFeatureDelayedDisposables != null) { +// getFeatureDelayedDisposables.clear(); +// } +// return; +// } +// Enumeration keys = requestFeatureStatusMap.keys(); +// while (keys.hasMoreElements()) { +// int key = keys.nextElement(); +// boolean contained = false; +// for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { +// if (facePreviewInfo.getTrackId() == key) { +// contained = true; +// break; +// } +// } +// if (!contained) { +// requestFeatureStatusMap.remove(key); +// livenessMap.remove(key); +// livenessErrorRetryMap.remove(key); +// extractErrorRetryMap.remove(key); +// } +// } +// +// +// } +// +// private String currentUid = null; +// +// private void searchFace(final FaceFeature frFace, final Integer requestId) { +// Observable +// .create(new ObservableOnSubscribe() { +// @Override +// public void subscribe(ObservableEmitter emitter) { +//// L.i( "subscribe: fr search start = " + System.currentTimeMillis() + " trackId = " + requestId); +// CompareResult compareResult = FaceServer.getInstance().getTopOfFaceLib(frFace); +//// L.i( "subscribe: fr search end = " + System.currentTimeMillis() + " trackId = " + requestId); +// emitter.onNext(compareResult); +// +// } +// }) +// .subscribeOn(Schedulers.computation()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe(new Observer() { +// @Override +// public void onSubscribe(Disposable d) { +// +// } +// +// @Override +// public void onNext(CompareResult compareResult) { +// if (compareResult == null || compareResult.getUserName() == null) { +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); +// faceHelper.setName(requestId, "VISITOR " + requestId); +// +// createRecord(); +// +// return; +// } +// +//// L.i( "onNext: fr search get result = " + System.currentTimeMillis() + " trackId = " + requestId + " similar = " + compareResult.getSimilar()); +// if (compareResult.getSimilar() > SIMILAR_THRESHOLD) { +// if (AppUtil.isEmpty(currentUid) +// || !currentUid.equals(compareResult.getUserName())) { +// createRecord(); +// +// currentUid = compareResult.getUserName(); +// mPresenter.getUserInfo(currentUid.substring(0, currentUid.indexOf(":"))); +// } +// +// boolean isAdded = false; +// if (compareResultList == null) { +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); +// faceHelper.setName(requestId, "VISITOR " + requestId); +// +// createRecord(); +// +//// loadJs("javacalljswithargs", "");//未识别人脸信息 +// return; +// } +// for (CompareResult compareResult1 : compareResultList) { +// if (compareResult1.getTrackId() == requestId) { +// isAdded = true; +// break; +// } +// } +// if (!isAdded) { +// //对于多人脸搜索,假如最大显示数量为 MAX_DETECT_NUM 且有新的人脸进入,则以队列的形式移除 +// if (compareResultList.size() >= MAX_DETECT_NUM) { +// compareResultList.remove(0); +// } +// //添加显示人员时,保存其trackId +// compareResult.setTrackId(requestId); +// compareResultList.add(compareResult); +// } +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.SUCCEED); +// faceHelper.setName(requestId, getString(R.string.recognize_success_notice, compareResult.getUserName())); +// } else { +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, "NOT_REGISTERED")); +// retryRecognizeDelayed(requestId); +// +// createRecord(); +//// mPresenter.getUserFace();//未获取到用户,重新拉取人脸数据 +// } +// } +// +// @Override +// public void onError(Throwable e) { +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, "NOT_REGISTERED")); +// retryRecognizeDelayed(requestId); +// +// createRecord(); +// } +// +// @Override +// public void onComplete() { +// +// } +// }); +// } +// +// /** +// * 将map中key对应的value增1回传 +// * +// * @param countMap map +// * @param key key +// * @return 增1后的value +// */ +// public int increaseAndGetValue(Map countMap, int key) { +// if (countMap == null) { +// return 0; +// } +// Integer value = countMap.get(key); +// if (value == null) { +// value = 0; +// } +// countMap.put(key, ++value); +// return value; +// } +// +// /** +// * 延迟 FAIL_RETRY_INTERVAL 重新进行活体检测 +// * +// * @param requestId 人脸ID +// */ +// private void retryLivenessDetectDelayed(final Integer requestId) { +// Observable.timer(FAIL_RETRY_INTERVAL, TimeUnit.MILLISECONDS) +// .subscribe(new Observer() { +// Disposable disposable; +// +// @Override +// public void onSubscribe(Disposable d) { +// disposable = d; +// delayFaceTaskCompositeDisposable.add(disposable); +// } +// +// @Override +// public void onNext(Long aLong) { +// +// } +// +// @Override +// public void onError(Throwable e) { +// e.printStackTrace(); +// } +// +// @Override +// public void onComplete() { +// // 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 +// if (livenessDetect) { +// faceHelper.setName(requestId, Integer.toString(requestId)); +// } +// livenessMap.put(requestId, LivenessInfo.UNKNOWN); +// delayFaceTaskCompositeDisposable.remove(disposable); +// } +// }); +// } +// +// /** +// * 延迟 FAIL_RETRY_INTERVAL 重新进行人脸识别 +// * +// * @param requestId 人脸ID +// */ +// private void retryRecognizeDelayed(final Integer requestId) { +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); +// Observable.timer(FAIL_RETRY_INTERVAL, TimeUnit.MILLISECONDS) +// .subscribe(new Observer() { +// Disposable disposable; +// +// @Override +// public void onSubscribe(Disposable d) { +// disposable = d; +// delayFaceTaskCompositeDisposable.add(disposable); +// } +// +// @Override +// public void onNext(Long aLong) { +// +// } +// +// @Override +// public void onError(Throwable e) { +// e.printStackTrace(); +// } +// +// @Override +// public void onComplete() { +// // 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测 +// faceHelper.setName(requestId, Integer.toString(requestId)); +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.TO_RETRY); +// delayFaceTaskCompositeDisposable.remove(disposable); +// } +// }); +// } +// +// @Override +// public void onValueSelected(Entry e, Highlight h) { +// +// } +// +// @Override +// public void onNothingSelected() { +// +// } +//} diff --git a/app/src/main/java/com/sw/st/ui/login/LoginRegistPresenter.java b/app/src/main/java/com/sw/st/ui/login/LoginRegistPresenter.java new file mode 100644 index 0000000..187d124 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/LoginRegistPresenter.java @@ -0,0 +1,519 @@ +package com.sw.st.ui.login; + + +import com.google.gson.Gson; +import com.sw.st.api.SwService; +import com.sw.st.base.BasePresenter; +import com.sw.st.model.DeviceMacModel; +import com.sw.st.model.DinnerType; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.MealRecordsInfo; +import com.sw.st.model.UserFaceModel; +import com.sw.st.model.UserInfo; +import com.sw.st.model.UserNutritionInfo; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; +import com.sw.st.utils.AppUtil; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import io.reactivex.disposables.Disposable; + +/** + * 登录注册 + */ + +public class LoginRegistPresenter extends BasePresenter { + +// public void getUserFace() { +// SwService.getUserFace() +// .compose(RxSchedulersHelper.io_main()) +// .compose(RxResultHelper.handleResult()) +// .subscribe(new RxObserver>() { +// +// @Override +// public void _onSubscribe(Disposable d) { +// getView().showProgress("加载中..."); +// } +// +// @Override +// public void _onNext(ArrayList list) { +// getView().getFaceFeatureSuccess(list); +// } +// +// @Override +// public void _onError(String errorMessage) { +// getView().getFaceFeatureFail(errorMessage); +// +// getView().hideProgress(); +// } +// +// @Override +// public void _onComplete() { +// getView().hideProgress(); +// } +// +// }); +// } + + public void getUserInfo(String rfid) { + SwService.getUserInfo(rfid) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + + @Override + public void _onSubscribe(Disposable d) { + getView().showProgress("加载中..."); + } + + @Override + public void _onNext(String info) { + if (AppUtil.isEmpty(info)) { + getView().getUserInfoFail("数据为空"); + return; + } + try { + JSONObject jsonObject = new JSONObject(info); + int code = jsonObject.optInt("code"); + if (code == 200) { + UserInfo userModel = new Gson().fromJson(jsonObject.optString("result"), + UserInfo.class); + getView().getUserInfoSuccess(userModel); + } else { + getView().getUserInfoFail(""); + } + } catch (JSONException e) { + e.printStackTrace(); + } + + + } + + @Override + public void _onError(String errorMessage) { + getView().getUserInfoFail(errorMessage); + + getView().hideProgress(); + } + + @Override + public void _onComplete() { + getView().hideProgress(); + } + + }); + } + + public void getUserInfoById(String uId) { + SwService.getUserInfoById(uId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + + @Override + public void _onSubscribe(Disposable d) { + getView().showProgress("加载中..."); + } + + @Override + public void _onNext(String info) { + if (AppUtil.isEmpty(info)) { + getView().getUserInfoFail("数据为空"); + return; + } + try { + JSONObject jsonObject = new JSONObject(info); + int code = jsonObject.optInt("code"); + if (code == 200) { + UserInfo userModel = new Gson().fromJson(jsonObject.optString("result"), + UserInfo.class); + getView().getUserInfoSuccess(userModel); + } else { + getView().getUserInfoFail(""); + } + } catch (JSONException e) { + e.printStackTrace(); + } + + + } + + @Override + public void _onError(String errorMessage) { + getView().getUserInfoFail(errorMessage); + + getView().hideProgress(); + } + + @Override + public void _onComplete() { + getView().hideProgress(); + } + + }); + } + + public void bindDeviceMac(String uid, String deviceMac) { + SwService.bindDeviceMac(uid, deviceMac) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + } + + @Override + public void _onError(String errorMessage) { + } + + @Override + public void _onComplete() { + } + }); + } + + public void getDeviceMac(String uid) { + SwService.getDeviceMac(uid) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(DeviceMacModel info) { + if (getView() != null) + getView().getDeviceMacSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getDeviceMacFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + }); + } + + public void getFoodInfoByMac(String deviceMac) { + SwService.getFoodInfoByMac(deviceMac) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(FoodInfoModel info) { + if (getView() != null) + getView().getFoodInfoByMacSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getFoodInfoByMacFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + }); + } + + public void getMealRecordsInfo(String userId) { + SwService.getMealRecordsInfo(userId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(MealRecordsInfo info) { + if (getView() != null) + getView().getMealRecordsInfoSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getMealRecordsInfoFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + + }); + } + + public void getUserNutrition(String restNum, String userId) { + SwService.getUserNutrition(restNum, userId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + + @Override + public void _onSubscribe(Disposable d) { + getView().showProgress("加载中..."); + } + + @Override + public void _onNext(String info) { + if (AppUtil.isEmpty(info)) { + getView().getUserNutritionInfoFail("数据为空"); + return; + } + try { + JSONObject jsonObject = new JSONObject(info); + int code = jsonObject.optInt("code"); + if (code == 200) { + UserNutritionInfo nutritionInfo = new Gson().fromJson(jsonObject.optString("result"), + UserNutritionInfo.class); + getView().getUserNutritionInfoSuccess(nutritionInfo); + } else { + getView().getUserNutritionInfoFail(""); + } + } catch (JSONException e) { + e.printStackTrace(); + } + + + } + + @Override + public void _onError(String errorMessage) { + getView().getUserNutritionInfoFail(errorMessage); + + getView().hideProgress(); + } + + @Override + public void _onComplete() { + getView().hideProgress(); + } + + }); + } + + public void getDinnerType() { + SwService.getDinnerType() + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(DinnerType info) { + if (getView() != null) + getView().getDinnerTypeSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getDinnerTypeFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + }); + } + + public void createRecord(String userId, String foodId, String restNo, float intake, double residueWeight) { + SwService.userEatFood(userId, foodId, restNo, intake, residueWeight) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + if (getView() != null) + getView().createRecordSuccess(info); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().createRecordFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + + +// public void getToken() { +// SwService.getToken() +// .compose(RxSchedulersHelper.io_main()) +// .compose(RxResultHelper.handleJsonResponse()) +// .subscribe(new RxObserver() { +// @Override +// public void _onSubscribe(Disposable d) { +// } +// +// @Override +// public void _onNext(String info) { +// try { +// JSONObject jsonObject = new JSONObject(info); +// if (jsonObject.optInt("respCode") == 0) { +// String data = jsonObject.optString("data"); +// if (getView() != null) +// getView().getTokenSuccess(data); +// } +// } catch (JSONException e) { +// e.printStackTrace(); +// if (getView() != null) +// getView().getTokenFail("数据解析异常"); +// } +// +// } +// +// @Override +// public void _onError(String errorMessage) { +// if (getView() != null) +// getView().getTokenFail(errorMessage); +// } +// +// @Override +// public void _onComplete() { +// } +// }); +// } + + /** + * @param foodId 菜品ID + * @param type 是否开餐1开餐,2加菜 + * @param totalWeight 菜品增重 + * @return + */ + public void addFoodTotalWeight(String foodId, int type, double totalWeight, String deviceMac) { + SwService.startMealService(foodId, type, totalWeight, deviceMac) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().addFoodWeightSuccess(data); + } else { + getView().addFoodWeightFail(jsonObject.optString("message")); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().addFoodWeightFail("数据解析异常"); + } + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().addFoodWeightFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); +// SwService.addFoodTotalWeight(foodId, type, totalWeight, deviceMac) +// .compose(RxSchedulersHelper.io_main()) +// .compose(RxResultHelper.handleJsonResponse()) +// .subscribe(new RxObserver() { +// @Override +// public void _onSubscribe(Disposable d) { +// } +// +// @Override +// public void _onNext(String info) { +// try { +// JSONObject jsonObject = new JSONObject(info); +// if (jsonObject.optInt("code") == 200) { +// String data = jsonObject.optString("data"); +// if (getView() != null) +// getView().addFoodWeightSuccess(data); +// } +// } catch (JSONException e) { +// e.printStackTrace(); +// if (getView() != null) +// getView().addFoodWeightFail("数据解析异常"); +// } +// } +// +// @Override +// public void _onError(String errorMessage) { +// if (getView() != null) +// getView().addFoodWeightFail(errorMessage); +// } +// +// @Override +// public void _onComplete() { +// } +// }); + } + + public void onLineRenewal(String deviceMac) { + SwService.onLineRenewal(deviceMac) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + + } + + @Override + public void _onError(String errorMessage) { + + } + + @Override + public void _onComplete() { + } + }); + } +} diff --git a/app/src/main/java/com/sw/st/ui/login/LoginRegistView.java b/app/src/main/java/com/sw/st/ui/login/LoginRegistView.java new file mode 100644 index 0000000..33d0987 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/LoginRegistView.java @@ -0,0 +1,61 @@ +package com.sw.st.ui.login; + + +import com.sw.st.base.BaseView; +import com.sw.st.model.DeviceMacModel; +import com.sw.st.model.DinnerType; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.MealRecordsInfo; +import com.sw.st.model.UserFaceModel; +import com.sw.st.model.UserInfo; +import com.sw.st.model.UserNutritionInfo; + +import java.util.ArrayList; + +public interface LoginRegistView extends BaseView { + + void upFaceFeatureSuccess(); + + void upFaceFeatureFail(String info); + + void getFaceFeatureSuccess(ArrayList list); + + void getFaceFeatureFail(String info); + + void getUserInfoSuccess(UserInfo userModel); + + void getUserInfoFail(String info); + + void getUserNutritionInfoSuccess(UserNutritionInfo userNutritionInfo); + + void getUserNutritionInfoFail(String info); + + void getDeviceMacSuccess(DeviceMacModel deviceMacModel); + + void getDeviceMacFail(String info); + + void getFoodInfoByMacSuccess(FoodInfoModel foodInfoModel); + + void getFoodInfoByMacFail(String info); + + void getMealRecordsInfoSuccess(MealRecordsInfo mealRecordsInfo); + + void getMealRecordsInfoFail(String info); + + void getDinnerTypeSuccess(DinnerType dinnerType); + + void getDinnerTypeFail(String info); + + void createRecordSuccess(String info); + + void createRecordFail(String info); + + + void getTokenSuccess(String info); + + void getTokenFail(String info); + + void addFoodWeightSuccess(String info); + + void addFoodWeightFail(String info); +} diff --git a/app/src/main/java/com/sw/st/ui/login/MyAxisValueFormatter.java b/app/src/main/java/com/sw/st/ui/login/MyAxisValueFormatter.java new file mode 100644 index 0000000..f58ca39 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/MyAxisValueFormatter.java @@ -0,0 +1,21 @@ +package com.sw.st.ui.login; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; + +import java.text.DecimalFormat; + +public class MyAxisValueFormatter implements IAxisValueFormatter +{ + + private final DecimalFormat mFormat; + + public MyAxisValueFormatter() { + mFormat = new DecimalFormat("###,###,###,##0.0"); + } + + @Override + public String getFormattedValue(float value, AxisBase axis) { + return mFormat.format(value) + " g"; + } +} diff --git a/app/src/main/java/com/sw/st/ui/login/MyFoodTagAdapters.java b/app/src/main/java/com/sw/st/ui/login/MyFoodTagAdapters.java new file mode 100644 index 0000000..cd62ac8 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/MyFoodTagAdapters.java @@ -0,0 +1,51 @@ +package com.sw.st.ui.login; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import com.sw.st.R; +import com.sw.st.ui.setting.FoodListModel; +import com.zhy.view.flowlayout.FlowLayout; +import com.zhy.view.flowlayout.TagAdapter; + +import java.util.List; + +/** + * Created by Administrator on 2019/4/23 0023. + */ + +public class MyFoodTagAdapters extends TagAdapter { + + private Context context; + + public MyFoodTagAdapters(Context context, List datas) { + super(datas); + this.context = context; + } + + @Override + public View getView(FlowLayout parent, int position, String tag) { + LayoutInflater inflater = LayoutInflater.from(context); + TextView tv = (TextView) inflater.inflate(R.layout.item_food_tag_flow_tv, parent, false); + switch (position % 3) { + case 0: + tv.setBackground(context.getResources().getDrawable(R.drawable.bg_food_tag_red)); + tv.setTextColor(0xFFFE4343); + break; + case 1: + tv.setBackground(context.getResources().getDrawable(R.drawable.bg_food_tag_yellow)); + tv.setTextColor(0xFFDDCC0A); + break; + case 2: + tv.setBackground(context.getResources().getDrawable(R.drawable.bg_food_tag_blue)); + tv.setTextColor(0xFF2CABF0); + break; + } + tv.setText(tag); + return tv; + } + + +} diff --git a/app/src/main/java/com/sw/st/ui/login/MyXValueFormatter.java b/app/src/main/java/com/sw/st/ui/login/MyXValueFormatter.java new file mode 100644 index 0000000..940dc37 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/MyXValueFormatter.java @@ -0,0 +1,32 @@ +package com.sw.st.ui.login; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; + +import java.text.DecimalFormat; + +public class MyXValueFormatter implements IAxisValueFormatter { + + private final DecimalFormat mFormat; + + public MyXValueFormatter() { + mFormat = new DecimalFormat("###,###,###,##0.0"); + } + + @Override + public String getFormattedValue(float value, AxisBase axis) { + if (value == 1) { + return "脂肪"; + } + if (value == 2) { + return "蛋白质"; + } + if (value == 3) { + return "碳水"; + } + if (value == 4) { + return "钠"; + } + return mFormat.format(value) + " g"; + } +} diff --git a/app/src/main/java/com/sw/st/ui/login/SelectIdentityActivity.java b/app/src/main/java/com/sw/st/ui/login/SelectIdentityActivity.java new file mode 100644 index 0000000..3b72ac4 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/SelectIdentityActivity.java @@ -0,0 +1,69 @@ +package com.sw.st.ui.login; + +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.sw.st.R; +import com.sw.st.base.BaseActivity; + +import butterknife.BindView; +import butterknife.OnClick; + +public class SelectIdentityActivity extends BaseActivity { + + @BindView(R.id.vip_mobile_edittext) + EditText mobileEditText; + @BindView(R.id.vip_mobile_layout) + LinearLayout mobileLayout; + @BindView(R.id.anonymous_button) + Button anonymousButton; + + @Override + protected int provideContentViewId() { + return R.layout.activity_select_identity; + } + + @Override + public void initView() { + + } + + @Override + public void initData() { + + } + + @Override + public void initListener() { + + } + + + @OnClick({R.id.vip_button, R.id.mobile_ok_button, R.id.anonymous_button}) + public void onViewClicked(View view) { + switch (view.getId()) { + case R.id.vip_button: + mobileLayout.setVisibility(View.VISIBLE); + anonymousButton.setVisibility(View.GONE); + break; + case R.id.mobile_ok_button: + finish(); + break; + case R.id.anonymous_button: + finish(); + break; + } + } + + @Override + protected SelectIdentityPresenter createPresenter() { + return new SelectIdentityPresenter(); + } + + @Override + protected void afterRequestPermission(int requestCode, boolean isAllGranted) { + + } +} diff --git a/app/src/main/java/com/sw/st/ui/login/SelectIdentityPresenter.java b/app/src/main/java/com/sw/st/ui/login/SelectIdentityPresenter.java new file mode 100644 index 0000000..c212fa7 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/SelectIdentityPresenter.java @@ -0,0 +1,13 @@ +package com.sw.st.ui.login; + + +import com.sw.st.base.BasePresenter; + +/** + * 登录注册 + */ + +public class SelectIdentityPresenter extends BasePresenter { + + +} diff --git a/app/src/main/java/com/sw/st/ui/login/SelectIdentityView.java b/app/src/main/java/com/sw/st/ui/login/SelectIdentityView.java new file mode 100644 index 0000000..694355b --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/login/SelectIdentityView.java @@ -0,0 +1,13 @@ +package com.sw.st.ui.login; + + +import com.sw.st.base.BaseView; + +public interface SelectIdentityView extends BaseView { + + void upFaceFeatureSuccess(); + + void upFaceFeatureFail(String info); + + +} diff --git a/app/src/main/java/com/sw/st/ui/rfid/RfidBindActivity.java b/app/src/main/java/com/sw/st/ui/rfid/RfidBindActivity.java new file mode 100644 index 0000000..0893a1d --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/rfid/RfidBindActivity.java @@ -0,0 +1,1076 @@ +//package com.sw.st.ui.rfid; +// +//import android.Manifest; +//import android.content.Intent; +//import android.graphics.Bitmap; +//import android.graphics.Point; +//import android.graphics.Rect; +//import android.hardware.Camera; +//import android.util.Log; +//import android.view.View; +//import android.view.ViewTreeObserver; +//import android.view.animation.Animation; +//import android.view.animation.AnimationUtils; +//import android.widget.ImageView; +//import android.widget.LinearLayout; +//import android.widget.TextView; +// +//import androidx.annotation.Nullable; +//import androidx.core.app.ActivityCompat; +// +//import com.arcsoft.face.AgeInfo; +//import com.arcsoft.face.ErrorInfo; +//import com.arcsoft.face.FaceEngine; +//import com.arcsoft.face.FaceFeature; +//import com.arcsoft.face.FaceInfo; +//import com.arcsoft.face.GenderInfo; +//import com.arcsoft.face.LivenessInfo; +//import com.arcsoft.face.enums.DetectFaceOrientPriority; +//import com.arcsoft.face.enums.DetectMode; +//import com.gyf.immersionbar.BarHide; +//import com.gyf.immersionbar.ImmersionBar; +//import com.sw.st.R; +//import com.sw.st.base.BaseActivity; +//import com.sw.st.utils.AppUtil; +//import com.sw.st.utils.L; +//import com.sw.st.utils.PrefUtils; +//import com.sw.st.utils.arcface.ConfigUtil; +//import com.sw.st.utils.arcface.DrawHelper; +//import com.sw.st.utils.arcface.DrawInfo; +//import com.sw.st.utils.arcface.camera.CameraListener; +//import com.sw.st.utils.arcface.camera.DualCameraHelper; +//import com.sw.st.utils.arcface.face.FaceHelper; +//import com.sw.st.utils.arcface.face.FaceListener; +//import com.sw.st.utils.arcface.face.LivenessType; +//import com.sw.st.utils.arcface.face.RecognizeColor; +//import com.sw.st.utils.arcface.face.RequestFeatureStatus; +//import com.sw.st.utils.arcface.face.RequestLivenessStatus; +//import com.sw.st.utils.arcface.model.FacePreviewInfo; +//import com.sw.st.utils.faceserver.CompareResult; +//import com.sw.st.utils.faceserver.FaceServer; +//import com.sw.st.view.FaceRectView; +// +//import org.greenrobot.eventbus.EventBus; +//import org.greenrobot.eventbus.Subscribe; +//import org.greenrobot.eventbus.ThreadMode; +// +//import java.io.ByteArrayOutputStream; +//import java.util.ArrayList; +//import java.util.Enumeration; +//import java.util.List; +//import java.util.Map; +//import java.util.concurrent.ConcurrentHashMap; +//import java.util.concurrent.TimeUnit; +// +//import butterknife.BindView; +//import butterknife.OnClick; +//import io.reactivex.Observable; +//import io.reactivex.ObservableEmitter; +//import io.reactivex.ObservableOnSubscribe; +//import io.reactivex.Observer; +//import io.reactivex.android.schedulers.AndroidSchedulers; +//import io.reactivex.disposables.CompositeDisposable; +//import io.reactivex.disposables.Disposable; +//import io.reactivex.schedulers.Schedulers; +// +//import static com.sw.st.ui.setting.FoodSettingActivity.REST_NUM; +// +//public class RfidBindActivity extends BaseActivity +// implements RfidBindView, ViewTreeObserver.OnGlobalLayoutListener { +// private String TAG = "RfidBindActivity"; +// +// private final String GET_RFID = "getRfid"; +// +// private static final int MAX_DETECT_NUM = 5; +// /** +// * 当FR成功,活体未成功时,FR等待活体的时间 +// */ +// private static final int WAIT_LIVENESS_INTERVAL = 100; +// /** +// * 失败重试间隔时间(ms) +// */ +// private static final long FAIL_RETRY_INTERVAL = 1000; +// /** +// * 出错重试最大次数 +// */ +// private static final int MAX_RETRY_TIME = 3; +// +// private DualCameraHelper cameraHelper; +// private DrawHelper drawHelper; +// private Camera.Size previewSize; +// /** +// * 优先打开的摄像头,本界面主要用于单目RGB摄像头设备,因此默认打开前置 +// */ +//// private Integer rgbCameraID = Camera.CameraInfo.CAMERA_FACING_FRONT; +// private Integer rgbCameraID = Camera.CameraInfo.CAMERA_FACING_BACK; +// private Integer cameraIrId = Camera.CameraInfo.CAMERA_FACING_FRONT; +// +// private DualCameraHelper cameraHelperIr; +// +// private volatile byte[] irData; +// +// /** +// * VIDEO模式人脸检测引擎,用于预览帧人脸追踪 +// */ +// private FaceEngine ftEngine; +// /** +// * 用于特征提取的引擎 +// */ +// private FaceEngine frEngine; +// /** +// * IMAGE模式活体检测引擎,用于预览帧人脸活体检测 +// */ +// private FaceEngine flEngine; +// +// private int ftInitCode = -1; +// private int frInitCode = -1; +// private int flInitCode = -1; +// private FaceHelper faceHelper; +// private List compareResultList; +// /** +// * 活体检测的开关 +// */ +// private boolean livenessDetect = false; +// /** +// * 注册人脸状态码,准备注册 +// */ +// private static final int REGISTER_STATUS_READY = 0; +// /** +// * 注册人脸状态码,注册中 +// */ +// private static final int REGISTER_STATUS_PROCESSING = 1; +// /** +// * 注册人脸状态码,注册结束(无论成功失败) +// */ +// private static final int REGISTER_STATUS_DONE = 2; +// +// private int registerStatus = REGISTER_STATUS_DONE; +// /** +// * 用于记录人脸识别相关状态 +// */ +// private ConcurrentHashMap requestFeatureStatusMap = new ConcurrentHashMap<>(); +// /** +// * 用于记录人脸特征提取出错重试次数 +// */ +// private ConcurrentHashMap extractErrorRetryMap = new ConcurrentHashMap<>(); +// /** +// * 用于存储活体值 +// */ +// private ConcurrentHashMap livenessMap = new ConcurrentHashMap<>(); +// /** +// * 用于存储活体检测出错重试次数 +// */ +// private ConcurrentHashMap livenessErrorRetryMap = new ConcurrentHashMap<>(); +// +// private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable(); +// private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable(); +// /** +// * 相机预览显示的控件,可为SurfaceView或TextureView +// */ +// private View previewView; +// private View previewViewIr; +// /** +// * 绘制人脸框的控件 +// */ +// private FaceRectView faceRectView; +// +// private static final int ACTION_REQUEST_PERMISSIONS = 0x001; +// /** +// * 识别阈值 +// */ +// private static final float SIMILAR_THRESHOLD = 0.8F; +// /** +// * 所需的所有权限信息 +// */ +// private static final String[] NEEDED_PERMISSIONS = new String[]{ +// Manifest.permission.CAMERA, +// Manifest.permission.READ_PHONE_STATE, +// Manifest.permission.WRITE_EXTERNAL_STORAGE, +// Manifest.permission.READ_EXTERNAL_STORAGE +// }; +// +// @BindView(R.id.registerScanCamera) +// LinearLayout scanCamere; +// @BindView(R.id.cancel) +// ImageView cancelImg; +// @BindView(R.id.name) +// TextView nameTv; +// @BindView(R.id.phone) +// TextView phoneTv; +// @BindView(R.id.level) +// TextView levelTv; +// @BindView(R.id.money) +// TextView moneyTv; +// @BindView(R.id.camera_tips) +// TextView cameraTipsTv; +// @BindView(R.id.scan_center_circle) +// ImageView centerScanCircle; +// @BindView(R.id.scan_success_layout) +// LinearLayout scanSuccessLayout; +// +//// private UserInfo mUserInfo; +//// private UserFaceInfo currentUserFaceInfo; +// +// private String userId = ""; +// private String rfId = ""; +// +// @Override +// protected int provideContentViewId() { +// return R.layout.activity_rfid_bind; +// } +// +// @Override +// public void init() { +// super.init(); +// +// ImmersionBar.with(this) +// .hideBar(BarHide.FLAG_HIDE_BAR) +// .statusBarAlpha(0f) +// .statusBarDarkFont(true) +// .statusBarColor(R.color.white) +// .init(); +// +// rfId = getIntent().getStringExtra("rfid"); +//// mUserInfo = (UserInfo) getIntent().getSerializableExtra("userInfo"); +//// memberId = mUserInfo.getMemberId(); +//// userId = mUserInfo.getUserId(); +// //本地人脸库初始化 +// FaceServer.getInstance().init(this); +// +// EventBus.getDefault().register(this); +// } +// +// private final String RFID_DEFAULT = "00000000"; +// +// @Subscribe(threadMode = ThreadMode.MAIN) +// public void onReceiveMsg(String rfId) { +// if (rfId.equals(RFID_DEFAULT)) { +// finish(); +// } +// Log.e(TAG, "onReceiveMsg: " + rfId); +// } +// +// +// @Override +// public void initView() { +// previewView = findViewById(R.id.single_camera_texture_preview); +// previewViewIr = findViewById(R.id.dual_camera_texture_previewIr); +// //在布局结束后才做初始化操作 +// previewView.getViewTreeObserver().addOnGlobalLayoutListener(this); +// +// faceRectView = findViewById(R.id.single_camera_face_rect_view); +// compareResultList = new ArrayList<>(); +// +//// if (AppUtil.isEmpty(mUserInfo.getName())) { +//// nameTv.setVisibility(View.GONE); +//// } else { +//// nameTv.setVisibility(View.VISIBLE); +//// nameTv.setText("会员姓名:" + mUserInfo.getName()); +//// } +//// if (AppUtil.isEmpty(mUserInfo.getPhone())) { +//// phoneTv.setVisibility(View.GONE); +//// } else { +//// phoneTv.setVisibility(View.VISIBLE); +//// phoneTv.setTag(mUserInfo.getMemberId()); +//// phoneTv.setText("手机号码:" + mUserInfo.getPhone()); +//// } +//// +//// levelTv.setText("会员等级:" + mUserInfo.getMemberLevel()); +//// moneyTv.setText("账户余额:" + DoubleUtil.sum(mUserInfo.getTopUpBalance(), mUserInfo.getRewardBalance())); +// } +// +// @Override +// public void initData() { +// Animation rotate = AnimationUtils.loadAnimation(this, R.anim.rotate_anim); +// if (rotate != null) { +// centerScanCircle.startAnimation(rotate); +// } else { +// centerScanCircle.setAnimation(rotate); +// centerScanCircle.startAnimation(rotate); +// } +// } +// +// @Override +// public void initListener() { +// +// } +// +// @OnClick({R.id.backImg, R.id.registerScanCamera, R.id.cancel, R.id.okButton, R.id.camera_layout}) +// public void onViewClicked(View view) { +// switch (view.getId()) { +// case R.id.backImg: +// finish(); +// break; +// case R.id.registerScanCamera: +// registerStatus = REGISTER_STATUS_READY; +// cancelImg.setVisibility(View.VISIBLE); +// scanCamere.setVisibility(View.GONE); +// break; +// case R.id.cancel: +// if (cameraHelper != null) { +// cameraHelper.start(); +// } +// if (cameraHelperIr != null) { +// cameraHelperIr.start(); +// } +//// currentUserFaceInfo = null; +// cancelImg.setVisibility(View.GONE); +// scanCamere.setVisibility(View.VISIBLE); +// break; +// case R.id.okButton: +// Intent intent = new Intent(RfidBindActivity.this, RfidMainActivity.class); +// intent.putExtra("userId", userId); +// startActivity(intent); +// finish(); +// break; +// case R.id.camera_layout: +// cameraTipsTv.setVisibility(View.GONE); +// try { +// if (cameraHelper != null) { +// cameraHelper.start(); +// } +// if (cameraHelperIr != null) { +// cameraHelperIr.start(); +// } +// } catch (RuntimeException e) { +// showToast(e.getMessage() + getString(R.string.camera_error_notice)); +// } +// cancelImg.setVisibility(View.GONE); +// scanCamere.setVisibility(View.VISIBLE); +// break; +// } +// } +// +// /** +// * bitmap 转 byte[]数组 +// */ +// public byte[] bitmap2byteArray(Bitmap bitmap) { +// ByteArrayOutputStream baos = new ByteArrayOutputStream(); +// bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); +// byte[] bytes = baos.toByteArray(); +// return bytes; +// } +// +// @Override +// protected void onResume() { +// super.onResume(); +// try { +// if (cameraHelper != null) { +// cameraHelper.start(); +// } +// if (cameraHelperIr != null) { +// cameraHelperIr.start(); +// } +// } catch (RuntimeException e) { +// showToast(e.getMessage() + getString(R.string.camera_error_notice)); +// } +// } +// +// @Override +// protected void onPause() { +// super.onPause(); +// if (cameraHelper != null) { +// cameraHelper.stop(); +// } +// if (cameraHelperIr != null) { +// cameraHelperIr.stop(); +// } +// } +// +// @Override +// protected void onDestroy() { +// if (cameraHelper != null) { +// cameraHelper.release(); +// cameraHelper = null; +// } +// if (cameraHelperIr != null) { +// cameraHelperIr.release(); +// cameraHelperIr = null; +// } +// +// unInitEngine(); +// if (getFeatureDelayedDisposables != null) { +// getFeatureDelayedDisposables.clear(); +// } +// if (delayFaceTaskCompositeDisposable != null) { +// delayFaceTaskCompositeDisposable.clear(); +// } +// if (faceHelper != null) { +// ConfigUtil.setTrackedFaceCount(this, faceHelper.getTrackedFaceCount()); +// faceHelper.release(); +// faceHelper = null; +// } +// +// FaceServer.getInstance().unInit(); +// +// EventBus.getDefault().unregister(this); +// super.onDestroy(); +// } +// +// @Override +// protected RfidBindPresenter createPresenter() { +// return new RfidBindPresenter(); +// } +// +// @Override +// protected void afterRequestPermission(int requestCode, boolean isAllGranted) { +// if (requestCode == ACTION_REQUEST_PERMISSIONS) { +// if (isAllGranted) { +// initEngine(); +// initCamera(); +// initIrCamera(); +// } else { +// showToast(getString(R.string.permission_denied)); +// } +// } +// } +// +// @Override +// public void onGlobalLayout() { +// previewView.getViewTreeObserver().removeOnGlobalLayoutListener(this); +// if (!checkPermissions(NEEDED_PERMISSIONS)) { +// ActivityCompat.requestPermissions(this, NEEDED_PERMISSIONS, ACTION_REQUEST_PERMISSIONS); +// } else { +// initEngine(); +// initCamera(); +// initIrCamera(); +// } +// } +// +// +// @Override +// public void bindUserFail(String info) { +// +// } +// +// @Override +// public void bindUserSuccess(String info) { +//// scanSuccessLayout.setVisibility(View.VISIBLE); +// setResult(1001, new Intent().putExtra("userId", userId)); +// finish(); +// } +// +// @Override +// public void showProgress(String tipString) { +// +// } +// +// @Override +// public void hideProgress() { +// +// } +// +// +// /** +// * 初始化引擎 +// */ +// private void initEngine() { +// ftEngine = new FaceEngine(); +// ftInitCode = ftEngine.init(this, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(this), +// 16, MAX_DETECT_NUM, FaceEngine.ASF_FACE_DETECT); +// +// frEngine = new FaceEngine(); +// frInitCode = frEngine.init(this, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, +// 16, MAX_DETECT_NUM, FaceEngine.ASF_FACE_RECOGNITION); +// +// flEngine = new FaceEngine(); +// //使用普通摄像头检测活体 +//// flInitCode = flEngine.init(this, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, +//// 16, MAX_DETECT_NUM, FaceEngine.ASF_LIVENESS); +// //使用红外摄像头检测活体 +// flInitCode = flEngine.init(this, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, +// 16, MAX_DETECT_NUM, FaceEngine.ASF_IR_LIVENESS); +// +// L.i("initEngine: init: " + ftInitCode); +// +// if (ftInitCode != ErrorInfo.MOK) { +// String error = getString(R.string.specific_engine_init_failed, "ftEngine", ftInitCode); +// L.i("initEngine: " + error); +// showToast(error); +// } +// if (frInitCode != ErrorInfo.MOK) { +// String error = getString(R.string.specific_engine_init_failed, "frEngine", frInitCode); +// L.i("initEngine: " + error); +// showToast(error); +// } +// if (flInitCode != ErrorInfo.MOK) { +// String error = getString(R.string.specific_engine_init_failed, "flEngine", flInitCode); +// L.i("initEngine: " + error); +// showToast(error); +// } +// } +// +// /** +// * 销毁引擎,faceHelper中可能会有特征提取耗时操作仍在执行,加锁防止crash +// */ +// private void unInitEngine() { +// if (ftInitCode == ErrorInfo.MOK && ftEngine != null) { +// synchronized (ftEngine) { +// int ftUnInitCode = ftEngine.unInit(); +// L.i("unInitEngine: " + ftUnInitCode); +// } +// } +// if (frInitCode == ErrorInfo.MOK && frEngine != null) { +// synchronized (frEngine) { +// int frUnInitCode = frEngine.unInit(); +// L.i("unInitEngine: " + frUnInitCode); +// } +// } +// if (flInitCode == ErrorInfo.MOK && flEngine != null) { +// synchronized (flEngine) { +// int flUnInitCode = flEngine.unInit(); +// L.i("unInitEngine: " + flUnInitCode); +// } +// } +// } +// +// private boolean isHasFace = false; +// +// private void initCamera() { +//// DisplayMetrics metrics = new DisplayMetrics(); +//// getWindowManager().getDefaultDisplay().getMetrics(metrics); +// +// final FaceListener faceListener = new FaceListener() { +// @Override +// public void onFail(Exception e) { +// L.e("onFail: " + e.getMessage()); +// } +// +// //请求FR的回调 +// @Override +// public void onFaceFeatureInfoGet(@Nullable final FaceFeature faceFeature, final Integer requestId, final Integer errorCode) { +// //FR成功 +// if (faceFeature != null) { +//// L.i( "onPreview: fr end = " + System.currentTimeMillis() + " trackId = " + requestId); +// Integer liveness = livenessMap.get(requestId); +// //不做活体检测的情况,直接搜索 +// if (!livenessDetect) { +// searchFace(faceFeature, requestId); +// } +// //活体检测通过,搜索特征 +// else if (liveness != null && liveness == LivenessInfo.ALIVE) { +// searchFace(faceFeature, requestId); +// } +// //活体检测未出结果,或者非活体,延迟执行该函数 +// else { +// if (requestFeatureStatusMap.containsKey(requestId)) { +// Observable.timer(WAIT_LIVENESS_INTERVAL, TimeUnit.MILLISECONDS) +// .subscribe(new Observer() { +// Disposable disposable; +// +// @Override +// public void onSubscribe(Disposable d) { +// disposable = d; +// getFeatureDelayedDisposables.add(disposable); +// } +// +// @Override +// public void onNext(Long aLong) { +// onFaceFeatureInfoGet(faceFeature, requestId, errorCode); +// } +// +// @Override +// public void onError(Throwable e) { +// +// } +// +// @Override +// public void onComplete() { +// getFeatureDelayedDisposables.remove(disposable); +// } +// }); +// } +// } +// +// } +// //特征提取失败 +// else { +// if (increaseAndGetValue(extractErrorRetryMap, requestId) > MAX_RETRY_TIME) { +// extractErrorRetryMap.put(requestId, 0); +// +// String msg; +// // 传入的FaceInfo在指定的图像上无法解析人脸,此处使用的是RGB人脸数据,一般是人脸模糊 +// if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) { +// msg = getString(R.string.low_confidence_level); +// } else { +// msg = "ExtractCode:" + errorCode; +// } +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, msg)); +// // 在尝试最大次数后,特征提取仍然失败,则认为识别未通过 +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); +// retryRecognizeDelayed(requestId); +// } else { +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.TO_RETRY); +// } +// } +// } +// +// @Override +// public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, final Integer requestId, Integer errorCode) { +// if (livenessInfo != null) { +// int liveness = livenessInfo.getLiveness(); +// livenessMap.put(requestId, liveness); +// // 非活体,重试 +// if (liveness == LivenessInfo.NOT_ALIVE) { +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, "NOT_ALIVE")); +// // 延迟 FAIL_RETRY_INTERVAL 后,将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 +// retryLivenessDetectDelayed(requestId); +// } +// } else { +// if (increaseAndGetValue(livenessErrorRetryMap, requestId) > MAX_RETRY_TIME) { +// livenessErrorRetryMap.put(requestId, 0); +// String msg; +// // 传入的FaceInfo在指定的图像上无法解析人脸,此处使用的是RGB人脸数据,一般是人脸模糊 +// if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) { +// msg = getString(R.string.low_confidence_level); +// } else { +// msg = "ProcessCode:" + errorCode; +// } +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, msg)); +// retryLivenessDetectDelayed(requestId); +// } else { +// livenessMap.put(requestId, LivenessInfo.UNKNOWN); +// } +// } +// } +// +// +// }; +// +// +// CameraListener cameraListener = new CameraListener() { +// @Override +// public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) { +// Camera.Size lastPreviewSize = previewSize; +// previewSize = camera.getParameters().getPreviewSize(); +// drawHelper = new DrawHelper(previewSize.width, previewSize.height, previewView.getWidth(), previewView.getHeight(), displayOrientation +// , cameraId, isMirror, false, false); +// L.i("onCameraOpened: " + drawHelper.toString()); +// // 切换相机的时候可能会导致预览尺寸发生变化 +// if (faceHelper == null || +// lastPreviewSize == null || +// lastPreviewSize.width != previewSize.width || lastPreviewSize.height != previewSize.height) { +// Integer trackedFaceCount = null; +// // 记录切换时的人脸序号 +// if (faceHelper != null) { +// trackedFaceCount = faceHelper.getTrackedFaceCount(); +// faceHelper.release(); +// } +// faceHelper = new FaceHelper.Builder() +// .ftEngine(ftEngine) +// .frEngine(frEngine) +// .flEngine(flEngine) +// .frQueueSize(MAX_DETECT_NUM) +// .flQueueSize(MAX_DETECT_NUM) +// .previewSize(previewSize) +// .faceListener(faceListener) +// .trackedFaceCount(trackedFaceCount == null ? ConfigUtil.getTrackedFaceCount(RfidBindActivity.this) : trackedFaceCount) +// .build(); +// } +// } +// +// +// @Override +// public void onPreview(final byte[] nv21, Camera camera) { +// if (faceRectView != null) { +// faceRectView.clearFaceInfo(); +// } +// List facePreviewInfoList = faceHelper.onPreviewFrame(nv21); +// if (facePreviewInfoList != null && faceRectView != null && drawHelper != null) { +// drawPreviewInfo(facePreviewInfoList); +// } +//// registerFace(nv21, facePreviewInfoList); +// clearLeftFace(facePreviewInfoList); +// +// if (facePreviewInfoList != null && facePreviewInfoList.size() > 0 && previewSize != null) { +// isHasFace = true; +// for (int i = 0; i < facePreviewInfoList.size(); i++) { +// Rect faceRect = facePreviewInfoList.get(i).getFaceInfo().getRect(); +//// L.e("mzf", "faceRect=" + i + "---" + faceRect.width() + "===" + faceRect.height()); +// Integer status = requestFeatureStatusMap.get(facePreviewInfoList.get(i).getTrackId()); +// /** +// * 在活体检测开启,在人脸识别状态不为成功或人脸活体状态不为处理中(ANALYZING)且不为处理完成(ALIVE、NOT_ALIVE)时重新进行活体检测 +// */ +// if (livenessDetect && (status == null || status != RequestFeatureStatus.SUCCEED)) { +// Integer liveness = livenessMap.get(facePreviewInfoList.get(i).getTrackId()); +// if (liveness == null +// || (liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE && liveness != RequestLivenessStatus.ANALYZING)) { +// livenessMap.put(facePreviewInfoList.get(i).getTrackId(), RequestLivenessStatus.ANALYZING); +// if (irData != null) { +// FaceInfo faceInfo = facePreviewInfoList.get(i).getFaceInfo().clone(); +// faceInfo.getRect().offset(0, 0); +// faceHelper.requestFaceLiveness(irData.clone(), faceInfo, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId(), LivenessType.IR); +// } else { +// faceHelper.requestFaceLiveness(nv21, facePreviewInfoList.get(i).getFaceInfo(), previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId(), LivenessType.RGB); +// } +// } +// } +// /** +// * 对于每个人脸,若状态为空或者为失败,则请求特征提取(可根据需要添加其他判断以限制特征提取次数), +// * 特征提取回传的人脸特征结果在{@link FaceListener#onFaceFeatureInfoGet(FaceFeature, Integer, Integer)}中回传 +// */ +// if (status == null +// || status == RequestFeatureStatus.TO_RETRY) { +// requestFeatureStatusMap.put(facePreviewInfoList.get(i).getTrackId(), RequestFeatureStatus.SEARCHING); +// faceHelper.requestFaceFeature(nv21, facePreviewInfoList.get(i).getFaceInfo(), previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, facePreviewInfoList.get(i).getTrackId()); +//// L.i( "onPreview: fr start = " + System.currentTimeMillis() + " trackId = " + facePreviewInfoList.get(i).getTrackedFaceCount()); +// } +// } +// } else { +// if (isHasFace) { +//// showToast("无人脸信息"); +// isHasFace = false; +// } +// } +// } +// +// @Override +// public void onCameraClosed() { +// L.i("onCameraClosed: "); +// } +// +// @Override +// public void onCameraError(Exception e) { +// L.i("onCameraError: " + e.getMessage()); +// } +// +// @Override +// public void onCameraConfigurationChanged(int cameraID, int displayOrientation) { +// if (drawHelper != null) { +// drawHelper.setCameraDisplayOrientation(displayOrientation); +// } +// L.i("onCameraConfigurationChanged: " + cameraID + " " + displayOrientation); +// } +// }; +// +//// cameraHelper = new CameraHelper.Builder() +//// .previewViewSize(new Point(previewView.getMeasuredWidth(), previewView.getMeasuredHeight())) +//// .rotation(getWindowManager().getDefaultDisplay().getRotation()) +//// .specificCameraId(rgbCameraID != null ? rgbCameraID : Camera.CameraInfo.CAMERA_FACING_FRONT) +//// .isMirror(false) +//// .previewOn(previewView) +//// .cameraListener(cameraListener) +//// .build(); +// cameraHelper = new DualCameraHelper.Builder() +// .previewViewSize(new Point(previewView.getMeasuredWidth(), previewView.getMeasuredHeight())) +// .rotation(getWindowManager().getDefaultDisplay().getRotation()) +// .specificCameraId(rgbCameraID != null ? rgbCameraID : Camera.CameraInfo.CAMERA_FACING_FRONT) +// .previewOn(previewView) +// .cameraListener(cameraListener) +//// .isMirror(rgbCameraID != null && Camera.CameraInfo.CAMERA_FACING_FRONT == rgbCameraID) +// .isMirror(true) +// .build(); +// cameraHelper.init(); +// cameraHelper.start(); +// +// } +// +// private void initIrCamera() { +// CameraListener irCameraListener = new CameraListener() { +// @Override +// public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) { +// +// } +// +// +// @Override +// public void onPreview(final byte[] nv21, Camera camera) { +// irData = nv21; +// } +// +// @Override +// public void onCameraClosed() { +// Log.i(TAG, "onCameraClosed: "); +// } +// +// @Override +// public void onCameraError(Exception e) { +// Log.i(TAG, "onCameraError: " + e.getMessage()); +// } +// +// @Override +// public void onCameraConfigurationChanged(int cameraID, int displayOrientation) { +// Log.i(TAG, "onCameraConfigurationChanged: " + cameraID + " " + displayOrientation); +// } +// }; +// +// cameraHelperIr = new DualCameraHelper.Builder() +// .previewViewSize(new Point(previewViewIr.getMeasuredWidth(), previewViewIr.getMeasuredHeight())) +// .rotation(getWindowManager().getDefaultDisplay().getRotation()) +// .specificCameraId(cameraIrId != null ? cameraIrId : Camera.CameraInfo.CAMERA_FACING_FRONT) +// .previewOn(previewViewIr) +// .cameraListener(irCameraListener) +// .isMirror(false) +//// .previewSize(new Point(1280, 960)) //相机预览大小设置,RGB与IR需使用相同大小 +//// .additionalRotation(180) //额外旋转角度 +// .build(); +// cameraHelperIr.init(); +// try { +// cameraHelperIr.start(); +// } catch (RuntimeException e) { +// showToast(e.getMessage() + getString(R.string.camera_error_notice)); +// } +// } +// +// +// private void drawPreviewInfo(List facePreviewInfoList) { +// List drawInfoList = new ArrayList<>(); +// for (int i = 0; i < facePreviewInfoList.size(); i++) { +// String name = faceHelper.getName(facePreviewInfoList.get(i).getTrackId()); +// Integer liveness = livenessMap.get(facePreviewInfoList.get(i).getTrackId()); +// Integer recognizeStatus = requestFeatureStatusMap.get(facePreviewInfoList.get(i).getTrackId()); +// +// // 根据识别结果和活体结果设置颜色 +// int color = RecognizeColor.COLOR_UNKNOWN; +// if (recognizeStatus != null) { +// if (recognizeStatus == RequestFeatureStatus.FAILED) { +// color = RecognizeColor.COLOR_FAILED; +// } +// if (recognizeStatus == RequestFeatureStatus.SUCCEED) { +// color = RecognizeColor.COLOR_SUCCESS; +// } +// } +// if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) { +// color = RecognizeColor.COLOR_FAILED; +// } +// +// drawInfoList.add(new DrawInfo(drawHelper.adjustRect(facePreviewInfoList.get(i).getFaceInfo().getRect()), +// GenderInfo.UNKNOWN, AgeInfo.UNKNOWN_AGE, liveness == null ? LivenessInfo.UNKNOWN : liveness, color, +// name == null ? String.valueOf(facePreviewInfoList.get(i).getTrackId()) : name)); +// } +// drawHelper.draw(faceRectView, drawInfoList); +// } +// +// +// /** +// * 删除已经离开的人脸 +// * +// * @param facePreviewInfoList 人脸和trackId列表 +// */ +// private void clearLeftFace(List facePreviewInfoList) { +// if (compareResultList != null) { +// for (int i = compareResultList.size() - 1; i >= 0; i--) { +// if (!requestFeatureStatusMap.containsKey(compareResultList.get(i).getTrackId())) { +// compareResultList.remove(i); +//// adapter.notifyItemRemoved(i); +// } +// } +// } +// if (facePreviewInfoList == null || facePreviewInfoList.size() == 0) { +// requestFeatureStatusMap.clear(); +// livenessMap.clear(); +// livenessErrorRetryMap.clear(); +// extractErrorRetryMap.clear(); +// if (getFeatureDelayedDisposables != null) { +// getFeatureDelayedDisposables.clear(); +// } +// return; +// } +// Enumeration keys = requestFeatureStatusMap.keys(); +// while (keys.hasMoreElements()) { +// int key = keys.nextElement(); +// boolean contained = false; +// for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { +// if (facePreviewInfo.getTrackId() == key) { +// contained = true; +// break; +// } +// } +// if (!contained) { +// requestFeatureStatusMap.remove(key); +// livenessMap.remove(key); +// livenessErrorRetryMap.remove(key); +// extractErrorRetryMap.remove(key); +// } +// } +// +// +// } +// +// private void searchFace(final FaceFeature frFace, final Integer requestId) { +// Observable +// .create(new ObservableOnSubscribe() { +// @Override +// public void subscribe(ObservableEmitter emitter) { +//// L.i( "subscribe: fr search start = " + System.currentTimeMillis() + " trackId = " + requestId); +// CompareResult compareResult = FaceServer.getInstance().getTopOfFaceLib(frFace); +//// L.i( "subscribe: fr search end = " + System.currentTimeMillis() + " trackId = " + requestId); +// emitter.onNext(compareResult); +// +// } +// }) +// .subscribeOn(Schedulers.computation()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe(new Observer() { +// @Override +// public void onSubscribe(Disposable d) { +// +// } +// +// @Override +// public void onNext(CompareResult compareResult) { +// if (compareResult == null || compareResult.getUserName() == null) { +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); +// faceHelper.setName(requestId, "VISITOR " + requestId); +// return; +// } +// +//// L.i( "onNext: fr search get result = " + System.currentTimeMillis() + " trackId = " + requestId + " similar = " + compareResult.getSimilar()); +// if (compareResult.getSimilar() > SIMILAR_THRESHOLD) { +//// if (AppUtil.isEmpty(currentUid) +//// || !currentUid.equals(compareResult.getUserName())) { +//// createRecord(); +//// +// String currentUid = compareResult.getUserName(); +// +// if (!AppUtil.isEmpty(currentUid)) { +// L.e(currentUid.substring(0, currentUid.indexOf(":"))); +// userId = currentUid.substring(0, currentUid.indexOf(":")); +// mPresenter.rfidBindUser(userId, +// rfId, PrefUtils.getString(RfidBindActivity.this, REST_NUM, "")); +// } +// +//// } +// +// boolean isAdded = false; +// if (compareResultList == null) { +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); +// faceHelper.setName(requestId, "VISITOR " + requestId); +// +// +//// loadJs("javacalljswithargs", "");//未识别人脸信息 +// return; +// } +// for (CompareResult compareResult1 : compareResultList) { +// if (compareResult1.getTrackId() == requestId) { +// isAdded = true; +// break; +// } +// } +// if (!isAdded) { +// //对于多人脸搜索,假如最大显示数量为 MAX_DETECT_NUM 且有新的人脸进入,则以队列的形式移除 +// if (compareResultList.size() >= MAX_DETECT_NUM) { +// compareResultList.remove(0); +// } +// //添加显示人员时,保存其trackId +// compareResult.setTrackId(requestId); +// compareResultList.add(compareResult); +// } +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.SUCCEED); +// faceHelper.setName(requestId, getString(R.string.recognize_success_notice, compareResult.getUserName())); +// } else { +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, "NOT_REGISTERED")); +// retryRecognizeDelayed(requestId); +// +//// mPresenter.getUserFace();//未获取到用户,重新拉取人脸数据 +// } +// } +// +// @Override +// public void onError(Throwable e) { +// faceHelper.setName(requestId, getString(R.string.recognize_failed_notice, "NOT_REGISTERED")); +// retryRecognizeDelayed(requestId); +// +// } +// +// @Override +// public void onComplete() { +// +// } +// }); +// } +// +// /** +// * 将map中key对应的value增1回传 +// * +// * @param countMap map +// * @param key key +// * @return 增1后的value +// */ +// public int increaseAndGetValue(Map countMap, int key) { +// if (countMap == null) { +// return 0; +// } +// Integer value = countMap.get(key); +// if (value == null) { +// value = 0; +// } +// countMap.put(key, ++value); +// return value; +// } +// +// /** +// * 延迟 FAIL_RETRY_INTERVAL 重新进行活体检测 +// * +// * @param requestId 人脸ID +// */ +// private void retryLivenessDetectDelayed(final Integer requestId) { +// Observable.timer(FAIL_RETRY_INTERVAL, TimeUnit.MILLISECONDS) +// .subscribe(new Observer() { +// Disposable disposable; +// +// @Override +// public void onSubscribe(Disposable d) { +// disposable = d; +// delayFaceTaskCompositeDisposable.add(disposable); +// } +// +// @Override +// public void onNext(Long aLong) { +// +// } +// +// @Override +// public void onError(Throwable e) { +// e.printStackTrace(); +// } +// +// @Override +// public void onComplete() { +// // 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 +// if (livenessDetect) { +// faceHelper.setName(requestId, Integer.toString(requestId)); +// } +// livenessMap.put(requestId, LivenessInfo.UNKNOWN); +// delayFaceTaskCompositeDisposable.remove(disposable); +// } +// }); +// } +// +// /** +// * 延迟 FAIL_RETRY_INTERVAL 重新进行人脸识别 +// * +// * @param requestId 人脸ID +// */ +// private void retryRecognizeDelayed(final Integer requestId) { +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.FAILED); +// Observable.timer(FAIL_RETRY_INTERVAL, TimeUnit.MILLISECONDS) +// .subscribe(new Observer() { +// Disposable disposable; +// +// @Override +// public void onSubscribe(Disposable d) { +// disposable = d; +// delayFaceTaskCompositeDisposable.add(disposable); +// } +// +// @Override +// public void onNext(Long aLong) { +// +// } +// +// @Override +// public void onError(Throwable e) { +// e.printStackTrace(); +// } +// +// @Override +// public void onComplete() { +// // 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测 +// faceHelper.setName(requestId, Integer.toString(requestId)); +// requestFeatureStatusMap.put(requestId, RequestFeatureStatus.TO_RETRY); +// delayFaceTaskCompositeDisposable.remove(disposable); +// } +// }); +// } +//} diff --git a/app/src/main/java/com/sw/st/ui/rfid/RfidBindPresenter.java b/app/src/main/java/com/sw/st/ui/rfid/RfidBindPresenter.java new file mode 100644 index 0000000..a7fd560 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/rfid/RfidBindPresenter.java @@ -0,0 +1,97 @@ +package com.sw.st.ui.rfid; + + +import com.sw.st.api.SwService; +import com.sw.st.base.BasePresenter; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; + +import org.json.JSONException; +import org.json.JSONObject; + +import io.reactivex.disposables.Disposable; + +/** + * 登录注册 + */ + +public class RfidBindPresenter extends BasePresenter { + + public void rfidBindUser(String uid, + String rfid, + String restId) { + SwService.rfidBindUser(uid, rfid, restId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("result"); + if (getView() != null) { + getView().bindUserSuccess(data); + } + } else { + String message = jsonObject.optString("message"); + if (getView() != null) { + getView().bindUserFail(message); + } + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) { + getView().bindUserFail("数据解析异常"); + } + } + if (getView() != null) { + getView().hideProgress(); + } + } + + @Override + public void _onError(String errorMessage) { + getView().bindUserFail(errorMessage); + } + }); + } + + public void getUserInfoByFaceFeatureData(String faceFeature) { +// SwService.getUserInfoByFaceFeatureData(faceFeature) +// .compose(RxSchedulersHelper.io_main()) +// .compose(RxResultHelper.handleResult()) +// .subscribe(new RxObserver() { +// +// @Override +// public void _onSubscribe(Disposable d) { +// if (getView() != null) +// getView().showProgress("查询中..."); +// } +// +// @Override +// public void _onNext(UserRootModel userBean) { +// if (getView() != null) +// getView().getUserInfoSuccess(userBean); +// } +// +// @Override +// public void _onError(String errorMessage) { +// if (getView() != null) { +// getView().getUserInfoFail(errorMessage); +// +// getView().hideProgress(); +// } +// } +// +// @Override +// public void _onComplete() { +// if (getView() != null) +// getView().hideProgress(); +// } +// +// }); + } +} diff --git a/app/src/main/java/com/sw/st/ui/rfid/RfidBindView.java b/app/src/main/java/com/sw/st/ui/rfid/RfidBindView.java new file mode 100644 index 0000000..7dda988 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/rfid/RfidBindView.java @@ -0,0 +1,11 @@ +package com.sw.st.ui.rfid; + + +import com.sw.st.base.BaseView; + +public interface RfidBindView extends BaseView { + + void bindUserFail(String info); + + void bindUserSuccess(String info); +} diff --git a/app/src/main/java/com/sw/st/ui/rfid/RfidMainActivity.java b/app/src/main/java/com/sw/st/ui/rfid/RfidMainActivity.java new file mode 100644 index 0000000..6f29185 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/rfid/RfidMainActivity.java @@ -0,0 +1,849 @@ +//package com.sw.st.ui.rfid; +// +//import android.content.Intent; +//import android.os.CountDownTimer; +//import android.os.Handler; +//import android.os.Looper; +//import android.os.Message; +//import android.os.SystemClock; +//import android.util.Log; +//import android.view.View; +//import android.view.ViewTreeObserver; +//import android.widget.ImageView; +//import android.widget.LinearLayout; +//import android.widget.SeekBar; +//import android.widget.TextView; +// +//import androidx.annotation.Nullable; +// +//import com.bumptech.glide.Glide; +//import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +//import com.bumptech.glide.request.RequestOptions; +//import com.github.mikephil.charting.data.Entry; +//import com.github.mikephil.charting.highlight.Highlight; +//import com.github.mikephil.charting.listener.OnChartValueSelectedListener; +//import com.gyf.immersionbar.BarHide; +//import com.gyf.immersionbar.ImmersionBar; +//import com.innohi.YNHAPI; +//import com.sw.st.R; +//import com.sw.st.base.BaseActivity; +//import com.sw.st.model.DeviceMacModel; +//import com.sw.st.model.DinnerModel; +//import com.sw.st.model.DinnerType; +//import com.sw.st.model.FoodInfoModel; +//import com.sw.st.model.MealRecordsInfo; +//import com.sw.st.model.UserFaceModel; +//import com.sw.st.model.UserInfo; +//import com.sw.st.model.UserNutritionInfo; +//import com.sw.st.ui.device.DeviceActivity; +//import com.sw.st.ui.login.LoginRegistPresenter; +//import com.sw.st.ui.login.LoginRegistView; +//import com.sw.st.ui.login.MyFoodTagAdapters; +//import com.sw.st.ui.login.SelectIdentityActivity; +//import com.sw.st.ui.setting.FoodSettingActivity; +//import com.sw.st.utils.AppUtil; +//import com.sw.st.utils.DeviceIdUtil; +//import com.sw.st.utils.L; +//import com.sw.st.utils.PrefUtils; +//import com.sw.st.utils.T; +//import com.sw.st.utils.faceserver.FaceServer; +//import com.zhy.view.flowlayout.TagFlowLayout; +// +//import java.io.UnsupportedEncodingException; +//import java.net.URLDecoder; +//import java.util.ArrayList; +// +//import butterknife.BindView; +//import butterknife.OnClick; +// +///** +// * desc:登录注册界面 +// */ +// +//public class RfidMainActivity extends BaseActivity +// implements LoginRegistView, ViewTreeObserver.OnGlobalLayoutListener, OnChartValueSelectedListener { +// private static final String TAG = "LoginActivity"; +// +// private final String COM_INFO = "com_info"; +// private final String REST_NUM = "restId"; +// +// private final int OFFSET_WEIGHT = 2; +// +// private String mUid = null; +// +// @BindView(R.id.food_img) +// ImageView foodImg; +// @BindView(R.id.main_user_info_layout) +// LinearLayout userInfoLayout; +// @BindView(R.id.main_user_name_tv) +// TextView userNameTv; +// @BindView(R.id.main_user_company_tv) +// TextView userCompanyTv; +// @BindView(R.id.main_foodname_tv) +// TextView foodNameTv; +// @BindView(R.id.main_foodname_take_tv) +// TextView foodTakeTv; +// @BindView(R.id.main_foodname_take_kcal_tv) +// TextView foodTakeKcalTv; +// @BindView(R.id.main_form_calorie_num_tv) +// TextView calorieNumTv; +// @BindView(R.id.main_dinner_min_tv) +// TextView dinnerMinTv; +// @BindView(R.id.main_dinner_max_tv) +// TextView dinnerMaxTv; +// @BindView(R.id.seekBar) +// SeekBar mSeekBar; +// @BindView(R.id.title_content_fat) +// TextView titleContentFat; +// @BindView(R.id.title_content_protein) +// TextView titleContentProtein; +// @BindView(R.id.title_content_cho) +// TextView titleContentCho; +// @BindView(R.id.title_flowlayout) +// TagFlowLayout titleFlowLayout; +// @BindView(R.id.middle_fat) +// TextView middleFat; +// @BindView(R.id.middle_protein) +// TextView middleProtein; +// @BindView(R.id.middle_cho) +// TextView middleCho; +// @BindView(R.id.bottom_weight_tv) +// TextView bottomWeight; +// @BindView(R.id.bottom_fat_tv) +// TextView bottomFat; +// @BindView(R.id.bottom_protein_tv) +// TextView bottomProtein; +// @BindView(R.id.bottom_cho_tv) +// TextView bottomCho; +// @BindView(R.id.bottom_kcal_tv) +// TextView bottomKcal; +// @BindView(R.id.rfid) +// TextView rfidTv; +// +// +// private int firstWeight = 0; +// private int currentWeight = 0; +// private int intake = 0; +// +// private String restNum; +// +// @Override +// protected LoginRegistPresenter createPresenter() { +// return new LoginRegistPresenter(); +// } +// +// @Override +// protected int provideContentViewId() { +// return R.layout.activity_rfid_main; +// } +// +// @Override +// protected void afterRequestPermission(int requestCode, boolean isAllGranted) { +// +// } +// +// @Override +// public void init() { +// super.init(); +// +// //本地人脸库初始化 +// FaceServer.getInstance().init(this); +// +// String strAdd = PrefUtils.getString(this, COM_INFO, "/dev/ttyS4"); +//// SerialPortManager.instance().open(strAdd, "9600", responseListener); +// +// initTimer(); +// +// //未设置餐厅或IP,自动跳转设置界面 +// restNum = PrefUtils.getString(RfidMainActivity.this, REST_NUM, null); +// String comInfo = PrefUtils.getString(RfidMainActivity.this, COM_INFO, null); +// //AppUtil.isEmpty(deviceIp) || +//// if (AppUtil.isEmpty(comInfo) || AppUtil.isEmpty(restNum)) { +//// startActivity(new Intent(LoginActivity.this, DeviceActivity.class)); +//// return; +//// } +// +// } +// +// @Override +// public void initView() { +// +// ImmersionBar.with(this) +// .hideBar(BarHide.FLAG_HIDE_BAR) +// .statusBarAlpha(0f) +// .statusBarDarkFont(true) +// .statusBarColor(R.color.white) +// .init(); +// +//// Typeface typeface = Typeface.createFromAsset(getAssets(), "fonts/FZYTK.TTF"); +//// bottomKcal.setTypeface(typeface); +// +// } +// +// private void initFoodInfo() { +// currentFoodInfo = (FoodInfoModel) getIntent().getSerializableExtra("foodInfo"); +// +// foodNameTv.setText(currentFoodInfo.getFoodName()); +// foodNameTv.setTag(currentFoodInfo.getId()); +// +// try { +// RequestOptions cropOptions = new RequestOptions(); +// cropOptions.transform(new RoundedCorners(12));//new CenterCrop() +// +// Glide.with(this).load(URLDecoder.decode(currentFoodInfo.getImgUrl(), "UTF-8")) +// .apply(cropOptions) +// .into(foodImg); +// } catch (UnsupportedEncodingException e) { +// e.printStackTrace(); +// } +// +// ArrayList tagArr = new ArrayList<>(); +// String foodLabel = currentFoodInfo.getFoodLabel(); +// if (!AppUtil.isEmpty(foodLabel)) { +// String[] feedNameArr = foodLabel.split(","); +// for (String feedName : feedNameArr) { +// tagArr.add(feedName); +// } +// } +// if (tagArr.size() > 0) { +// MyFoodTagAdapters myFlowAdapters = new MyFoodTagAdapters(this, tagArr.size() > 4 ? tagArr.subList(0, 4) : tagArr); +// titleFlowLayout.setAdapter(myFlowAdapters); +// myFlowAdapters.notifyDataChanged(); +// titleFlowLayout.setVisibility(View.VISIBLE); +// } else { +// titleFlowLayout.setVisibility(View.GONE); +// } +// +// //初始化营养信息 +// float kcal = AppUtil.formatFloat2((currentFoodInfo.getStFoodInfoMaterial().getEnergyKcal())); +// float fat = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getFat()); +// float protein = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getProtein()); +// float cho = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getCho()); +// float na = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getNa() / 1000); +// +// calorieNumTv.setText(kcal + ""); +// titleContentFat.setText("脂肪/" + fat + "g"); +// titleContentCho.setText("碳水/" + cho + "g"); +// titleContentProtein.setText("蛋白质/" + protein + "g"); +// } +// +// //设置数据 +// private void setData(int weight, boolean isSetUserInfo) { +// if (currentFoodInfo == null) { +// return; +// } +// float kcal = AppUtil.formatFloat2((currentFoodInfo.getStFoodInfoMaterial().getEnergyKcal() * weight / 100)); +// +// float fat = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getFat() * weight / 100); +// float protein = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getProtein() * weight / 100); +// float cho = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getCho() * weight / 100); +// float na = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getNa() / 1000 * weight / 100); +// if (isSetUserInfo) { +//// foodTakeTv.setText("取用量" + weight + "g(" +//// + kcal +//// + "Kcal)"); +// foodTakeTv.setText(weight + ""); +// foodTakeKcalTv.setText(kcal + ""); +// setEnergyKcal(energyKcal + kcal); +//// formWeightTv.setText("(" + weight + "g)"); +// middleFat.setText("脂肪/" + fat + "g"); +// middleProtein.setText("蛋白质/" + protein + "g"); +// middleCho.setText("碳水/" + cho + "g"); +// +// setBottomData(weight, fat, protein, cho); +// } else { +//// foodTakeTv.setText("取用量" + 0 + "g(" +//// + 0 +//// + "Kcal)"); +// foodTakeTv.setText("0"); +// foodTakeKcalTv.setText("0"); +// setEnergyKcal(energyKcal); +// middleFat.setText("脂肪/--"); +// middleProtein.setText("蛋白质/--"); +// middleCho.setText("碳水/--"); +// } +// } +// +// @Override +// protected void onResume() { +// super.onResume(); +// +// } +// +// private void setBottomData(int weight, float fat, float protein, float cho) { +// if (userNutritionInfo != null) { +// bottomFat.setText(AppUtil.formatFloat2(userNutritionInfo.getFat() + fat) + "g"); +// bottomProtein.setText(AppUtil.formatFloat2(userNutritionInfo.getProtein() + protein) + "g"); +// bottomCho.setText(AppUtil.formatFloat2(userNutritionInfo.getCho() + cho) + "g"); +// } else { +// bottomFat.setText(AppUtil.formatFloat2(fat) + "g"); +// bottomProtein.setText(AppUtil.formatFloat2(protein) + "g"); +// bottomCho.setText(AppUtil.formatFloat2(cho) + "g"); +// } +// bottomWeight.setText(AppUtil.formatFloat2(totalWeight + weight) + "g"); +// } +// +// private Handler handler; +// +// @Override +// public void initData() { +// handler = new Handler(Looper.getMainLooper()) { +// @Override +// public void handleMessage(Message msg) { +// switch (msg.what) { +// case 1://返回socket数据 +// L.e("onLineRenewal-light===" + intake); +// openLedLight(YNHAPI.Light.Light_Red); +// +//// rfidTv.setText("RFID:" + rfId); +// //====================================== +//// if (firstWeight == 0) {//保存首次重量 +//// firstWeight = (int) deviceWeight; +//// } +//// currentWeight = (int) deviceWeight; +//// if (firstWeight - currentWeight > 0) { +//// intake = firstWeight - currentWeight; +//// setData(intake, true); +//// if (AppUtil.isEmpty(mUid) && intake > OFFSET_WEIGHT) { +//// L.e("onLineRenewal-light===" + intake); +////// openLedLight(YNHAPI.Light.Light_Red); +//// } +//// } else if (firstWeight - currentWeight == 0) { +//// intake = firstWeight - currentWeight; +//// setData(100, false); +//// setBottomData(0, 0, 0, 0); +//// } +// break; +// case 2://获取本餐信息 +//// mPresenter.getMealRecordsInfo(mUid); +// +// mPresenter.getUserNutrition(restNum, mUid); +// break; +// } +// } +// }; +// initFoodInfo(); +// } +// +// private void openLedLight(YNHAPI.Light index) { +// if (YNHAPI.getLightState(index)) { +// return; +// } +// YNHAPI.setLightState(index, true); +// new Handler().postDelayed(new Runnable() { +// public void run() { +// YNHAPI.setLightState(index, false); +// } +// }, 5000); //5秒 +// } +// +// private final long CLICK_INTERVAL_TIME = 300; +// private long lastClickTime = 0; +// +// @Override +// public void initListener() { +// userInfoLayout.setOnClickListener(new View.OnClickListener() { +// @Override +// public void onClick(View v) { +// long currentTimeMillis = SystemClock.uptimeMillis(); +// if (currentTimeMillis - lastClickTime < CLICK_INTERVAL_TIME) { +// Intent intent = new Intent(RfidMainActivity.this, DeviceActivity.class); +// startActivity(intent); +// return; +// } +// lastClickTime = currentTimeMillis; +// } +// }); +// foodNameTv.setOnClickListener(new View.OnClickListener() { +// @Override +// public void onClick(View v) { +// long currentTimeMillis = SystemClock.uptimeMillis(); +// if (currentTimeMillis - lastClickTime < CLICK_INTERVAL_TIME) { +// Intent intent = new Intent(RfidMainActivity.this, FoodSettingActivity.class); +// intent.putExtra("current_food", foodNameTv.getText().toString()); +// startActivity(intent); +// finish(); +// return; +// } +// lastClickTime = currentTimeMillis; +// } +// }); +// rfidTv.setOnClickListener(new View.OnClickListener() { +// @Override +// public void onClick(View v) { +// finish(); +// } +// }); +// } +// +// @Override +// public void onGlobalLayout() { +// +// } +// +// @OnClick({R.id.main_foodname_take_tv}) +// public void onViewClicked(View view) { +// switch (view.getId()) { +// case R.id.main_foodname_take_tv: +// break; +// } +// } +// +// +// @Override +// protected void onPause() { +// super.onPause(); +// +// } +// +// @Override +// protected void onDestroy() { +//// SerialPortManager.instance().close(); +// super.onDestroy(); +// } +// +// @Override +// public void showProgress(String tipString) { +//// showWaitingDialog(tipString); +// } +// +// @Override +// public void hideProgress() { +//// hideWaitingDialog(); +// } +// +// @Override +// public void upFaceFeatureSuccess() { +// finish(); +// startActivity(new Intent(this, SelectIdentityActivity.class)); +// } +// +// @Override +// public void upFaceFeatureFail(String info) { +// T.showShort(this, info); +// finish(); +// startActivity(new Intent(this, SelectIdentityActivity.class)); +// } +// +// @Override +// public void getFaceFeatureSuccess(ArrayList list) { +// if (list != null && list.size() > 0) { +// FaceServer.getInstance().clearAllFaces(this); +// +// for (int i = 0; i < list.size(); i++) { +// UserFaceModel faceModel = list.get(i); +// FaceServer.getInstance().saveFaceFeature(faceModel.getUserId() + ":" + faceModel.getUserFaceId(), faceModel.getFaceFeature()); +// } +// } else { +// showToast("人脸数据获取失败"); +// } +// } +// +// @Override +// public void getFaceFeatureFail(String info) { +// +// } +// +// +// @Override +// public void getUserInfoSuccess(UserInfo userModel) { +// if (userModel == null) { +// showToast("获取用户信息失败"); +// return; +// } +// firstWeight = 0; +// mUid = userModel.getUserId(); +// +// userInfoLayout.setVisibility(View.VISIBLE); +// userNameTv.setText(userModel.getRealname()); +// userCompanyTv.setText(userModel.getSecondDepartName()); +//// mPresenter.bindDeviceMac(mUid, DeviceIdUtil.getDeviceId(this)); +// +// Message message = new Message(); +// message.what = 2; +// handler.sendMessageDelayed(message, 700); +// +// if (mCountDownTimer != null) { +// mCountDownTimer.cancel(); +// mCountDownTimer.start(); +// } +// } +// +// @Override +// public void getUserInfoFail(String info) { +//// Intent i = new Intent(this, RfidBindActivity.class); +//// i.putExtra("rfid", rfId); +//// startActivityForResult(i, 1001); +// } +// +// @Override +// public void getUserNutritionInfoSuccess(UserNutritionInfo userModel) { +// userNutritionInfo = userModel; +// +// energyKcal = userNutritionInfo.getEnergyKcal(); +// totalWeight = userNutritionInfo.getFoodWeight(); +// +// setEnergyKcal(energyKcal); +// dinnerMinTv.setText(userNutritionInfo.getRecommendMin() + "kcal"); +// dinnerMaxTv.setText(userNutritionInfo.getRecommendMax() + "kcal"); +// setBottomData(0, 0, 0, 0); +// } +// +// @Override +// public void getUserNutritionInfoFail(String info) { +// +// } +// +// +// @Override +// protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { +// super.onActivityResult(requestCode, resultCode, data); +// if (requestCode == 1001 && data != null) { +// mUid = data.getStringExtra("userId"); +// mPresenter.getUserInfoById(mUid); +// } +// +// } +// +// @Override +// public void getDeviceMacSuccess(DeviceMacModel deviceMacModel) { +// if (!deviceMacModel.getDeviceMac().equals(DeviceIdUtil.getDeviceId(this))) {//绑定设备已切换 +// createRecord(); +// } +// } +// +// private double anonymousLastWeight = 0; +// private long lastTimeMillis = 0; +// private final long ANONYMOUS_SUBMIT_INTERVAL = 1000 * 60; +// +// private void createAnonymousRecord() { +// long currentTime = System.currentTimeMillis(); +// if (lastTimeMillis == 0) { +// lastTimeMillis = currentTime; +// } +// if (anonymousLastWeight == 0) { +// anonymousLastWeight = deviceWeight; +// } +// if (currentTime - lastTimeMillis > ANONYMOUS_SUBMIT_INTERVAL) { +// lastTimeMillis = currentTime; +// double currentIntake = anonymousLastWeight - deviceWeight; +// Log.e("mzf", "createAnonymousRecord===" + currentIntake + ""); +// if (AppUtil.isEmpty(mUid) +// && currentIntake > OFFSET_WEIGHT) { +// String anonymousId = "anonymous_" + restNum + "_user_id_000"; +// mPresenter.createRecord(anonymousId, +// foodNameTv.getTag().toString(), restNum, (int) currentIntake, deviceWeight); +// +// runOnUiThread(new Runnable() { +// @Override +// public void run() { +// resetData(); +// } +// }); +// } +// anonymousLastWeight = deviceWeight; +// } +// +// } +// +// private void createRecord() { +// if (!AppUtil.isEmpty(mUid) +// && intake > OFFSET_WEIGHT +// && !AppUtil.isEmpty(foodNameTv.getTag().toString())) { +// mPresenter.createRecord(mUid, foodNameTv.getTag().toString(), restNum, intake, deviceWeight); +// anonymousLastWeight = deviceWeight; +// } +// if (!AppUtil.isEmpty(mUid)) { +// runOnUiThread(new Runnable() { +// @Override +// public void run() { +// resetData(); +// } +// }); +// } +// } +// +// private void resetData() { +// //--------------初始化状态------------------------ +// mUid = null; +// rfId = null; +// userInfoLayout.setVisibility(View.INVISIBLE); +// userNameTv.setText(null); +// userCompanyTv.setText(null); +// +// firstWeight = 0; +// currentWeight = 0; +// intake = 0; +// energyKcal = 0; +// userNutritionInfo = null; +// totalWeight = 0; +// setBottomData(0, 0, 0, 0); +// setEnergyKcal(0); +// setData(100, false); +// +// if (mCountDownTimer != null) { +// mCountDownTimer.cancel(); +// } +// } +// +// @Override +// public void getDeviceMacFail(String info) { +// +// } +// +// private FoodInfoModel currentFoodInfo; +// +// @Override +// public void getFoodInfoByMacSuccess(FoodInfoModel foodInfoModel) { +// foodNameTv.setText(foodInfoModel.getFoodName()); +// foodNameTv.setTag(foodInfoModel.getId()); +// +// try { +// RequestOptions cropOptions = new RequestOptions(); +// cropOptions.transform(new RoundedCorners(12));//new CenterCrop() +// +// Glide.with(this).load(URLDecoder.decode(foodInfoModel.getImgUrl(), "UTF-8")) +// .apply(cropOptions) +// .into(foodImg); +// } catch (UnsupportedEncodingException e) { +// e.printStackTrace(); +// } +// +// ArrayList tagArr = new ArrayList<>(); +// String foodLabel = foodInfoModel.getFoodLabel(); +// if (!AppUtil.isEmpty(foodLabel)) { +// String[] feedNameArr = foodLabel.split(","); +// for (String feedName : feedNameArr) { +// tagArr.add(feedName); +// } +// } +// if (tagArr.size() > 0) { +// MyFoodTagAdapters myFlowAdapters = new MyFoodTagAdapters(this, tagArr.size() > 4 ? tagArr.subList(0, 4) : tagArr); +// titleFlowLayout.setAdapter(myFlowAdapters); +// myFlowAdapters.notifyDataChanged(); +// titleFlowLayout.setVisibility(View.VISIBLE); +// } else { +// titleFlowLayout.setVisibility(View.GONE); +// } +// +// currentFoodInfo = foodInfoModel; +//// setData(100, false); +// +// //初始化营养信息 +// float kcal = AppUtil.formatFloat2((currentFoodInfo.getStFoodInfoMaterial().getEnergyKcal())); +// float fat = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getFat()); +// float protein = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getProtein()); +// float cho = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getCho()); +// float na = AppUtil.formatFloat2(currentFoodInfo.getStFoodInfoMaterial().getNa() / 1000); +// +// calorieNumTv.setText(kcal + ""); +// titleContentFat.setText("脂肪/" + fat + "g"); +// titleContentCho.setText("碳水/" + cho + "g"); +// titleContentProtein.setText("蛋白质/" + protein + "g"); +// } +// +// @Override +// public void getFoodInfoByMacFail(String info) { +// +// } +// +// private ArrayList dinnerList; +// private float energyKcal = 0; +// private UserNutritionInfo userNutritionInfo; +// private float totalWeight = 0; +// +// @Override +// public void getMealRecordsInfoSuccess(MealRecordsInfo mealRecordsInfo) { +//// if (mealRecordsInfo != null && mealRecordsInfo.getNeedEnergyVo() != null) { +//// MealRecordsInfo.NeedEnergyVo needEnergyVo = mealRecordsInfo.getNeedEnergyVo(); +//// dinnerList = new ArrayList<>(); +//// dinnerList.add(new DinnerModel(needEnergyVo.getDinner1Min(), needEnergyVo.getDinner1Max())); +//// dinnerList.add(new DinnerModel(needEnergyVo.getDinner2Min(), needEnergyVo.getDinner2Max())); +//// dinnerList.add(new DinnerModel(needEnergyVo.getDinner3Min(), needEnergyVo.getDinner3Max())); +//// if (mealRecordsInfo.getFoodExtInfoVo() != null) { +//// foodExtInfoVo = mealRecordsInfo.getFoodExtInfoVo(); +//// energyKcal = foodExtInfoVo.getEnergyKcal(); +//// } +//// totalWeight = mealRecordsInfo.getTotalWeight(); +//// mPresenter.getDinnerType(); +//// } +// } +// +// @Override +// public void getMealRecordsInfoFail(String info) { +// +// } +// +//// private DinnerModel currentDinner; +// +// @Override +// public void getDinnerTypeSuccess(DinnerType dinnerType) { +// DinnerModel dinnerModel = dinnerList.get(dinnerType.getDinnerType() - 1); +// +// if (dinnerModel == null) { +// return; +// } +// +//// currentDinner = dinnerModel; +//// setEnergyKcal(energyKcal); +//// dinnerMinTv.setText(currentDinner.getDinnerMin() + "kcal"); +//// dinnerMaxTv.setText(currentDinner.getDinnerMax() + "kcal"); +//// setBottomData(0, 0, 0, 0); +// } +// +// private void setEnergyKcal(float kcal) { +// kcal = AppUtil.formatFloat2(kcal); +// if (userNutritionInfo == null) { +// mSeekBar.setProgress(0); +// bottomKcal.setText(kcal + ""); +// return; +// } +// +// mSeekBar.setProgress((int) (kcal / userNutritionInfo.getRecommendMax() * 100)); +// bottomKcal.setText(kcal + ""); +//// mSeekBar.setTextInfo(kcal + "kcal"); +// +//// if (kcal > currentDinner.getDinnerMax()) { +//// kcalYellowTv.setVisibility(View.INVISIBLE); +//// kcalBlueTv.setVisibility(View.INVISIBLE); +//// kcalRedTv.setVisibility(View.VISIBLE); +//// kcalRedTv.setText("能量:" + kcal + "kcal"); +//// } else if (kcal > currentDinner.getDinnerMin()) { +//// kcalYellowTv.setVisibility(View.INVISIBLE); +//// kcalBlueTv.setVisibility(View.VISIBLE); +//// kcalRedTv.setVisibility(View.INVISIBLE); +//// kcalBlueTv.setText("能量:" + kcal + "kcal"); +//// } else { +//// kcalYellowTv.setVisibility(View.VISIBLE); +//// kcalBlueTv.setVisibility(View.INVISIBLE); +//// kcalRedTv.setVisibility(View.INVISIBLE); +//// kcalYellowTv.setText("能量:" + kcal + "kcal"); +//// } +// } +// +// @Override +// public void getDinnerTypeFail(String info) { +// +// } +// +// @Override +// public void createRecordSuccess(String info) { +// +// } +// +// @Override +// public void createRecordFail(String info) { +// +// } +// +// @Override +// public void getTokenSuccess(String info) { +// +// } +// +// @Override +// public void getTokenFail(String info) { +// +// } +// +// @Override +// public void addFoodWeightSuccess(String info) { +// +// } +// +// @Override +// public void addFoodWeightFail(String info) { +// +// } +// +// +// private final long COUNT_TIME = 60000 * 5; //5分钟倒计时提交数据 +// private CountDownTimer mCountDownTimer; +// +// private void initTimer() { +// mCountDownTimer = new CountDownTimer(COUNT_TIME, 1000) { +// @Override +// public void onTick(long millisUntilFinished) { +// L.e("mCountDownTimer", millisUntilFinished / 1000 + "s"); +// } +// +// @Override +// public void onFinish() { +// L.e("mCountDownTimer", "onFinish"); +// createRecord(); +// } +// }; +// } +// +// +// private double deviceWeight = 0; +// private String rfId = ""; +// private final String RFID_DEFAULT = "00000000"; +// +//// SerialReadThread.ResponseListener responseListener = new SerialReadThread.ResponseListener() { +//// +//// @Override +//// public void onGetRfid(String id) { +//// EventBus.getDefault().post(id); +//// if (id.equals(RFID_DEFAULT)) { +//// if (!AppUtil.isEmpty(rfId) && +//// !rfId.equals(RFID_DEFAULT)) { +//// if (intake > 0) { +//// createRecord(); +//// } else { +//// runOnUiThread(new Runnable() { +//// @Override +//// public void run() { +//// resetData(); +//// } +//// }); +//// } +//// } +//// return; +//// } +//// if (rfId != null && rfId.equals(id)) { +//// return; +//// } +//// rfId = id; +//// mPresenter.getUserInfo(rfId); +//// } +//// +//// @Override +//// public void onGetWeightInfo(int weight) { +//// deviceWeight = weight; +//// if (firstWeight == 0) {//保存首次重量 +//// firstWeight = weight; +//// } +//// currentWeight = weight; +//// +//// if (firstWeight - currentWeight >= 0) { +//// intake = firstWeight - currentWeight; +//// runOnUiThread(new Runnable() { +//// @Override +//// public void run() { +//// setData(intake, true); +//// } +//// }); +//// } else if (currentWeight - firstWeight > 100) {//大于100 认为是加菜 +//// firstWeight = weight; +//// mPresenter.addFoodTotalWeight(currentFoodInfo.getId(), +//// 2, deviceWeight, restNum); +//// } +////// if (AppUtil.isEmpty(mUid) && intake > OFFSET_WEIGHT) { +////// Message message = new Message(); +////// message.what = 1; +////// handler.sendMessage(message); +////// } +//// } +//// }; +// +// @Override +// public void onValueSelected(Entry e, Highlight h) { +// +// } +// +// @Override +// public void onNothingSelected() { +// +// } +//} diff --git a/app/src/main/java/com/sw/st/ui/setting/FoodListModel.java b/app/src/main/java/com/sw/st/ui/setting/FoodListModel.java new file mode 100644 index 0000000..27d33bc --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/setting/FoodListModel.java @@ -0,0 +1,31 @@ +package com.sw.st.ui.setting; + +public class FoodListModel { + private String id; + private String foodName; + private String foodImg; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFoodName() { + return foodName; + } + + public void setFoodName(String foodName) { + this.foodName = foodName; + } + + public String getFoodImg() { + return foodImg; + } + + public void setFoodImg(String foodImg) { + this.foodImg = foodImg; + } +} diff --git a/app/src/main/java/com/sw/st/ui/setting/FoodNameAdapter.java b/app/src/main/java/com/sw/st/ui/setting/FoodNameAdapter.java new file mode 100644 index 0000000..223febe --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/setting/FoodNameAdapter.java @@ -0,0 +1,40 @@ +package com.sw.st.ui.setting; + +import android.content.Context; +import android.widget.TextView; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.viewholder.BaseViewHolder; +import com.sw.st.R; +import com.sw.st.model.NewFoodInfoModel; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class FoodNameAdapter extends BaseQuickAdapter { + + Context context; + + public FoodNameAdapter(@Nullable List strs, Context context) { + super(R.layout.item_food_name_tv, strs); + this.context = context; + } + + @Override + protected void convert(@NotNull BaseViewHolder baseViewHolder, NewFoodInfoModel info) { + + TextView textView = baseViewHolder.findView(R.id.tv); +// baseViewHolder.setText(R.id.tv, info.getFoodName()); + textView.setText(info.getFoodName()); + textView.setSelected(info.isSelect()); + if(info.isSelect()){ + + }else{ + + } + } + + +} diff --git a/app/src/main/java/com/sw/st/ui/setting/FoodSettingActivity.java b/app/src/main/java/com/sw/st/ui/setting/FoodSettingActivity.java new file mode 100644 index 0000000..74320b5 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/setting/FoodSettingActivity.java @@ -0,0 +1,544 @@ +package com.sw.st.ui.setting; + +import android.Manifest; +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Build; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.listener.OnItemClickListener; +import com.google.gson.reflect.TypeToken; +import com.gyf.immersionbar.BarHide; +import com.gyf.immersionbar.ImmersionBar; +import com.lzy.okgo.OkGo; +import com.lzy.okgo.model.HttpHeaders; +import com.sw.st.R; +import com.sw.st.base.BaseActivity; +import com.sw.st.model.FoodGoodsInfo; +import com.sw.st.model.NewFoodInfoModel; +import com.sw.st.model.UserFaceModel; +import com.sw.st.net.helper.Convert; +import com.sw.st.utils.AppUtil; +import com.sw.st.utils.InputMethod; +import com.sw.st.utils.L; +import com.sw.st.utils.PrefUtils; +import com.wabon.wbintelligenthardwaresdk.api.WeigherTwo; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.OnClick; +import pub.devrel.easypermissions.EasyPermissions; + +public class FoodSettingActivity extends BaseActivity + implements FoodSettingView { + + private static final int REQUEST_CODE_QRCODE_PERMISSIONS = 1; + public static final int LEFT_WEIGHT_INDEX = 1; + public static final int RIGHT_WEIGHT_INDEX = 2; + private final String[] Permissions = {Manifest.permission.READ_PHONE_STATE, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.VIBRATE}; + public static final String REST_NUM = "restId"; + public static final String REST_NAME = "restName"; + public static final String CURRENT_FOOD_DATA = "current_food_data"; + @BindView(R.id.setting_search_edit) + EditText searchEdit; + @BindView(R.id.setting_search_edit_cancel_img) + ImageView cancelImg; + // @BindView(R.id.item_shopping_flowlayout) +// TagFlowLayout flowLayout; + @BindView(R.id.setting_current_food_tv) + TextView currentFoodTv; + @BindView(R.id.currentWeight) + TextView currentWeight; + @BindView(R.id.start) + Button startButton; + @BindView(R.id.foodRecyclerView) + RecyclerView foodRecyclerView; + + // private MyFlowAdapters myFlowAdapters; + private FoodNameAdapter foodNameAdapter; + private ArrayList foodTagList = new ArrayList<>(); + + private NewFoodInfoModel currentFoodInfo; + private String restNo = "99"; + + private ArrayList allFoodList = new ArrayList<>(); + private ArrayList searchResultFoodList = new ArrayList<>(); + + private int deviceWeight = 0; + private boolean isInitScale = false; + + private double leftDeviceWeight = 0; + private double rightDeviceWeight = 0; + private int currentDeviceIndex = LEFT_WEIGHT_INDEX; + + @Override + protected int provideContentViewId() { + return R.layout.activity_setting; + } + + @Override + public void init() { + super.init(); + + if (!EasyPermissions.hasPermissions(this, Permissions)) { + EasyPermissions.requestPermissions(this, "扫描二维码需要打开相机和文件权限", REQUEST_CODE_QRCODE_PERMISSIONS, Permissions); + } + + WeigherTwo.init("/dev/ttyS7"); + } + + + @Override + public void initView() { + ImmersionBar.with(this) + .hideBar(BarHide.FLAG_HIDE_BAR) + .statusBarAlpha(0f) + .statusBarDarkFont(true) + .statusBarColor(R.color.white) + .init(); + } + + @Override + public void initData() { + initScale(); + mPresenter.getToken(AppUtil.getUDID(mContext)); + + foodNameAdapter = new FoodNameAdapter(null, mContext); + foodRecyclerView.setAdapter(foodNameAdapter); + } + + @Override + public void initListener() { + searchEdit.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (AppUtil.isEmpty(s.toString())) { + cancelImg.setVisibility(View.GONE); + } else { + cancelImg.setVisibility(View.VISIBLE); + } + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + searchEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + // 当按了搜索之后关闭软键盘 + InputMethod.closeInputMethod(FoodSettingActivity.this, v); + + searchResultFoodList.clear(); + + mPresenter.getRestInfoFoodsByName(restNo, searchEdit.getText().toString()); + return true; + } + return false; + } + }); + + foodNameAdapter.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(@NonNull BaseQuickAdapter adapter, @NonNull View view, int position) { + // 当按了搜索之后关闭软键盘 + InputMethod.closeInputMethod(FoodSettingActivity.this, startButton); + + startButton.setVisibility(View.VISIBLE); + + currentFoodInfo = (NewFoodInfoModel) adapter.getItem(position); + currentFoodTv.setText(currentFoodInfo.getFoodName()); + + List foodInfoModelList = foodNameAdapter.getData(); + for (NewFoodInfoModel foodInfoModel : foodInfoModelList) { + foodInfoModel.setSelect(false); + } + foodNameAdapter.getItem(position).setSelect(true); + foodNameAdapter.notifyDataSetChanged(); + + mPresenter.getGoodsUseList(currentFoodInfo.getId(), 1000); + } + }); + + } + + + @OnClick({R.id.setting_search_edit_cancel_img, R.id.back, + R.id.start, R.id.currentWeight, R.id.zeroTv, R.id.zeroTitleTv}) + public void onViewClicked(View view) { + switch (view.getId()) { + case R.id.title_name_tv: + break; + case R.id.title_back_iv: + finish(); + break; + case R.id.setting_search_edit_cancel_img: + searchEdit.setText(null); + InputMethod.closeInputMethod(this, view); + + + if (foodTagList == null || foodTagList.size() == 0) { +// myFlowAdapters = new MyFlowAdapters(this, allFoodList); + + foodNameAdapter.setList(allFoodList); + } else { + foodNameAdapter.setList(foodTagList); + } + + break; + case R.id.back: + finish(); + break; + case R.id.start: + currentDeviceIndex=RIGHT_WEIGHT_INDEX; + break; + case R.id.currentWeight: + currentDeviceIndex=RIGHT_WEIGHT_INDEX; + break; + case R.id.zeroTitleTv: + showProgress("请稍候..."); + weightZero(currentDeviceIndex); + break; + case R.id.zeroTv: + weightTare(currentDeviceIndex); + break; + } + } + + @Override + protected FoodSettingPresenter createPresenter() { + return new FoodSettingPresenter(); + } + + @Override + protected void afterRequestPermission(int requestCode, boolean isAllGranted) { + + } + + @Override + public void getFoodListSuccess(ArrayList list, String type) { + switch (type) { + case "0": + allFoodList = list; + L.e("FoodListSize===" + allFoodList.size()); + if (foodTagList == null || foodTagList.size() == 0) { + foodNameAdapter.setList(allFoodList); + } + break; + case "1": + foodTagList = list; + foodNameAdapter.setList(foodTagList); + break; + } + + } + + @Override + public void getFoodListFail(String info) { + + } + + @Override + public void getTokenSuccess(String info) { + String token = info; + if (!TextUtils.isEmpty(token)) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.headersMap.put("Authorization", token); + OkGo.getInstance().addCommonHeaders(httpHeaders); + } + mPresenter.getRestInfoFoodsByType(restNo, "1"); +// mPresenter.getRestInfoFoodsByType(restNo, "0"); + + } + + @Override + public void getTokenFail(String info) { + + } + + @Override + public void addFoodWeightSuccess(String info) { + String currentFoodInfoStr = Convert.toJson(currentFoodInfo); + L.e(currentFoodInfoStr); + PrefUtils.setString(FoodSettingActivity.this, CURRENT_FOOD_DATA, currentFoodInfoStr); + + finish(); + } + + @Override + public void addFoodWeightFail(String info) { + hideProgress(); + showToast(info); + } + + @Override + public void getRestInfoSuccess(String info) { + Log.e("mzf", info); + try { + JSONObject jsonInfo = new JSONObject(info); + String restName = jsonInfo.optString("restName"); +// String restNo = jsonInfo.optString("restNo"); + + String restId = jsonInfo.getString("id"); + + PrefUtils.setString(this, REST_NAME, restName); + PrefUtils.setString(this, REST_NUM, restId); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public void getRestInfoFail(String info) { + showToast(info); + Dialog alertDialog = new AlertDialog.Builder(this) + .setTitle("失败") + .setMessage(info) + .create(); + alertDialog.show(); + } + + @Override + public void getFaceFeatureSuccess(ArrayList list) { + } + + @Override + public void getFaceFeatureFail(String info) { + + } + + @Override + public void searchFoodListSuccess(ArrayList list) { + searchResultFoodList = list; + + foodNameAdapter.setList(searchResultFoodList); + } + + @Override + public void searchFoodListFail(String info) { + + } + + ArrayList foodGoodsInfoArrayList; + + @Override + public void getGoodsUseListSuccess(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + foodGoodsInfoArrayList = Convert.fromJson(jsonObject.optString("voList"), new TypeToken>() { + }.getType()); + + L.e("size===" + foodGoodsInfoArrayList.size()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + @Override + public void getGoodsUseListFail(String info) { + + } + + @Override + public void saveFoodGoodsUseInfoSuccess(String info) { + + } + + @Override + public void saveFoodGoodsUseInfoFail(String info) { + + } + + @Override + public void showProgress(String tipString) { + showWaitingDialog(tipString); + } + + @Override + public void hideProgress() { + hideWaitingDialog(); + } + + + @Override + protected void onDestroy() { + super.onDestroy(); + try { + WeigherTwo.unInit(); + } catch (Exception e) { + e.printStackTrace(); + } + + System.exit(0); + } + + public void weightZero(int index) { + WeigherTwo.zeroTwo(index); + } + + public void weightTare(int index) { + WeigherTwo.tareTwo(index); + } + + /** + * 初始化称体 + */ + private void initScale() { + WeigherTwo.setListener(new WeigherTwo.Listener() { + @Override + public void onInit(boolean connect) { + L.e("onZero onInit===" + connect); + if (connect) {//初始化成功 +// Weigher.getWeight(); + WeigherTwo.startContinuousRead(); + + WeigherTwo.zeroTwo(LEFT_WEIGHT_INDEX); + isInitScale = true; + } else {//失败再次尝试 + WeigherTwo.init("/dev/ttyS7"); + } + } + + @Override + public void onZero() { + if (isInitScale) {//初始化后置零需要一个一个操作,两个同时会失败 + isInitScale = false; + WeigherTwo.zeroTwo(RIGHT_WEIGHT_INDEX); + } + } + + + @Override + public void onGetWeight(int address, int state, int weight) { + String stateStr = ""; +// switch (state) { +// case SensorScale.STATE_STABLE: +// stateStr = "稳定"; +// break; +// case SensorScale.STATE_UNSTABLE: +// stateStr = "不稳定"; +// return; +// case SensorScale.STATE_OVER_WEIGHT: +// stateStr = "量程溢出"; +// return; +// } + + + if (address == LEFT_WEIGHT_INDEX) { + leftDeviceWeight = weight; + } + + if (address == RIGHT_WEIGHT_INDEX) { + rightDeviceWeight = weight; + } + + + //====================================================== + + runOnUiThread(new Runnable() { + @Override + public void run() { + if (currentDeviceIndex == LEFT_WEIGHT_INDEX) { + currentWeight.setText(formatWeight(leftDeviceWeight)); + } else if (currentDeviceIndex == RIGHT_WEIGHT_INDEX) { + currentWeight.setText(formatWeight(rightDeviceWeight)); + } + } + }); + } + + @Override + public void onTare() { + + } + + @Override + public void onSetIdentify() { + + } + + @Override + public void onReadIdentify(int rate) { + + } + + @Override + public void onFail(int errCode) { + String str = ""; + switch (errCode) { + case WeigherTwo.ERR_001: + str = "电子称未初始化"; + break; + case WeigherTwo.ERR_002: + str = "开机零位异常"; + break; + case WeigherTwo.ERR_003: + str = "传感器故障"; + break; + case WeigherTwo.ERR_004: + str = "鉴别率超出范围"; + break; + case WeigherTwo.ERR_PCB_NOT_SUPPORT: + str = "主板不支持, " + Build.MODEL; + break; + } + + } + + @Override + public void onFastFilter() { + + } + + + @Override + public void onReadParam() { + + } + + @Override + public void onRangCal() { + + } + }); + + } + + private String formatWeight(double weight) { +// weight = weight < 0 ? 0 : weight; + if (weight > 1000) { + return "余量 " + AppUtil.formatDouble(weight / 1000) + " 千克"; + } else { + return "余量 " + weight + " 克"; + } + } + +} diff --git a/app/src/main/java/com/sw/st/ui/setting/FoodSettingPresenter.java b/app/src/main/java/com/sw/st/ui/setting/FoodSettingPresenter.java new file mode 100644 index 0000000..1fb5268 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/setting/FoodSettingPresenter.java @@ -0,0 +1,290 @@ +package com.sw.st.ui.setting; + + +import com.sw.st.api.SwService; +import com.sw.st.base.BasePresenter; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.NewFoodInfoModel; +import com.sw.st.model.UserFaceModel; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; +import com.sw.st.ui.device.BindDeviceModel; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import io.reactivex.disposables.Disposable; + + +public class FoodSettingPresenter extends BasePresenter { + + + public void getRestInfoFoodsByType(String restNo, String type) { + SwService.getRestInfoFoodsByType(restNo, type) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver>() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(ArrayList info) { + if (getView() != null) + getView().getFoodListSuccess(info, type); + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getFoodListFail(errorMessage); + } + + @Override + public void _onComplete() { + if (getView() != null) + getView().hideProgress(); + } + }); + } + + public void getRestInfoFoodsByName(String restNo, String foodName) { + getView().showProgress("搜索中..."); + SwService.getRestInfoFoodsByName(restNo, foodName) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver>() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(ArrayList info) { + if (getView() != null) { + getView().searchFoodListSuccess(info); + getView().hideProgress(); + } + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) { + getView().searchFoodListFail(errorMessage); + getView().hideProgress(); + } + } + + @Override + public void _onComplete() { + } + }); + } + + public void getToken(String devId) { + SwService.getToken(devId) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().getTokenSuccess(data); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().getTokenFail("数据解析异常"); + } + + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getTokenFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + + /** + * @param foodId 菜品ID + * @param type 1开餐,2加菜 + * @param totalWeight 菜品增重 + * @return + */ + public void startMealService(String foodId, int type, double totalWeight, String deviceMac) { + SwService.startMealService(foodId, type, totalWeight, deviceMac) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().addFoodWeightSuccess(data); + } else { + getView().addFoodWeightFail(jsonObject.optString("message")); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().addFoodWeightFail("数据解析异常"); + } + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().addFoodWeightFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + + + public void getRestInfoByRestNo(String restNo) { + getView().showProgress("查询中..."); + SwService.getRestInfoByRestNo(restNo) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("result"); + if (getView() != null) { + getView().getRestInfoSuccess(data); + getView().hideProgress(); + } + } else { + String message = jsonObject.optString("message"); + if (getView() != null) { + getView().getRestInfoFail(message); + getView().hideProgress(); + } + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) { + getView().getRestInfoFail("数据解析异常"); + getView().hideProgress(); + } + } + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) { + getView().getRestInfoFail(errorMessage); + getView().hideProgress(); + } + } + + @Override + public void _onComplete() { + } + }); + } + + + public void getUserFace() { + SwService.getUserFace() + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleResult()) + .subscribe(new RxObserver>() { + + @Override + public void _onSubscribe(Disposable d) { + getView().showProgress("加载中..."); + } + + @Override + public void _onNext(ArrayList list) { + getView().getFaceFeatureSuccess(list); + } + + @Override + public void _onError(String errorMessage) { + getView().getFaceFeatureFail(errorMessage); + + getView().hideProgress(); + } + + @Override + public void _onComplete() { + getView().hideProgress(); + } + + }); + } + + public void getGoodsUseList(String foodId, double foodWeight) { + SwService.getGoodsUseList(foodId, foodWeight) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onSubscribe(Disposable d) { + } + + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + String data = jsonObject.optString("data"); + if (getView() != null) + getView().getGoodsUseListSuccess(data); + } + } catch (JSONException e) { + e.printStackTrace(); + if (getView() != null) + getView().getGoodsUseListFail("数据解析异常"); + } + + } + + @Override + public void _onError(String errorMessage) { + if (getView() != null) + getView().getGoodsUseListFail(errorMessage); + } + + @Override + public void _onComplete() { + } + }); + } + +} diff --git a/app/src/main/java/com/sw/st/ui/setting/FoodSettingView.java b/app/src/main/java/com/sw/st/ui/setting/FoodSettingView.java new file mode 100644 index 0000000..51724d0 --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/setting/FoodSettingView.java @@ -0,0 +1,47 @@ +package com.sw.st.ui.setting; + + +import com.sw.st.base.BaseView; +import com.sw.st.model.FoodInfoModel; +import com.sw.st.model.NewFoodInfoModel; +import com.sw.st.model.UserFaceModel; + +import java.util.ArrayList; + +public interface FoodSettingView extends BaseView { + + void getFoodListSuccess(ArrayList list, String type); + + void getFoodListFail(String info); + + void getTokenSuccess(String info); + + void getTokenFail(String info); + + void addFoodWeightSuccess(String info); + + void addFoodWeightFail(String info); + + void getRestInfoSuccess(String info); + + void getRestInfoFail(String info); + + void getFaceFeatureSuccess(ArrayList list); + + void getFaceFeatureFail(String info); + + void searchFoodListSuccess(ArrayList list); + + void searchFoodListFail(String info); + + + void getGoodsUseListSuccess(String info); + + void getGoodsUseListFail(String info); + + + void saveFoodGoodsUseInfoSuccess(String info); + + void saveFoodGoodsUseInfoFail(String info); + +} diff --git a/app/src/main/java/com/sw/st/ui/setting/MyFlowAdapters.java b/app/src/main/java/com/sw/st/ui/setting/MyFlowAdapters.java new file mode 100644 index 0000000..bf230aa --- /dev/null +++ b/app/src/main/java/com/sw/st/ui/setting/MyFlowAdapters.java @@ -0,0 +1,37 @@ +package com.sw.st.ui.setting; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import com.sw.st.R; +import com.sw.st.model.FoodInfoModel; +import com.zhy.view.flowlayout.FlowLayout; +import com.zhy.view.flowlayout.TagAdapter; + +import java.util.List; + +/** + * Created by Administrator on 2019/4/23 0023. + */ + +public class MyFlowAdapters extends TagAdapter { + + private Context context; + + public MyFlowAdapters(Context context, List datas) { + super(datas); + this.context = context; + } + + @Override + public View getView(FlowLayout parent, int position, FoodInfoModel foodListModel) { + LayoutInflater inflater = LayoutInflater.from(context); + TextView tv = (TextView) inflater.inflate(R.layout.item_flow_tv, parent, false); + tv.setText(foodListModel.getFoodName()); + return tv; + } + + +} diff --git a/app/src/main/java/com/sw/st/utils/AppUtil.java b/app/src/main/java/com/sw/st/utils/AppUtil.java new file mode 100644 index 0000000..464fe6b --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/AppUtil.java @@ -0,0 +1,661 @@ +package com.sw.st.utils; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.content.ActivityNotFoundException; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.view.View; +import android.widget.TextView; + +import androidx.core.content.FileProvider; + +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URLEncoder; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.Locale; +import java.util.UUID; + +import static android.content.Context.TELEPHONY_SERVICE; + +public class AppUtil { + + public static String getAppPackageName(Context context) { + String packageName = ""; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); + packageName = pi.packageName; + if (AppUtil.isEmpty(packageName)) { + return ""; + } + } catch (Exception e) { + e.printStackTrace(); + } + return packageName + ".single"; + } + + public static String getAppVersionName(Context context) { + String versionName = ""; + // int versioncode=1; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); + versionName = pi.versionName; + // versioncode = pi.versionCode;表示更新了多少次 + if (versionName == null || versionName.length() <= 0) { + return ""; + } + } catch (Exception e) { + e.printStackTrace(); + } + return versionName; + } + + public static int getAppVersionCode(Context context) { + int versioncode = 1; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); + versioncode = pi.versionCode; + + } catch (Exception e) { + e.printStackTrace(); + } + return versioncode; + } + + /** + * 打电话 + *

+ * Intent.ACTION_DIAL Intent.ACTION_CALL + * + * @param context + * @param mobile + */ + public static void callUp(Context context, String mobile) { + Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + + mobile)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + /** + * 获取设备ID + * + * @param context + * @return + */ + public static String getDevId(Context context) { + TelephonyManager TelephonyMgr = (TelephonyManager) context + .getSystemService(TELEPHONY_SERVICE); + return TelephonyMgr.getDeviceId(); + } + + + public static String formatDateGetFull(String date) { + if (isEmpty(date)) { + return ""; + } + Date d = new Date(Long.parseLong(date)); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + return dateFormat1.format(d); + } + + public static String formatDateGetCurrentTime() { + Date d = new Date(System.currentTimeMillis()); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + return dateFormat1.format(d); + } + + public static String formatDateGetDayWeek(long date) { + if (date == 0) { + return ""; + } + Date d = new Date(date); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd EEEE"); + return dateFormat1.format(d); + } + + + public static String formatDateGetFull(long date) { + if (date == 0) { + return ""; + } + Date d = new Date(date); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + return dateFormat1.format(d); + } + + public static String formatDateGetDay(long date) { + if (date == 0) { + return ""; + } + Date d = new Date(date); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd"); + return dateFormat1.format(d); + } + + public static boolean isEmpty(String s) { + if (TextUtils.isEmpty(s) || s.trim().equals("null")) { + return true; + } else { + return false; + } + } + + + /** + * 格式化浮点型 + * + * @param data + * @return + */ + public static String formatDouble(double data) { + return new DecimalFormat("0.00").format(data); + } + + + /** + * 格式化分钟 + * + * @param minutes + * @return + */ + public static String formatMinutes(int minutes) { + int hour = minutes / 60; + int minute = minutes % 60; + if (hour > 0 && minute > 0) { + return hour + "小时" + minute + "分钟"; + } else if (hour > 0) { + return hour + "小时"; + } else { + return minute + "分钟"; + } + } + + + //com.fawan.news + public static void goToMarket(Context context, String packageName) { + Uri uri = Uri.parse("market://details?id=" + packageName); + Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri); + try { + context.startActivity(goToMarket); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + } + } + + /** + * true为存在,false为不存在 + * + * @param context + * @param packageName + * @return + */ + public static boolean isInstallApp(Context context, String packageName) { + try { + context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + /** + * 格式化float 保留两位小数 + * + * @param data + * @return + */ + public static float formatFloat2(float data) { +// DecimalFormat decimalFormat = new DecimalFormat("0.00");//构造方法的字符格式这里如果小数不足2位,会以0补足. +// return decimalFormat.format(data);//返回字符串 + + int scale = 2;//设置位数 + int roundingMode = 4;//表示四舍五入,可以选择其他舍值方式,例如去尾,等等. + BigDecimal bd = new BigDecimal((double) data); + bd = bd.setScale(scale, roundingMode); + data = bd.floatValue(); + return data; + } + + /** + * 格式化float 保留两位小数 + * + * @param data + * @return + */ + public static float formatFloat1(float data) { +// DecimalFormat decimalFormat = new DecimalFormat("0.00");//构造方法的字符格式这里如果小数不足2位,会以0补足. +// return decimalFormat.format(data);//返回字符串 + + int scale = 1;//设置位数 + int roundingMode = 4;//表示四舍五入,可以选择其他舍值方式,例如去尾,等等. + BigDecimal bd = new BigDecimal((double) data); + bd = bd.setScale(scale, roundingMode); + data = bd.floatValue(); + return data; + } + + /** + * 格式化浮点型 + * + * @param data + * @return + */ + public static String formatDouble1(double data) { + return new DecimalFormat("0.0").format(data); + } + + /** + * Android 6.0 之前(不包括6.0)获取mac地址 + * 必须的权限 + * + * @param context * @return + */ + public static String getMacDefault(Context context) { + String mac = ""; + if (context == null) { + return mac; + } + WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo info = null; + try { + info = wifi.getConnectionInfo(); + } catch (Exception e) { + e.printStackTrace(); + } + + if (info == null) { + return null; + } + mac = info.getMacAddress(); + if (!TextUtils.isEmpty(mac)) { + mac = mac.toUpperCase(Locale.ENGLISH); + } + return mac; + } + + /** + * Android 6.0-Android 7.0 获取mac地址 + */ + public static String getMacAddress() { + String macSerial = null; + String str = ""; + + try { + Process pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address"); + InputStreamReader ir = new InputStreamReader(pp.getInputStream()); + LineNumberReader input = new LineNumberReader(ir); + + while (null != str) { + str = input.readLine(); + if (str != null) { + macSerial = str.trim();//去空格 + break; + } + } + } catch (IOException ex) { + // 赋予默认值 + ex.printStackTrace(); + } + + return macSerial; + } + + /** + * Android 7.0之后获取Mac地址 + * 遍历循环所有的网络接口,找到接口是 wlan0 + * 必须的权限 + * + * @return + */ + public static String getMacFromHardware() { + try { + ArrayList all = Collections.list(NetworkInterface.getNetworkInterfaces()); + for (NetworkInterface nif : all) { + if (!nif.getName().equals("wlan0")) + continue; + byte[] macBytes = nif.getHardwareAddress(); + if (macBytes == null) return ""; + StringBuilder res1 = new StringBuilder(); + for (Byte b : macBytes) { + res1.append(String.format("%02X:", b)); + } + if (!TextUtils.isEmpty(res1)) { + res1.deleteCharAt(res1.length() - 1); + } + return res1.toString(); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return ""; + } + + /** + * 获取mac地址(适配所有Android版本) + * + * @return + */ + public static String getMac(Context context) { + String mac = ""; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + mac = getMacDefault(context); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + mac = getMacAddress(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mac = getMacFromHardware(); + } + return mac; + } + + //把String转化为float + public static double convertToFloat(String number, double defaultValue) { + if (TextUtils.isEmpty(number)) { + return defaultValue; + } + try { + return Double.parseDouble(number); + } catch (Exception e) { + return defaultValue; + } + + } + + + /** + * 获取设备唯一 UDID + * + * @param context + * @return + */ + @SuppressLint("MissingPermission") + public static String getUDID(Context context) { +// return "SWSN:" + getMac(context); + String mac = getMac(context); + if (isEmpty(mac)) { + String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + if (!androidID.equals("")) { + try { + if (!"9774d56d682e549c".equals(androidID)) { + androidID = UUID.nameUUIDFromBytes(androidID.getBytes("utf8")).toString(); + } else { + @SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId(); + androidID = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")).toString() : UUID.randomUUID().toString(); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + return androidID; + } + + //需要权限 android.permission.READ_PHONE_STATE + TelephonyManager TelephonyMgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE); + String szImei = TelephonyMgr.getDeviceId(); + if (!szImei.equals("")) { + return szImei; + } + + //需要权限 android.permission.ACCESS_WIFI_STATE + WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + String m_szWLANMAC = wm.getConnectionInfo().getMacAddress(); + if (!m_szWLANMAC.equals("")) { + return m_szWLANMAC; + } + + //需要权限 android.permission.BLUETOOTH + BluetoothAdapter m_BluetoothAdapter = null; + m_BluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + String m_szBTMAC = m_BluetoothAdapter.getAddress(); + if (!m_szBTMAC.equals("")) { + return m_szBTMAC; + } + return getUniquePsuedoID(); + } else + return "SWSN:" + getMac(context); + } + + //获得 Psuedo ID + public static String getUniquePsuedoID() { + String serial = null; + String m_szDevIDShort = "35" + + Build.BOARD.length() % 10 + Build.BRAND.length() % 10 + + Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 + + Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 + + Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 + + Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 + + Build.TAGS.length() % 10 + Build.TYPE.length() % 10 + + Build.USER.length() % 10; //13 位 + try { + serial = android.os.Build.class.getField("SERIAL").get(null).toString(); + //API>=9 使用serial号 + return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString(); + } catch (Exception exception) { + //serial需要一个初始化,随意值 + serial = "serial"; + } + + //使用硬件信息拼凑出来的15位号码 + return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString(); + } + + /** + * 安装apk + * + * @param activity + * @param apkFile + */ + public static void installApk(Context activity, File apkFile) { + //文件有所有者概念,现在是属于当前进程的,需要把这个文件暴露给系统安装程序(其他进程)去安装 + //因此,可能会存在权限问题,需要做下面的设置 + //如果文件是sdcard上的,就不需要这个操作了 + try { + apkFile.setExecutable(true, false); + apkFile.setReadable(true, false); + apkFile.setWritable(true, false); + } catch (Exception e) { + e.printStackTrace(); + } + + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setAction(Intent.ACTION_VIEW); + Uri uri; + + //TODO N FileProvider + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + uri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileProvider", apkFile); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + + } else { + uri = Uri.fromFile(apkFile); + } + + intent.setDataAndType(uri, "application/vnd.android.package-archive"); + activity.startActivity(intent); + + //TODO 0 INSTALL PERMISSION + //在AndroidManifest中加入权限即可 + } + + /* +   @pararm apkPath 等待安装的app全路径,如:/sdcard/app/app.apk + **/ + public static boolean clientInstall(String apkPath) { + PrintWriter PrintWriter = null; + Process process = null; + try { + process = Runtime.getRuntime().exec("su"); + PrintWriter = new PrintWriter(process.getOutputStream()); + PrintWriter.println("chmod 777 " + apkPath); + PrintWriter + .println("export LD_LIBRARY_PATH=/vendor/lib:/system/lib"); + PrintWriter.println("pm install -r " + apkPath); + // PrintWriter.println("exit"); + PrintWriter.flush(); + PrintWriter.close(); + int value = process.waitFor(); + L.e("静默安装返回值:" + value); + return true; +// return returnResult(value); + } catch (Exception e) { + e.printStackTrace(); + L.e("安装apk出现异常"); + } finally { + if (process != null) { + process.destroy(); + } + } + return false; + } + + private void execLinuxCommand() { + String cmd = "sleep 120; am start -n 包名/包名.第一个Activity的名称"; + //Runtime对象 + Runtime runtime = Runtime.getRuntime(); + try { + Process localProcess = runtime.exec("su"); + OutputStream localOutputStream = localProcess.getOutputStream(); + DataOutputStream localDataOutputStream = new DataOutputStream(localOutputStream); + localDataOutputStream.writeBytes(cmd); + localDataOutputStream.flush(); + L.e("设备准备重启"); + } catch (IOException e) { + L.i("strLine:" + e.getMessage()); + e.printStackTrace(); + + } + } + + + /** + * 判断网络连接状态 + * + * @param context + * @return + */ + public static boolean isNetworkConnected(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo(); + if (mNetworkInfo != null) { + return mNetworkInfo.isAvailable(); + } + } + return false; + } + + /** + * 判断WiFi连接状态 + * + * @param context + * @return + */ + public static boolean isWifiConnected(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mWiFiNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + if (mWiFiNetworkInfo != null) { + return mWiFiNetworkInfo.isAvailable(); + } + } + return false; + } + + /** + * 判断移动网络状态 + * + * @param context + * @return + */ + public static boolean isMobileConnected(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); + if (mMobileNetworkInfo != null) { + return mMobileNetworkInfo.isAvailable(); + } + } + return false; + } + + /** + * 获取网络连接类型 + * + * @param context + * @return + */ + public static int getConnectedType(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo(); + if (mNetworkInfo != null && mNetworkInfo.isAvailable()) { + return mNetworkInfo.getType(); + } + } + return -1; + } + + public static int getMaxInt(int a, int b, int c) { + // 使用Math.max获取两个中的最大值 + int max1 = Math.max(a, b); + int max2 = Math.max(max1, c); // 使用之前计算的最大值来比较第三个 + return max2; + } + + public static int getMinInt(int a, int b, int c) { + // 使用Math.min获取两个中的最小值 + int min1 = Math.min(a, b); + int min2 = Math.min(min1, c); // 使用之前计算的最小值来比较第三个 + return min2; + } +} diff --git a/app/src/main/java/com/sw/st/utils/Base64.java b/app/src/main/java/com/sw/st/utils/Base64.java new file mode 100644 index 0000000..a1b1c70 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/Base64.java @@ -0,0 +1,268 @@ +package com.sw.st.utils; + +public final class Base64 { + + private static final int BASELENGTH = 128; + private static final int LOOKUPLENGTH = 64; + private static final int TWENTYFOURBITGROUP = 24; + private static final int EIGHTBIT = 8; + private static final int SIXTEENBIT = 16; + private static final int FOURBYTE = 4; + private static final int SIGN = -128; + private static char PAD = '='; + private static byte[] base64Alphabet = new byte[BASELENGTH]; + private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static { + for (int i = 0; i < BASELENGTH; ++i) { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + + } + + private static boolean isWhiteSpace(char octect) { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) { + return (octect == PAD); + } + + private static boolean isData(char octect) { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData + * Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) { + + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 + : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) + : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) + : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) + : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded + * string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) { + + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) + || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) + || !isData((d4 = base64Data[dataIndex++]))) { + return null; + }// if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) + || !isData((d2 = base64Data[dataIndex++]))) { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data + * the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/app/src/main/java/com/sw/st/utils/BatteryChangeReceiver.java b/app/src/main/java/com/sw/st/utils/BatteryChangeReceiver.java new file mode 100644 index 0000000..26bd287 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/BatteryChangeReceiver.java @@ -0,0 +1,44 @@ +package com.sw.st.utils; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.BatteryManager; + +public class BatteryChangeReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null) { + int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 0); + int healthy = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 0); + int voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); + int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 3); + String technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY); + int temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); + boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false); + + String desc = String.format("%s : 收到广播:%s", + AppUtil.formatDateGetCurrentTime(), intent.getAction()); + desc = String.format("%s\n电量刻度=%d", desc, scale); + desc = String.format("%s\n当前电量=%d", desc, level); + desc = String.format("%s\n当前状态=%s", desc, mStatus[status]); + desc = String.format("%s\n健康程度=%s", desc, mHealthy[healthy]); + desc = String.format("%s\n当前电压=%d", desc, voltage); + desc = String.format("%s\n当前电源=%s", desc, mPlugged[plugged]); + desc = String.format("%s\n当前技术=%s", desc, technology); + desc = String.format("%s\n当前温度=%d", desc, temperature / 10); + desc = String.format("%s\n是否提供电池=%s", desc, present ? "是" : "否"); + + L.e("desc===" + desc); + } + } + + private static String[] mStatus = {"不存在", "未知", "正在充电", "正在断电", "不在充电", "充满"}; + private static String[] mHealthy = {"不存在", "未知", "良好", "过热", "坏了", "短路", "未知错误", "冷却"}; + private static String[] mPlugged = {"电池", "充电器", "USB", "不存在", "无线"}; + + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/CrashHandler.java b/app/src/main/java/com/sw/st/utils/CrashHandler.java new file mode 100644 index 0000000..2ee11f6 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/CrashHandler.java @@ -0,0 +1,97 @@ +package com.sw.st.utils; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; + +import com.sw.st.net.helper.Convert; +import com.sw.st.ui.init.InitActivity; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class CrashHandler implements Thread.UncaughtExceptionHandler { + //系统默认UncaughtExceptionHandler + private Thread.UncaughtExceptionHandler mDefaultHandler; + private Context mContext; + private static CrashHandler mInstance; + private String DeviceUrl = "/businesslog/stBusinessLog/add";//上传错误网址 + + //空的构造方法 + private CrashHandler() { + + } + + //单例,获取CrashHandler实例 + public static synchronized CrashHandler getInstance() { + if (null == mInstance) { + mInstance = new CrashHandler(); + } + return mInstance; + } + + public void init(Context context) { + mContext = context; + mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + //设置该CrashHandler为系统默认的 + Thread.setDefaultUncaughtExceptionHandler(this); + } + + //uncaughtException 回调函数 + @Override + public void uncaughtException(Thread thread, Throwable ex) { + if (!handleException(ex) && mDefaultHandler != null) { + //如果自己没处理交给系统处理 + mDefaultHandler.uncaughtException(thread, ex); + } + } + + //收集错误信息.发送到服务器, 处理了该异常返回true, 否则false + private boolean handleException(Throwable ex) { + if (ex == null) { + return false; + } + //获取错误信息 + final Writer result = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(result); + ex.printStackTrace(printWriter); + String errorReport = result.toString(); + + Map map = new ConcurrentHashMap<>(); + map.put("errorData", errorReport); + map.put("id", AppUtil.getUDID(mContext)); + map.put("versionNo", AppUtil.getAppVersionName(mContext)); + map.put("versionName", AppUtil.getAppPackageName(mContext)); + String logData = Convert.toJson(map); + + Map requestMap = new ConcurrentHashMap<>(); + requestMap.put("logData", logData); + requestMap.put("logType", "1"); + + L.e(Convert.toJson(requestMap)); + + PrefUtils.setString(mContext, "crashUrl", InitActivity.BASE_URL + DeviceUrl); + PrefUtils.setString(mContext, "crashData", Convert.toJson(requestMap)); + + Intent intent = new Intent(mContext, InitActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + android.os.Process.killProcess(android.os.Process.myPid()); + + return true; + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/DeviceIdUtil.java b/app/src/main/java/com/sw/st/utils/DeviceIdUtil.java new file mode 100644 index 0000000..491fd93 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/DeviceIdUtil.java @@ -0,0 +1,247 @@ +package com.sw.st.utils; + +import android.content.Context; +import android.os.Environment; +import android.telephony.TelephonyManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.security.MessageDigest; +import java.util.UUID; + +public class DeviceIdUtil { + + //保存文件的路径 + private static final String CACHE_DEVICES_DIR = "csair-mmp-devices/devices"; + //保存的文件 采用隐藏文件的形式进行保存 + private static final String DEVICES_FILE_NAME = ".DEVICES"; + + /** + * 获取设备唯一标识符 + * + * @param context + * @return + */ + public static String getDeviceId(Context context) { +// //读取保存的在sd卡中的唯一标识符 + String deviceId = readDeviceID(context); + //判断是否已经生成过, + if (deviceId != null && !"".equals(deviceId)) { + return deviceId; + } + //用于生成最终的唯一标识符 + StringBuffer s = new StringBuffer(); + try { + //获取IMES(也就是常说的DeviceId) + deviceId = getIMIEStatus(context); + s.append(deviceId); + } catch (Exception e) { + e.printStackTrace(); + } + + try { + //获取设备的MACAddress地址 去掉中间相隔的冒号 + deviceId = getLocalMac(context).replace(":", ""); + s.append(deviceId); + } catch (Exception e) { + e.printStackTrace(); + } +// } + + //如果以上搜没有获取相应的则自己生成相应的UUID作为相应设备唯一标识符 + if (s == null || s.length() <= 0) { + UUID uuid = UUID.randomUUID(); + deviceId = uuid.toString().replace("-", ""); + s.append(deviceId); + } + //为了统一格式对设备的唯一标识进行md5加密 最终生成32位字符串 + String md5 = getMD5(s.toString(), false); + if (s.length() > 0) { + //持久化操作, 进行保存到SD卡中 + saveDeviceID(md5, context); + } + return md5; + } + + + /** + * 读取固定的文件中的内容,这里就是读取sd卡中保存的设备唯一标识符 + * + * @param context + * @return + */ + private static String readDeviceID(Context context) { + File file = getDevicesDir(context); + StringBuffer buffer = new StringBuffer(); + try { + FileInputStream fis = new FileInputStream(file); + InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); + Reader in = new BufferedReader(isr); + int i; + while ((i = in.read()) > -1) { + buffer.append((char) i); + } + in.close(); + return buffer.toString(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 获取设备的DeviceId(IMES) 这里需要相应的权限
+ * 需要 READ_PHONE_STATE 权限 + * + * @param context + * @return + */ + private static String getIMIEStatus(Context context) { + TelephonyManager tm = (TelephonyManager) context + .getSystemService(Context.TELEPHONY_SERVICE); + String deviceId = tm.getDeviceId(); + return deviceId; + } + + + /** + * 获取设备MAC 地址 由于 6.0 以后 WifiManager 得到的 MacAddress得到都是 相同的没有意义的内容 + * 所以采用以下方法获取Mac地址 + * + * @param context + * @return + */ + private static String getLocalMac(Context context) { +// WifiManager wifi = (WifiManager) context +// .getSystemService(Context.WIFI_SERVICE); +// WifiInfo info = wifi.getConnectionInfo(); +// return info.getMacAddress(); + + + String macAddress = null; + StringBuffer buf = new StringBuffer(); + NetworkInterface networkInterface = null; + try { + networkInterface = NetworkInterface.getByName("eth1"); + if (networkInterface == null) { + networkInterface = NetworkInterface.getByName("wlan0"); + } + if (networkInterface == null) { + return ""; + } + byte[] addr = networkInterface.getHardwareAddress(); + + + for (byte b : addr) { + buf.append(String.format("%02X:", b)); + } + if (buf.length() > 0) { + buf.deleteCharAt(buf.length() - 1); + } + macAddress = buf.toString(); + } catch (SocketException e) { + e.printStackTrace(); + return ""; + } + return macAddress; + + + } + + /** + * 保存 内容到 SD卡中, 这里保存的就是 设备唯一标识符 + * + * @param str + * @param context + */ + private static void saveDeviceID(String str, Context context) { + File file = getDevicesDir(context); + try { + FileOutputStream fos = new FileOutputStream(file); + Writer out = new OutputStreamWriter(fos, "UTF-8"); + out.write(str); + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 对挺特定的 内容进行 md5 加密 + * + * @param message 加密明文 + * @param upperCase 加密以后的字符串是是大写还是小写 true 大写 false 小写 + * @return + */ + private static String getMD5(String message, boolean upperCase) { + String md5str = ""; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + + byte[] input = message.getBytes(); + + byte[] buff = md.digest(input); + + md5str = bytesToHex(buff, upperCase); + + } catch (Exception e) { + e.printStackTrace(); + } + return md5str; + } + + + private static String bytesToHex(byte[] bytes, boolean upperCase) { + StringBuffer md5str = new StringBuffer(); + int digital; + for (int i = 0; i < bytes.length; i++) { + digital = bytes[i]; + + if (digital < 0) { + digital += 256; + } + if (digital < 16) { + md5str.append("0"); + } + md5str.append(Integer.toHexString(digital)); + } + if (upperCase) { + return md5str.toString().toUpperCase(); + } + return md5str.toString().toLowerCase(); + } + + /** + * 统一处理设备唯一标识 保存的文件的地址 + * + * @param context + * @return + */ + private static File getDevicesDir(Context context) { + File mCropFile = null; + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + File cropdir = new File(Environment.getExternalStorageDirectory(), CACHE_DEVICES_DIR); + if (!cropdir.exists()) { + cropdir.mkdirs(); + } + mCropFile = new File(cropdir, DEVICES_FILE_NAME); + } else { + File cropdir = new File(context.getFilesDir(), CACHE_DEVICES_DIR); + if (!cropdir.exists()) { + cropdir.mkdirs(); + } + mCropFile = new File(cropdir, DEVICES_FILE_NAME); + } + return mCropFile; + } +} + diff --git a/app/src/main/java/com/sw/st/utils/FileUtil.java b/app/src/main/java/com/sw/st/utils/FileUtil.java new file mode 100644 index 0000000..97211fa --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/FileUtil.java @@ -0,0 +1,692 @@ +package com.sw.st.utils; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.sw.st.application.App; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringReader; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +public class FileUtil { + private static boolean isSaveLog = false; + public static final String FILE_STR_PATH = App.getmContext().getExternalCacheDir().getAbsolutePath(); + + public static final int SIZETYPE_B = 1;//获取文件大小单位为B的double值 + public static final int SIZETYPE_KB = 2;//获取文件大小单位为KB的double值 + public static final int SIZETYPE_MB = 3;//获取文件大小单位为MB的double值 + public static final int SIZETYPE_GB = 4;//获取文件大小单位为GB的double值 + private static String urlNull = "原文件路径不存在"; + private static String isFile = "原文件不是文件"; + private static String canRead = "原文件不能读"; + private static String copyFalse = "备份失败!"; + private static String cFromFile = "创建原文件出错:"; + private static String ctoFile = "创建备份文件出错:"; + + + /** + * 写入文件 + * FileUtil.byteWriteFile(getExternalFilesDir("123"),"test.jpg",nv21); + * + * @param filePath + * @param fileName + * @param bytes + */ + public static void byteWriteFile(File filePath, String fileName, byte[] bytes) { + BufferedOutputStream bout = null; + try { + File file = new File(filePath, fileName); + if (file.exists() == false) { + if (file.getParentFile().exists() == false) { + file.getParentFile().mkdirs(); + } + file.createNewFile(); + } + bout = new BufferedOutputStream(new FileOutputStream(file, false)); + bout.write(bytes); + bout.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (bout != null) { + try { + bout.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * 获取文件名 + * + * @param path 路径 + * @return 文件名 + */ + public static String getFileName(String path) { + int index = path.lastIndexOf("/"); + return path.substring(index + 1); + } + + /** + * 根据文件路径获取文件 + * + * @param path 路径 + * @return + */ + public static File getFileByPath(String path) { + File file = new File(path); + if (file.exists()) { + return new File(path); + } else { + return null; + } + } + + /** + * 重命名文件 + * + * @param filePath 文件路径 + * @param newName 新名称 + * @return {@code true}: 重命名成功
{@code false}: 重命名失败 + */ + public static boolean rename(String filePath, String newName) { + return rename(getFileByPath(filePath), newName); + } + + /** + * 重命名文件 + * + * @param file 文件 + * @param newName 新名称 + * @return {@code true}: 重命名成功
{@code false}: 重命名失败 + */ + public static boolean rename(File file, String newName) { + // 文件为空返回false + if (file == null) return false; + // 文件不存在返回false + if (!file.exists()) return false; + // 新的文件名为空返回false + if (AppUtil.isEmpty(newName)) return false; + // 如果文件名没有改变返回true + if (newName.equals(file.getName())) return true; + File newFile = new File(file.getParent() + File.separator + newName); + // 如果重命名的文件已存在返回false + return !newFile.exists() + && file.renameTo(newFile); + } + + + /** + * 判断文件是否存在 + * + * @param path 文件的路径,含文件后缀和文件名 + * @return 是否存在 + */ + public static boolean isFileExists(String path) { + File file = new File(path); + if (file.exists()) { + return true; + } else { + return false; + } + } + + /** + * 判断文件是否存在 + * + * @return 是否存在 + */ + public static boolean isFileExists(File file) { + if (file.exists()) { + return true; + } else { + return false; + } + } + + /** + * 删除文件夹及文件夹下所有内容 + * + * @param path 文件夹路径 + * @return 返回是否删除成功 + */ + public static boolean deleteFiles(String path) { + if (getFileByPath(path) != null) { + return deleteFiles(getFileByPath(path)); + } else { + return false; + } + } + + /** + * 删除文件夹及文件夹下所有内容 + * + * @param file 文件 + * @return 返回是否删除成功 + */ + public static boolean deleteFiles(File file) { + try { + if (file.exists()) { // 判断文件是否存在 + if (file.isFile()) { // 判断是否是文件 + file.delete(); // delete()方法 + } else if (file.isDirectory()) { // 否则如果它是一个目录 + File files[] = file.listFiles(); // 声明目录下所有的文件 files[]; + for (int i = 0; i < files.length; i++) { // 遍历目录下所有的文件 + deleteFiles(files[i].getPath()); // 把每个文件 用这个方法进行迭代 + } + file.delete();//删除目录 + } + //file.delete(); + return true; + } else { + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * 获取文件的Uri + * + * @param path 文件的路径 + * @return 文件的Uri + */ + public static Uri getUriFromFile(String path) { + File file = new File(path); + return Uri.fromFile(file); + } + + /** + * 获取文件指定文件的指定单位的大小 + * + * @param filePath 文件路径 + * @param sizeType 获取大小的类型1为B、2为KB、3为MB、4为GB + * @return double值的大小 + */ + public static double getFileOrFilesSize(String filePath, int sizeType) { + File file = new File(filePath); + long blockSize = 0; + try { + if (file.isDirectory()) { + blockSize = getFileSizes(file); + } else { + blockSize = getFileSize(file); + } + } catch (Exception e) { + e.printStackTrace(); + L.i("获取文件大小", "获取失败!"); + } + return FormetFileSize(blockSize, sizeType); + } + + /** + * 调用此方法自动计算指定文件或指定文件夹的大小 + * + * @param filePath 文件路径 + * @return 计算好的带B、KB、MB、GB的字符串 + */ + public static String getAutoFileOrFilesSize(String filePath) { + File file = new File(filePath); + long blockSize = 0; + try { + if (file.isDirectory()) { + blockSize = getFileSizes(file); + } else { + blockSize = getFileSize(file); + } + } catch (Exception e) { + e.printStackTrace(); + L.i("获取文件大小", "获取失败!"); + } + return FormetFileSize(blockSize); + } + + /** + * 获取指定文件大小 + * + * @param file + * @return + * @throws Exception + */ + private static long getFileSize(File file) throws Exception { + long size = 0; + if (file.exists()) { + FileInputStream fis = null; + fis = new FileInputStream(file); + size = fis.available(); + } else { +// file.createNewFile(); + L.i("获取文件大小", "文件不存在!"); + } + return size; + } + + /** + * 获取目录下指定文件名的文件包括子目录 + *

大小写忽略

+ * + * @param dirPath 目录路径 + * @param fileName 文件名 + * @return 文件链表 + */ + public static List searchFileInDir(String dirPath, String fileName) { + return searchFileInDir(getFileByPath(dirPath), fileName); + } + + /** + * 获取目录下指定文件名的文件包括子目录 + *

大小写忽略

+ * + * @param dir 目录 + * @param fileName 文件名 + * @return 文件链表 + */ + public static List searchFileInDir(File dir, String fileName) { + if (dir == null || !dir.isDirectory()) return null; + List list = new ArrayList<>(); + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (file.getName().toUpperCase().equals(fileName.toUpperCase())) { + list.add(file); + } + if (file.isDirectory()) { + list.addAll(searchFileInDir(file, fileName)); + } + } + } + return list; + } + + + /** + * 获取指定文件夹 + * + * @param f + * @return + * @throws Exception + */ + private static long getFileSizes(File f) throws Exception { + long size = 0; + File flist[] = f.listFiles(); + for (int i = 0; i < flist.length; i++) { + if (flist[i].isDirectory()) { + size = size + getFileSizes(flist[i]); + } else { + size = size + getFileSize(flist[i]); + } + } + return size; + } + + /** + * 转换文件大小 + * + * @param fileS + * @return + */ + private static String FormetFileSize(long fileS) { + DecimalFormat df = new DecimalFormat("#.00"); + String fileSizeString = ""; + String wrongSize = "0B"; + if (fileS == 0) { + return wrongSize; + } + if (fileS < 1024) { + fileSizeString = df.format((double) fileS) + "B"; + } else if (fileS < 1048576) { + fileSizeString = df.format((double) fileS / 1024) + "KB"; + } else if (fileS < 1073741824) { + fileSizeString = df.format((double) fileS / 1048576) + "MB"; + } else { + fileSizeString = df.format((double) fileS / 1073741824) + "GB"; + } + return fileSizeString; + } + + /** + * 转换文件大小,指定转换的类型 + * + * @param fileS + * @param sizeType + * @return + */ + private static double FormetFileSize(long fileS, int sizeType) { + DecimalFormat df = new DecimalFormat("#.00"); + double fileSizeLong = 0; + switch (sizeType) { + case SIZETYPE_B: + fileSizeLong = Double.valueOf(df.format((double) fileS)); + break; + case SIZETYPE_KB: + fileSizeLong = Double.valueOf(df.format((double) fileS / 1024)); + break; + case SIZETYPE_MB: + fileSizeLong = Double.valueOf(df.format((double) fileS / 1048576)); + break; + case SIZETYPE_GB: + fileSizeLong = Double.valueOf(df.format((double) fileS / 1073741824)); + break; + default: + break; + } + return fileSizeLong; + } + + /** + * 复制文件 + * + * @param fromFilePath 旧文件地址和名称 + * @param toFilePath 新文件地址和名称 + * @return 返回备份文件的信息,ok是成功,其它就是错误 + */ + public static File copyFile(String fromFilePath, String toFilePath) { + File fromFile = null; + File toFile = null; + try { + fromFile = new File(fromFilePath); + } catch (Exception e) { + L.i(cFromFile + e.getMessage()); + return null; + } + + try { + toFile = new File(toFilePath); + if (toFile.isDirectory()) { + toFile = new File(toFilePath.endsWith("/") ? toFilePath + fromFile.getName() : toFilePath + "/" + fromFile.getName()); + } + } catch (Exception e) { + L.i(ctoFile + e.getMessage()); + return null; + } + + if (!fromFile.exists()) { + L.i(urlNull); + return null; + } + if (!fromFile.isFile()) { + L.i(isFile); + return null; + } + if (!fromFile.canRead()) { + L.i(canRead); + return null; + } + + // 复制到的路径如果不存在就创建 + if (!toFile.getParentFile().exists()) { + toFile.getParentFile().mkdirs(); + } + + if (toFile.exists()) { + toFile.delete(); + } + + if (!toFile.canWrite()) { + //return notWrite; + } + + try { + FileInputStream fosfrom = new FileInputStream( + fromFile); + FileOutputStream fosto = new FileOutputStream(toFile); + byte bt[] = new byte[1024]; + int c; + + while ((c = fosfrom.read(bt)) > 0) { + fosto.write(bt, 0, c); // 将内容写到新文件当中 + } + //关闭数据流 + fosfrom.close(); + fosto.close(); + } catch (Exception e) { + e.printStackTrace(); + L.i(copyFalse + e.getMessage()); + return null; + } + return toFile; + } + + /** + * 创建文件 + * + * @param path 文件路径 + * @return 创建的文件 + */ + public static synchronized File createNewFile(String path) { + File file = new File(path); + String parentPath = path.substring(0, path.lastIndexOf("/") + 1); + File parentFile = new File(parentPath); + if (!parentFile.exists()) { + parentFile.mkdirs(); + } + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + return file; + } + + /*** + * 根据文件后缀回去MIME类型 + ****/ + + private static String getMIMEType(File file) { + String type = "*/*"; + String fName = file.getName(); + //获取后缀名前的分隔符"."在fName中的位置。 + int dotIndex = fName.lastIndexOf("."); + if (dotIndex < 0) { + return type; + } + /* 获取文件的后缀名*/ + String end = fName.substring(dotIndex, fName.length()).toLowerCase(); + if (end == "") return type; + //在MIME和文件类型的匹配表中找到对应的MIME类型。 + for (int i = 0; i < MIME_MapTable.length; i++) { //MIME_MapTable??在这里你一定有疑问,这个MIME_MapTable是什么? + if (end.equals(MIME_MapTable[i][0])) + type = MIME_MapTable[i][1]; + } + return type; + } + + /** + * 获取全路径中的文件拓展名 + * + * @param file 文件 + * @return 文件拓展名 + */ + public static String getFileExtension(File file) { + if (file == null) return null; + return getFileExtension(file.getPath()); + } + + /** + * 获取全路径中的文件拓展名 + * + * @param filePath 文件路径 + * @return 文件拓展名 + */ + public static String getFileExtension(String filePath) { + if (AppUtil.isEmpty(filePath)) return filePath; + int lastPoi = filePath.lastIndexOf('.'); + int lastSep = filePath.lastIndexOf(File.separator); + if (lastPoi == -1 || lastSep >= lastPoi) return ""; + return filePath.substring(lastPoi + 1); + } + + /** + * 调用系统应用打开文件 + * + * @param context 上下文 + * @param file 文件 + */ + public static void openFile(Context context, File file) { + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + //设置intent的Action属性 + intent.setAction(Intent.ACTION_VIEW); + //获取文件file的MIME类型 + String type = getMIMEType(file); + //设置intent的data和Type属性。 + intent.setDataAndType(Uri.fromFile(file), type); + //跳转 + try { + context.startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + L.i("找不到打开此文件的应用!"); + } + } + + /** + * 保存文本文件 + * FileUtil.saveStrFile("测试", "log.txt", FILE_STR_PATH, true); + * + * @param res 写入内容 + * @param fileName 文件名 + * @param filePath 路径 + * @param append true新增 false替换 + * @return + */ + public static boolean saveStrFile(String res, String fileName, String filePath, boolean append) { + boolean flag = true; + BufferedReader bufferedReader = null; + BufferedWriter bufferedWriter = null; + try { + File file = new File(filePath, fileName); + + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + } + bufferedReader = new BufferedReader(new StringReader(res)); + bufferedWriter = new BufferedWriter(new FileWriter(file, append)); + char buf[] = new char[1024]; //字符缓冲区 + int len; + while ((len = bufferedReader.read(buf)) != -1) { + bufferedWriter.write(buf, 0, len); + } + bufferedWriter.flush(); + bufferedReader.close(); + bufferedWriter.close(); + } catch (IOException e) { + e.printStackTrace(); + flag = false; + return flag; + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return flag; + } + + public static void saveLog(String... info) { + if (!isSaveLog) { + return; + } + new Thread() { + @Override + public void run() { + super.run(); + StringBuffer stringBuffer = new StringBuffer(); + for (String s : info) { + stringBuffer.append(s).append("==="); + } + stringBuffer.append((AppUtil.formatDateGetCurrentTime())).append("\n"); + FileUtil.saveStrFile(stringBuffer.toString(), + "log" + AppUtil.formatDateGetDay(java.lang.System.currentTimeMillis()) + ".txt", FILE_STR_PATH, true); + + } + }.start(); + } + + private static final String[][] MIME_MapTable = { + // {后缀名,MIME类型} + {".3gp", "video/3gpp"}, + {".apk", "application/vnd.android.package-archive"}, + {".asf", "video/x-ms-asf"}, + {".avi", "video/x-msvideo"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".c", "text/plain"}, + {".class", "application/octet-stream"}, + {".conf", "text/plain"}, + {".cpp", "text/plain"}, + {".doc", "application/msword"}, + {".docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".exe", "application/octet-stream"}, + {".gif", "image/gif"}, + {".gtar", "application/x-gtar"}, + {".gz", "application/x-gzip"}, + {".h", "text/plain"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".jar", "application/java-archive"}, + {".java", "text/plain"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "application/x-javascript"}, + {".log", "text/plain"}, + {".m3u", "audio/x-mpegurl"}, + {".m4a", "audio/mp4a-latm"}, + {".m4b", "audio/mp4a-latm"}, + {".m4p", "audio/mp4a-latm"}, + {".m4u", "video/vnd.mpegurl"}, + {".m4v", "video/x-m4v"}, + {".mov", "video/quicktime"}, + {".mp2", "audio/x-mpeg"}, + {".mp3", "audio/x-mpeg"}, + {".mp4", "video/mp4"}, + {".mpc", "application/vnd.mpohun.certificate"}, + {".mpe", "video/mpeg"}, + {".mpeg", "video/mpeg"}, + {".mpg", "video/mpeg"}, + {".mpg4", "video/mp4"}, + {".mpga", "audio/mpeg"}, + {".msg", "application/vnd.ms-outlook"}, + {".ogg", "audio/ogg"}, + {".pdf", "application/pdf"}, + {".png", "image/png"}, + {".pps", "application/vnd.ms-powerpoint"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptx", + "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".prop", "text/plain"}, {".rc", "text/plain"}, + {".rmvb", "audio/x-pn-realaudio"}, {".rtf", "application/rtf"}, + {".sh", "text/plain"}, {".tar", "application/x-tar"}, + {".tgz", "application/x-compressed"}, {".txt", "text/plain"}, + {".wav", "audio/x-wav"}, {".wma", "audio/x-ms-wma"}, + {".wmv", "audio/x-ms-wmv"}, + {".wps", "application/vnd.ms-works"}, {".xml", "text/plain"}, + {".z", "application/x-compress"}, + {".zip", "application/x-zip-compressed"}, {"", "*/*"}}; +} diff --git a/app/src/main/java/com/sw/st/utils/GpioUtils.java b/app/src/main/java/com/sw/st/utils/GpioUtils.java new file mode 100644 index 0000000..2e9244a --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/GpioUtils.java @@ -0,0 +1,175 @@ +package com.sw.st.utils; + +import android.os.SystemClock; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.DataOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +/** + * Created by Administrator on 2018/11/9. + */ + +public class GpioUtils { + + private static final String TAG = "GpioUtils"; + + /* + 给export申请权限 + */ + public static void upgradeRootPermissionForExport(){ + upgradeRootPermission("/sys/class/gpio/export"); + } + + /* + 配置一个gpio路径 + */ + public static boolean exportGpio(int gpio) { + return writeNode("/sys/class/gpio/export", ""+gpio); + } + + /* + 给获取io口的状态的路径申请权限,该方法需要在exportGpio后调用 + */ + public static void upgradeRootPermissionForGpio(int gpio){ + upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/direction"); + upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/value"); + } + + + /* + 设置io口为输入或输出 + */ + public static boolean setGpioDirection(int gpio, int arg){ + String gpioDirection = ""; + if(arg == 0) gpioDirection = "out"; + else if(arg == 1) gpioDirection = "in"; + else return false; + return writeNode("/sys/class/gpio/gpio" + gpio + "/direction", gpioDirection); + } + + /* + 获取io口的状态为输出还是输入 + */ + public static String getGpioDirection(int gpio){ + String gpioDirection =""; + gpioDirection = readNode("/sys/class/gpio/gpio" + gpio + "/direction"); + return gpioDirection; + } + + /* + 给输出io口写值,高电平或低电平 + */ + public static boolean writeGpioValue(int gpio, String arg){ + return writeNode("/sys/class/gpio/gpio" + gpio + "/value", arg); + } + + //获取当前gpio是高还是低 + public static String getGpioValue(int gpio) { + return readNode("/sys/class/gpio/gpio" + gpio + "/value"); + } + + + private static boolean upgradeRootPermission(String path) { + Process process = null; + DataOutputStream os = null; + try { + String cmd="chmod 777 " + path; + process = Runtime.getRuntime().exec("su"); //切换到root帐号 + os = new DataOutputStream(process.getOutputStream()); + os.writeBytes(cmd + "\n"); + os.writeBytes("exit\n"); + os.flush(); + process.waitFor(); + } catch (Exception e) { + } finally { + try { + if (os != null) { + os.close(); + } + process.destroy(); + } catch (Exception e) { + } + } + try { + return process.waitFor()==0; + } catch (InterruptedException e) { + e.printStackTrace(); + } + return false; + } + + private static boolean writeNode(String path, String arg) { + Log.d(TAG, "Gpio_test set node path: " + path + " arg: " + arg); + if (path == null || arg == null) { + Log.e(TAG, "set node error"); + return false; + } + FileWriter fileWriter = null; + BufferedWriter bufferedWriter = null; + try { + fileWriter = new FileWriter(path); + fileWriter.write(arg); + } catch (Exception e) { + Log.e(TAG, "Gpio_test write node error!! path" + path + " arg: " + arg); + e.printStackTrace(); + return false; + } finally { + try { + if (fileWriter != null) { + fileWriter.close(); + } + if (bufferedWriter != null) { + bufferedWriter.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return true; + } + + private static long mTime = 0; + private static int mFailTimes = 0; + private static String readNode(String path) { + String result = ""; + + FileReader fread = null; + BufferedReader buffer = null; + try { + fread = new FileReader(path); + buffer = new BufferedReader(fread); + String str = null; + while ((str = buffer.readLine()) != null) { + result = str; + break; + } + mFailTimes = 0; + } catch (IOException e) { + Log.e(TAG, "IO Exception"); + e.printStackTrace(); + if (mTime == 0 || SystemClock.uptimeMillis() - mTime < 1000) { + mFailTimes++; + } + if (mFailTimes >= 3) { + Log.d(TAG, "read format node continuous failed three times, exist thread"); + } + } finally { + try { + if (buffer != null) { + buffer.close(); + } + if (fread != null) { + fread.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } +} diff --git a/app/src/main/java/com/sw/st/utils/InputMethod.java b/app/src/main/java/com/sw/st/utils/InputMethod.java new file mode 100644 index 0000000..234fb65 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/InputMethod.java @@ -0,0 +1,54 @@ +package com.sw.st.utils; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import java.util.Timer; +import java.util.TimerTask; + + +public class InputMethod { + public static boolean isSoftInputShow(Activity activity) { + // 虚拟键盘隐藏 判断view是否为空 + View view = activity.getWindow().peekDecorView(); + if (view != null) { + // 隐藏虚拟键盘 + InputMethodManager inputmanger = (InputMethodManager) activity + .getSystemService(Activity.INPUT_METHOD_SERVICE); +// inputmanger.hideSoftInputFromWindow(view.getWindowToken(),0); + return inputmanger.isActive() && activity.getWindow().getCurrentFocus() != null; + } + return false; + } + + static public void closeInputMethod(Context context, View view) { + try { + //获取输入法的服务 + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + //判断是否在激活状态 + if (imm.isActive()) { + //隐藏输入法!!, + imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } catch (Exception e) { + } finally { + } + } + + static public void openInputMethod(final View editText) { + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + + public void run() { + InputMethodManager inputManager = (InputMethodManager) editText + .getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + inputManager.showSoftInput(editText, 0); + } + + }, 200); + } + +} diff --git a/app/src/main/java/com/sw/st/utils/L.java b/app/src/main/java/com/sw/st/utils/L.java new file mode 100644 index 0000000..b42afb7 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/L.java @@ -0,0 +1,57 @@ +package com.sw.st.utils; + +/** + * Log统一管理类 + */ +public class L { + + private L() { + /* cannot be instantiated */ + throw new UnsupportedOperationException("cannot be instantiated"); + } + + public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化 + private static final String TAG = "mzf"; + + // 下面四个是默认tag的函数 + public static void i(String msg) { + if (isDebug) + android.util.Log.i(TAG, msg); + } + + public static void d(String msg) { + if (isDebug) + android.util.Log.d(TAG, msg); + } + + public static void e(String msg) { + if (isDebug) + android.util.Log.e(TAG, msg); + } + + public static void v(String msg) { + if (isDebug) + android.util.Log.v(TAG, msg); + } + + // 下面是传入自定义tag的函数 + public static void i(String tag, String msg) { + if (isDebug) + android.util.Log.i(tag, msg); + } + + public static void d(String tag, String msg) { + if (isDebug) + android.util.Log.d(tag, msg); + } + + public static void e(String tag, String msg) { + if (isDebug) + android.util.Log.e(tag, msg); + } + + public static void v(String tag, String msg) { + if (isDebug) + android.util.Log.v(tag, msg); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/LogcatHelper.java b/app/src/main/java/com/sw/st/utils/LogcatHelper.java new file mode 100644 index 0000000..88c0c96 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/LogcatHelper.java @@ -0,0 +1,165 @@ +package com.sw.st.utils; + +import android.content.Context; +import android.os.Environment; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class LogcatHelper { + + private static LogcatHelper INSTANCE = null; + private static String PATH_LOGCAT; + private LogDumper mLogDumper = null; + private int mPId; + + /** + * + * 初始化目录 + * + * */ + public void init(Context context) { + if (Environment.getExternalStorageState().equals( + Environment.MEDIA_MOUNTED)) {// 优先保存到SD卡中 + PATH_LOGCAT = Environment.getExternalStorageDirectory() + .getAbsolutePath() + File.separator + "sw"; + } else {// 如果SD卡不存在,就保存到本应用的目录下 + PATH_LOGCAT = context.getFilesDir().getAbsolutePath() + + File.separator + "sw"; + } + File file = new File(PATH_LOGCAT); + if (!file.exists()) { + file.mkdirs(); + } + + } + + public static LogcatHelper getInstance(Context context) { + if (INSTANCE == null) { + INSTANCE = new LogcatHelper(context); + } + return INSTANCE; + } + + private LogcatHelper(Context context) { + init(context); + mPId = android.os.Process.myPid(); + } + + public void start() { + if (mLogDumper == null) + mLogDumper = new LogDumper(String.valueOf(mPId), PATH_LOGCAT); + mLogDumper.start(); + } + + public void stop() { + if (mLogDumper != null) { + mLogDumper.stopLogs(); + mLogDumper = null; + } + } + + private class LogDumper extends Thread { + + private Process logcatProc; + private BufferedReader mReader = null; + private boolean mRunning = true; + String cmds = null; + private String mPID; + private FileOutputStream out = null; + + public LogDumper(String pid, String dir) { + mPID = pid; + try { + out = new FileOutputStream(new File(dir, "log-" + + getFileName() + ".txt")); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + /** + * + * 日志等级:*:v , *:d , *:w , *:e , *:f , *:s + * + * 显示当前mPID程序的 E和W等级的日志. + * + * */ + + // cmds = "logcat *:e *:w | grep \"(" + mPID + ")\""; + // cmds = "logcat | grep \"(" + mPID + ")\"";//打印所有日志信息 + // cmds = "logcat -s way";//打印标签过滤信息 + cmds = "logcat *:e *:i | grep \"(" + mPID + ")\""; + + } + + public void stopLogs() { + mRunning = false; + } + + @Override + public void run() { + try { + logcatProc = Runtime.getRuntime().exec(cmds); + mReader = new BufferedReader(new InputStreamReader( + logcatProc.getInputStream()), 1024); + String line = null; + while (mRunning && (line = mReader.readLine()) != null) { + if (!mRunning) { + break; + } + if (line.length() == 0) { + continue; + } + if (out != null && line.contains(mPID)) { + out.write((line + "\n").getBytes()); + } + } + + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (logcatProc != null) { + logcatProc.destroy(); + logcatProc = null; + } + if (mReader != null) { + try { + mReader.close(); + mReader = null; + } catch (IOException e) { + e.printStackTrace(); + } + } + if (out != null) { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + out = null; + } + + } + + } + + } + public String getFileName() { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + String date = format.format(new Date(System.currentTimeMillis())); + return date;// 2012年10月03日 23:41:31 + } + +// public String getDateEN() { +// SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); +// String date1 = format1.format(new Date(System.currentTimeMillis())); +// return date1;// 2012-10-03 23:41:31 +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/OnDoubleClickListener.java b/app/src/main/java/com/sw/st/utils/OnDoubleClickListener.java new file mode 100644 index 0000000..197e156 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/OnDoubleClickListener.java @@ -0,0 +1,55 @@ +package com.sw.st.utils; + +import android.annotation.SuppressLint; +import android.view.MotionEvent; +import android.view.View; + +public class OnDoubleClickListener implements View.OnTouchListener { + //记录点击次数 + private int count = 0; + //记录第一次点击时间 + private long firstClick = 0; + //记录第二次点击时间 + private long secondClick = 0; + //两次点击时间间隔,单位毫秒 + private final int totalTime = 1000; + //自定义回调接口,用于进行双击事件的回调给调用者 + private DoubleClickCallback mCallback; + + public interface DoubleClickCallback { + void onDoubleClick(); + } + + public OnDoubleClickListener(DoubleClickCallback callback) { + super(); + this.mCallback = callback; + } + + /** + * 触摸事件处理 + */ + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + if (MotionEvent.ACTION_DOWN == event.getAction()) {//按下 + count++; + if (1 == count) { + firstClick = System.currentTimeMillis();//记录第一次点击时间 + } else if (2 == count) { + secondClick = System.currentTimeMillis();//记录第二次点击时间 + if (secondClick - firstClick < totalTime) {//判断二次点击时间间隔是否在设定的间隔时间之内 + if (mCallback != null) { + mCallback.onDoubleClick(); + } + count = 0; + firstClick = 0; + } else { + firstClick = secondClick; + count = 1; + } + secondClick = 0; + } + } + return true; + } +} diff --git a/app/src/main/java/com/sw/st/utils/PowerChangeReceiver.java b/app/src/main/java/com/sw/st/utils/PowerChangeReceiver.java new file mode 100644 index 0000000..5aa15bf --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/PowerChangeReceiver.java @@ -0,0 +1,19 @@ +package com.sw.st.utils; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class PowerChangeReceiver extends BroadcastReceiver { + private static String mChange = ""; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null) { + mChange = String.format("%s\n%s : 收到广播:%s", + mChange, AppUtil.formatDateGetCurrentTime(), intent.getAction()); + + L.e(mChange); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/PrefUtils.java b/app/src/main/java/com/sw/st/utils/PrefUtils.java new file mode 100644 index 0000000..2da3902 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/PrefUtils.java @@ -0,0 +1,68 @@ +package com.sw.st.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +public class PrefUtils { + + public static final String PREF_NAME = "sw_selforder"; + + public static boolean getBoolean(Context ctx, String key, + boolean defaultValue) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + return sp.getBoolean(key, defaultValue); + } + + public static void setBoolean(Context ctx, String key, boolean value) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + sp.edit().putBoolean(key, value).commit(); + } + + public static String getString(Context ctx, String key, String defaultValue) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + return sp.getString(key, defaultValue); + } + + public static void setString(Context ctx, String key, String value) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + sp.edit().putString(key, value).commit(); + } + + public static int getInt(Context ctx, String key, int defaultValue) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + return sp.getInt(key, defaultValue); + } + + public static void setInt(Context ctx, String key, int value) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + sp.edit().putInt(key, value).commit(); + } + + public static float getFloat(Context ctx, String key, float defaultValue) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + return sp.getFloat(key, defaultValue); + } + + public static void setFloat(Context ctx, String key, float value) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + sp.edit().putFloat(key, value).commit(); + } + + public static void clearData(Context ctx, String key) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + sp.edit().remove(key).clear().commit(); + } + + public static void clearAllData(Context ctx) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + sp.edit().clear().commit(); + } +} diff --git a/app/src/main/java/com/sw/st/utils/ScanGunKeyEventHelper.java b/app/src/main/java/com/sw/st/utils/ScanGunKeyEventHelper.java new file mode 100644 index 0000000..544a51c --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/ScanGunKeyEventHelper.java @@ -0,0 +1,165 @@ +package com.sw.st.utils; + +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.text.TextUtils; +import android.view.KeyEvent; + +import static android.content.Context.INPUT_SERVICE; + +public class ScanGunKeyEventHelper { + private final static long MESSAGE_DELAY = 500; //延迟500ms,判断扫码是否完成。 + private final StringBuffer mStringBufferResult; //扫码内容 + private boolean mCaps; //大小写区分 + private final Handler mHandler; + private final Runnable mScanningFishedRunnable; + private OnScanSuccessListener mOnScanSuccessListener; + private final Context mContext; + + // private String mDeviceName = "TMC HIDKeyBoard"; + private String mDeviceName = "Linux 3.4.35 with ak-hsudc Composite Gadget (ACM + HID)"; + + + public ScanGunKeyEventHelper(Context context, OnScanSuccessListener onScanSuccessListener) { + + mContext = context; + mOnScanSuccessListener = onScanSuccessListener; + mStringBufferResult = new StringBuffer(); + mHandler = new Handler(); + mScanningFishedRunnable = this::performScanSuccess; + } + + /** + * 返回扫码成功后的结果 + */ + private void performScanSuccess() { + String barcode = mStringBufferResult.toString(); + if (mOnScanSuccessListener != null && !TextUtils.isEmpty(barcode)) + mOnScanSuccessListener.onScanSuccess(barcode); + mStringBufferResult.setLength(0); + } + + /** + * 扫码枪事件解析 + * + * @param event + */ + public void analysisKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + //字母大小写判断 + checkLetterStatus(event); + if (event.getAction() == KeyEvent.ACTION_DOWN) { + char aChar = getInputCode(event); + if (aChar != 0) { + mStringBufferResult.append(aChar); + } + if (keyCode == KeyEvent.KEYCODE_ENTER) { + //若为回车键,直接返回 + mHandler.removeCallbacks(mScanningFishedRunnable); + mHandler.post(mScanningFishedRunnable); + } else { + //延迟post,若500ms内,有其他事件 + mHandler.removeCallbacks(mScanningFishedRunnable); + mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY); + } + + } + } + + //检查shift键 + private void checkLetterStatus(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + //按着shift键,表示大写 + mCaps = true; + } else { + //松开shift键,表示小写 + mCaps = false; + } + } + } + + /** + * 获取扫描内容 + * + * @param event + * @return + */ + private char getInputCode(KeyEvent event) { + int keyCode = event.getKeyCode(); + char aChar; + if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) { + //字母 + aChar = (char) ((mCaps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A); + } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { + //数字 + aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0); + } else { + //其他符号 + switch (keyCode) { + case KeyEvent.KEYCODE_PERIOD: + aChar = '.'; + break; + case KeyEvent.KEYCODE_MINUS: + aChar = mCaps ? '_' : '-'; + break; + case KeyEvent.KEYCODE_SLASH: + aChar = '/'; + break; + case KeyEvent.KEYCODE_BACKSLASH: + aChar = mCaps ? '|' : '\\'; + break; + default: + aChar = 0; + break; + } + } + return aChar; + } + + public interface OnScanSuccessListener { + void onScanSuccess(String barcode); + } + + public void onDestroy() { + mHandler.removeCallbacks(mScanningFishedRunnable); + mOnScanSuccessListener = null; + } + + /** + * 输入设备是否存在 + * + * @param deviceName + * @return + */ + public boolean isInputDeviceExist(String deviceName) { + + InputManager inputManager = (InputManager) mContext.getSystemService(INPUT_SERVICE); + int[] deviceIds = inputManager.getInputDeviceIds(); + for (int id : deviceIds) { + if (inputManager.getInputDevice(id).getName().equals(deviceName)) { + return true; + } + } + return false; + } + + /** + * 是否为扫码枪事件(部分机型KeyEvent获取的名字错误) + * + * @param event + * @return + */ + public boolean isScanGunEvent(KeyEvent event) { + L.e("event===" + event.getDevice().getName() + + "===Char===" + event.getCharacters() + + "===Action===" + event.getAction()); + return event.getDevice().getName().equals(mDeviceName); + +// return true; + } + + +} diff --git a/app/src/main/java/com/sw/st/utils/SystemCtrlUtil.java b/app/src/main/java/com/sw/st/utils/SystemCtrlUtil.java new file mode 100644 index 0000000..69aa5e4 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/SystemCtrlUtil.java @@ -0,0 +1,177 @@ +package com.sw.st.utils; + +import android.content.Context; +import android.util.Log; + +import java.io.DataOutputStream; +import java.io.PrintWriter; + +/** + * Created by wujn on 2018/12/13. + * Version : v1.0 + * Function: 系统级别的控制,包括root + * + * 参考:https://blog.csdn.net/andywuchuanlong/article/details/44150317 + */ +public class SystemCtrlUtil { + + /** + * root下静默安装 + * */ + public static boolean rootSlienceInstallApk(String apkPath){ + PrintWriter PrintWriter = null; + Process process = null; + try { + process = Runtime.getRuntime().exec("su"); + PrintWriter = new PrintWriter(process.getOutputStream()); + PrintWriter.println("chmod 777 "+apkPath); + PrintWriter.println("export LD_LIBRARY_PATH=/vendor/lib:/system/lib"); + PrintWriter.println("pm install -r "+apkPath); +// PrintWriter.println("exit"); + PrintWriter.flush(); + PrintWriter.close(); + int value = process.waitFor(); + return returnResult(value); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + if(process!=null){ + process.destroy(); + } + } + return false; + } + + /** + * root下启动apk + * */ + public static boolean rootStartApk(String packageName,String activityName){ + boolean isSuccess = false; + String cmd = "am start -n " + packageName + "/" + activityName + " \n"; + Process process = null; + try { + process = Runtime.getRuntime().exec(cmd); + int value = process.waitFor(); + return returnResult(value); + } catch (Exception e) { + e.printStackTrace(); + } finally{ + if(process!=null){ + process.destroy(); + } + } + return isSuccess; + } + + + + /** + * root下静默删除 + * */ + public static boolean rootSlienceUninstallApk(String packageName){ + PrintWriter PrintWriter = null; + Process process = null; + try { + process = Runtime.getRuntime().exec("su"); + PrintWriter = new PrintWriter(process.getOutputStream()); + PrintWriter.println("LD_LIBRARY_PATH=/vendor/lib:/system/lib "); + PrintWriter.println("pm uninstall "+packageName); + PrintWriter.flush(); + PrintWriter.close(); + int value = process.waitFor(); + return returnResult(value); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + if(process!=null){ + process.destroy(); + } + } + return false; + } + + + + /** + * 判断手机是否有root权限 + */ + public static boolean sysHasRootPerssion(){ + PrintWriter PrintWriter = null; + Process process = null; + try { + process = Runtime.getRuntime().exec("su"); + PrintWriter = new PrintWriter(process.getOutputStream()); + PrintWriter.flush(); + PrintWriter.close(); + int value = process.waitFor(); + return returnResult(value); + } catch (Exception e) { + e.printStackTrace(); + }finally{ + if(process!=null){ + process.destroy(); + } + } + return false; + } + + /** + * root下执行cmd的返回值 + * */ + private static boolean returnResult(int value){ + // 代表成功 + if (value == 0) { + return true; + } else if (value == 1) { // 失败 + return false; + } else { // 未知情况 + return false; + } + } + + + /** + * 判断app是否有root权限 + */ + public static boolean appHasRootPerssion(Context context){ + return RootCommand("chmod 777 "+context.getPackageCodePath()); + } + + + /** + * 应用程序运行命令获取 Root权限,设备必须已破解(获得ROOT权限) + * @param command 命令:String apkRoot="chmod 777 "+getPackageCodePath(); RootCommand(apkRoot); + * @return 应用程序是/否获取Root权限 + */ + public static boolean RootCommand(String command) + { + Process process = null; + DataOutputStream os = null; + try + { + process = Runtime.getRuntime().exec("su"); + os = new DataOutputStream(process.getOutputStream()); + os.writeBytes(command + "\n"); + os.writeBytes("exit\n"); + os.flush(); + return returnResult(process.waitFor()); + } catch (Exception e) + { + Log.d("*** DEBUG ***", "ROOT REE" + e.getMessage()); + return false; + } finally + { + try + { + if (os != null) + { + os.close(); + } + process.destroy(); + } catch (Exception e) + { + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/T.java b/app/src/main/java/com/sw/st/utils/T.java new file mode 100644 index 0000000..a7c3db8 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/T.java @@ -0,0 +1,86 @@ +package com.sw.st.utils; + +import android.content.Context; +import android.widget.Toast; + +/** + * Toast统一管理类 + */ +public class T { + + private T() { + /* cannot be instantiated */ + throw new UnsupportedOperationException("cannot be instantiated"); + } + + public static boolean isShow = true; + + /** + * 短时间显示Toast + * + * @param context + * @param message + */ + public static void showShort(Context context, CharSequence message) { + if (isShow) + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } + + /** + * 短时间显示Toast + * + * @param context + * @param message + */ + public static void showShort(Context context, int message) { + if (isShow) + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } + + /** + * 长时间显示Toast + * + * @param context + * @param message + */ + public static void showLong(Context context, CharSequence message) { + if (isShow) + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + } + + /** + * 长时间显示Toast + * + * @param context + * @param message + */ + public static void showLong(Context context, int message) { + if (isShow) + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + } + + /** + * 自定义显示Toast时间 + * + * @param context + * @param message + * @param duration + */ + public static void show(Context context, CharSequence message, int duration) { + if (isShow) + Toast.makeText(context, message, duration).show(); + } + + /** + * 自定义显示Toast时间 + * + * @param context + * @param message + * @param duration + */ + public static void show(Context context, int message, int duration) { + if (isShow) + Toast.makeText(context, message, duration).show(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/ToastUtils.java b/app/src/main/java/com/sw/st/utils/ToastUtils.java new file mode 100644 index 0000000..de64699 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/ToastUtils.java @@ -0,0 +1,101 @@ +package com.sw.st.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + + +import com.sw.st.R; + +import java.util.Timer; +import java.util.TimerTask; + + +/** + * The type Toast utils. + */ +public class ToastUtils { + + private static Toast toast; + @SuppressLint("StaticFieldLeak") + private static TextView textCenterView; + + /** + * Show center toast. + * + * @param text the text + */ + public static void showToast(Context context, String text) { + if (toast == null) { + View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); + textCenterView = view.findViewById(R.id.toast_tv); + toast = new Toast(context); + toast.setGravity(Gravity.CENTER, 0, 20); + toast.setDuration(Toast.LENGTH_SHORT); + toast.setView(view); + } + + textCenterView.setText(text); + toast.show(); + } + + /** + * Show center toast. + * + * @param text the text + */ + public static void showToast(Context context, String text, int duration) { + if (toast == null) { + View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); + textCenterView = view.findViewById(R.id.toast_tv); + toast = new Toast(context); + toast.setGravity(Gravity.CENTER, 0, 20); + toast.setView(view); + } + + textCenterView.setText(text); + toast.setDuration(duration); + toast.show(); + } + + public static void showLongToast(Context context, String text) { + if (toast == null) { + View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); + textCenterView = view.findViewById(R.id.toast_tv); + toast = new Toast(context); + toast.setGravity(Gravity.TOP, 0, 20); + toast.setDuration(Toast.LENGTH_LONG); + toast.setView(view); + } + textCenterView.setText(text); + showMyToast(toast, 1000000 * 30); + } + + + //自定义Toast控件 + private static void showMyToast(final Toast toast, final int cnt) { + final Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + toast.show(); + } + }, 0, Toast.LENGTH_LONG); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + toast.cancel(); + timer.cancel(); + } + }, cnt); + } + + public static void showSystemToast(Context context, String text) { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); + } + +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/MqttFactory.java b/app/src/main/java/com/sw/st/utils/mqtt/MqttFactory.java new file mode 100644 index 0000000..97d8c28 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/MqttFactory.java @@ -0,0 +1,187 @@ +package com.sw.st.utils.mqtt; + + +import android.content.Context; + +import com.sw.st.utils.L; +import com.sw.st.utils.mqtt.service.Config; +import com.sw.st.utils.mqtt.service.MqttActionListener; +import com.sw.st.utils.mqtt.service.MqttAndroidClient; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; + + +/** + * Created by ZhangHs on 2018/4/19. + * 对mqtt的封装 + */ + +public class MqttFactory { + private final Context context; + private final String serverIP; + private final String port; + private final boolean autoConnect; + private final int connectionTimeout; + private final int keepAliveInterval; + private final String clientId; + private final MqttCallback callback; + + private MqttAndroidClient client; + private MqttConnectOptions options; + private MqttActionListener actionListener; + + public MqttFactory(Builder builder) { + this.context = builder.context; + this.serverIP = builder.serverIP; + this.autoConnect = builder.autoConnect; + this.connectionTimeout = builder.connectionTimeout; + this.keepAliveInterval = builder.keepAliveInterval; + this.clientId = builder.clientId; + this.callback = builder.callback; + this.port = builder.port; + init(); + } + + private void init() { + client = new MqttAndroidClient(context, "tcp://" + serverIP + ":" + port, clientId); + client.setCallback(callback); + options = new MqttConnectOptions(); + options.setCleanSession(false); + options.setAutomaticReconnect(autoConnect); + options.setConnectionTimeout(connectionTimeout); + options.setKeepAliveInterval(keepAliveInterval); + options.setUserName(Config.MQTT_USER_NAME); + options.setPassword(Config.MQTT_PASS_WORD.toCharArray()); + } + + public void connect() { + try { + client.connect(options, null, actionListener = new MqttActionListener(MqttActionListener.TYPE.CONNECTMQTT) { + @Override + public void onSuccess(IMqttToken iMqttToken) { + super.onSuccess(iMqttToken); + L.e("连接成功"); + } + }); + } catch (MqttException e) { + e.printStackTrace(); + } + } + + public void connect(MqttActionListener listener) { + try { + client.connect(options, null, actionListener = listener); + } catch (MqttException e) { + e.printStackTrace(); + } + } + + public void reconnect() { + try { + client.reconnect(); + } catch (MqttException e) { + e.printStackTrace(); + } + } + + public static class Builder { + private Context context; + private String serverIP; + private String port; + private boolean autoConnect; + private int connectionTimeout; + private int keepAliveInterval; + private String clientId; + private MqttCallback callback; + + public Builder(Context context) { + this.context = context; + } + + public Builder port(String port) { + this.port = port; + return this; + } + + public Builder serverIP(String serverIP) { + this.serverIP = serverIP; + return this; + } + + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Builder keepAliveInterval(int keepAliveInterval) { + this.keepAliveInterval = keepAliveInterval; + return this; + } + + public Builder autoConnect(boolean autoConnect) { + this.autoConnect = autoConnect; + return this; + } + + public Builder connectionTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + public Builder callback(MqttCallback callback) { + this.callback = callback; + return this; + } + + public MqttFactory build() { + return new MqttFactory(this); + } + + } + + + public void subscribe(String[] topics, int[] qos, Object userContext, IMqttActionListener listener) { + if (client != null && actionListener != null && actionListener.isConnect()) { + try { + client.subscribe(topics, qos, userContext, listener); + } catch (MqttException e) { + e.printStackTrace(); + } + } + } + + public void publish(String topic, byte[] messages, int qos, boolean retain, Object userContext, IMqttActionListener listener) { + if (client != null && actionListener != null && actionListener.isConnect()) { + try { + client.publish(topic, messages, qos, retain, userContext, listener); + } catch (MqttException e) { + e.printStackTrace(); + } + } + } + + public boolean isConnect() { + if (client != null && actionListener != null) { + + return actionListener.isConnect() && client.isConnected(); + } else { + return false; + } + } + + public void destroy() { + if (client != null && actionListener != null && actionListener.isConnect()) + try { + client.disconnect(); + } catch (MqttException e) { + e.printStackTrace(); + } + client = null; + } + + +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/MqttIn.java b/app/src/main/java/com/sw/st/utils/mqtt/MqttIn.java new file mode 100644 index 0000000..3b827a1 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/MqttIn.java @@ -0,0 +1,91 @@ +package com.sw.st.utils.mqtt; + +import android.content.Context; + +import com.sw.st.utils.mqtt.service.MqttActionListener; +import com.sw.st.utils.mqtt.service.MqttCallBackListener; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; + +/** + * Created by ZhangHs on 2018/4/19. + */ + +public class MqttIn { + public static MqttIn mqttIn; + private Context context; + private MqttFactory factory; + + private MqttIn(Context context) { + this.context = context; + } + + public static MqttIn getInstance(Context context) { + if (mqttIn == null) { + mqttIn = new MqttIn(context); + } + return mqttIn; + } + + public void init(String ip, String port) { + if (factory == null) { + factory = new MqttFactory.Builder(context). + autoConnect(false) + .clientId(System.currentTimeMillis() + "") +// .clientId("a20170e2-72aa-3230-89ae-86911ff9f74c") + .connectionTimeout(100) + .keepAliveInterval(20) +// .serverIP("vip.shuziweidao.com") + .serverIP("192.168.10.173") +// .serverIP(ip) + .port(port) + .callback(new MqttCallBackListener()) + .build(); + } + } + + public void connect() { + if (factory != null) { + factory.connect(); + } + } + + public void connect(MqttActionListener listener) { + if (factory != null) { + factory.connect(listener); + } + } + + public void reconnect() { + if (factory != null) { + factory.reconnect(); + } + } + + public boolean isConnect() { + if (factory != null) { + return factory.isConnect(); + } + return false; + } + + public void destroy() { + if (factory != null) { + factory.destroy(); + } + factory = null; + } + + public void subscribe(String[] topics, int[] qos, Object userContext, IMqttActionListener listener) { + if (factory != null) { + factory.subscribe(topics, qos, userContext, listener); + } + } + + public void publish(String topic, byte[] messages, int qos, boolean retain, Object userContext, IMqttActionListener listener) { + if (factory != null) { + factory.publish(topic, messages, qos, retain, userContext, listener); + } + } + +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/AlarmPingSender.java b/app/src/main/java/com/sw/st/utils/mqtt/service/AlarmPingSender.java new file mode 100644 index 0000000..041efeb --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/AlarmPingSender.java @@ -0,0 +1,126 @@ +package com.sw.st.utils.mqtt.service; + +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build.VERSION; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttPingSender; +import org.eclipse.paho.client.mqttv3.internal.ClientComms; + + +/** + * Created by ZhangHs on 2018/4/20. + */ +@SuppressLint("WrongConstant") +public class AlarmPingSender implements MqttPingSender { + private static final String TAG = "AlarmPingSender"; + private ClientComms comms; + private MqttService service; + private BroadcastReceiver alarmReceiver; + private AlarmPingSender that; + private PendingIntent pendingIntent; + private volatile boolean hasStarted = false; + + public AlarmPingSender(MqttService service) { + if(service == null) { + throw new IllegalArgumentException("Neither service nor client can be null."); + } else { + this.service = service; + this.that = this; + } + } + + + public void init(ClientComms comms) { + this.comms = comms; + this.alarmReceiver = new AlarmReceiver(); + } + + + + public void start() { +// String action = MqttServiceConstants.PING_SENDER+ this.comms.getClient().getClientId(); + String action = MqttServiceConstants.PING_SENDER+"sw"; + Log.d(TAG, "Register alarmreceiver to MqttService " + action); + this.service.registerReceiver(this.alarmReceiver, new IntentFilter(action)); + this.pendingIntent = PendingIntent.getBroadcast(this.service, 0, new Intent(action), PendingIntent.FLAG_IMMUTABLE);//134217728 + this.schedule(this.comms.getKeepAlive()); + this.hasStarted = true; + } + + public void stop() { + Log.d(TAG, "Unregister alarmreceiver to MqttService" + this.comms.getClient().getClientId()); + if(this.hasStarted) { + if(this.pendingIntent != null) { + AlarmManager alarmManager = (AlarmManager)this.service.getSystemService("alarm"); + alarmManager.cancel(this.pendingIntent); + } + + this.hasStarted = false; + + try { + this.service.unregisterReceiver(this.alarmReceiver); + } catch (IllegalArgumentException var2) { + ; + } + } + + } + + public void schedule(long delayInMilliseconds) { + long nextAlarmInMilliseconds = System.currentTimeMillis() + delayInMilliseconds; + Log.d(TAG, "Schedule next alarm at " + nextAlarmInMilliseconds); + AlarmManager alarmManager = (AlarmManager)this.service.getSystemService("alarm"); + if(VERSION.SDK_INT >= 23) { + Log.d(TAG, "Alarm scheule using setExactAndAllowWhileIdle, next: " + delayInMilliseconds); + alarmManager.setExactAndAllowWhileIdle(0, nextAlarmInMilliseconds, this.pendingIntent); + } else if(VERSION.SDK_INT >= 19) { + Log.d(TAG, "Alarm scheule using setExact, delay: " + delayInMilliseconds); + alarmManager.setExact(0, nextAlarmInMilliseconds, this.pendingIntent); + } else { + alarmManager.set(0, nextAlarmInMilliseconds, this.pendingIntent); + } + + } + + class AlarmReceiver extends BroadcastReceiver { + private WakeLock wakelock; + private final String wakeLockTag; + + AlarmReceiver() { + this.wakeLockTag = MqttServiceConstants.PING_WAKELOCK +AlarmPingSender.this.that.comms.getClient().getClientId(); + } + + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "Sending Ping at:" + System.currentTimeMillis()); + PowerManager pm = (PowerManager) AlarmPingSender.this.service.getSystemService("power"); + this.wakelock = pm.newWakeLock(1, this.wakeLockTag); + this.wakelock.acquire(); + IMqttToken token = AlarmPingSender.this.comms.checkForActivity(new IMqttActionListener() { + public void onSuccess(IMqttToken asyncActionToken) { + Log.d(TAG, "Success. Release lock(" + AlarmReceiver.this.wakeLockTag + "):" + System.currentTimeMillis()); + AlarmReceiver.this.wakelock.release(); + } + + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + Log.d(TAG, "Failure. Release lock(" + AlarmReceiver.this.wakeLockTag + "):" + System.currentTimeMillis()); + AlarmReceiver.this.wakelock.release(); + } + }); + if(token == null && this.wakelock.isHeld()) { + this.wakelock.release(); + } + + } + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/Config.java b/app/src/main/java/com/sw/st/utils/mqtt/service/Config.java new file mode 100644 index 0000000..3fb1005 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/Config.java @@ -0,0 +1,12 @@ +package com.sw.st.utils.mqtt.service; + + +public class Config { + +// public static String MQTT_SERVER_IP = "192.168.7.22"; + public static String MQTT_SERVER_IP = "vip.shuziweidao.com"; + + public static String MQTT_SERVER_PORT = "1883"; + public static String MQTT_USER_NAME = "sw_mqtt"; + public static String MQTT_PASS_WORD = "150326sw@"; +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/DatabaseMessageStore.java b/app/src/main/java/com/sw/st/utils/mqtt/service/DatabaseMessageStore.java new file mode 100644 index 0000000..3f042a0 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/DatabaseMessageStore.java @@ -0,0 +1,254 @@ +package com.sw.st.utils.mqtt.service; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.database.sqlite.SQLiteOpenHelper; + +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.util.Iterator; +import java.util.UUID; + + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public class DatabaseMessageStore implements MessageStore { + private static final String TAG = "DatabaseMessageStore"; + private static final String MTIMESTAMP = "mtimestamp"; + private static final String ARRIVED_MESSAGE_TABLE_NAME = "MqttArrivedMessageTable"; + private SQLiteDatabase db = null; + private MQTTDatabaseHelper mqttDb = null; + private MqttTraceHandler traceHandler = null; + + public DatabaseMessageStore(MqttService service, Context context) { + this.traceHandler = service; + this.mqttDb = new MQTTDatabaseHelper(this.traceHandler, context); + this.traceHandler.traceDebug(TAG, "DatabaseMessageStore complete"); + } + + public String storeArrived(String clientHandle, String topic, MqttMessage message) { + this.db = this.mqttDb.getWritableDatabase(); + this.traceHandler.traceDebug(TAG, "storeArrived{" + clientHandle + "}, {" + message.toString() + "}"); + byte[] payload = message.getPayload(); + int qos = message.getQos(); + boolean retained = message.isRetained(); + boolean duplicate = message.isDuplicate(); + ContentValues values = new ContentValues(); + String id = UUID.randomUUID().toString(); + values.put(MqttServiceConstants.MESSAGE_ID, id); + values.put(MqttServiceConstants.CLIENT_HANDLE, clientHandle); + values.put(MqttServiceConstants.DESTINATION_NAME, topic); + values.put(MqttServiceConstants.PAYLOAD, payload); + values.put(MqttServiceConstants.QOS, Integer.valueOf(qos)); + values.put(MqttServiceConstants.RETAINED, Boolean.valueOf(retained)); + values.put(MqttServiceConstants.DUPLICATE, Boolean.valueOf(duplicate)); + values.put(MTIMESTAMP, Long.valueOf(System.currentTimeMillis())); + + try { + this.db.insertOrThrow(ARRIVED_MESSAGE_TABLE_NAME, (String)null, values); + } catch (SQLException var11) { + this.traceHandler.traceException(TAG, "onUpgrade", var11); + throw var11; + } + + int count = this.getArrivedRowCount(clientHandle); + this.traceHandler.traceDebug(TAG, "storeArrived: inserted message with id of {" + id + "} - Number of messages in database for this clientHandle = " + count); + return id; + } + + private int getArrivedRowCount(String clientHandle) { + int count = 0; + String[] projection = new String[]{"messageId"}; + String selection = "clientHandle=?"; + String[] selectionArgs = new String[]{clientHandle}; + Cursor c = this.db.query(ARRIVED_MESSAGE_TABLE_NAME, projection, selection, selectionArgs, (String)null, (String)null, (String)null); + if(c.moveToFirst()) { + count = c.getInt(0); + } + + c.close(); + return count; + } + + public boolean discardArrived(String clientHandle, String id) { + this.db = this.mqttDb.getWritableDatabase(); + this.traceHandler.traceDebug(TAG, "discardArrived{" + clientHandle + "}, {" + id + "}"); + String[] selectionArgs = new String[]{id, clientHandle}; + + int rows; + try { + rows = this.db.delete(ARRIVED_MESSAGE_TABLE_NAME, "messageId=? AND clientHandle=?", selectionArgs); + } catch (SQLException var6) { + this.traceHandler.traceException(TAG, "discardArrived", var6); + throw var6; + } + + if(rows != 1) { + this.traceHandler.traceError(TAG, "discardArrived - Error deleting message {" + id + "} from database: Rows affected = " + rows); + return false; + } else { + int count = this.getArrivedRowCount(clientHandle); + this.traceHandler.traceDebug(TAG, "discardArrived - Message deleted successfully. - messages in db for this clientHandle " + count); + return true; + } + } + + public Iterator getAllArrivedMessages(final String clientHandle) { + return new Iterator() { + private Cursor c; + private boolean hasNext; + private final String[] selectionArgs = new String[]{clientHandle}; + + { + DatabaseMessageStore.this.db = DatabaseMessageStore.this.mqttDb.getWritableDatabase(); + if(clientHandle == null) { + this.c = DatabaseMessageStore.this.db.query(ARRIVED_MESSAGE_TABLE_NAME, (String[])null, (String)null, (String[])null, (String)null, (String)null, "mtimestamp ASC"); + } else { + this.c = DatabaseMessageStore.this.db.query(ARRIVED_MESSAGE_TABLE_NAME, (String[])null, "clientHandle=?", this.selectionArgs, (String)null, (String)null, "mtimestamp ASC"); + } + + this.hasNext = this.c.moveToFirst(); + } + + public boolean hasNext() { + if(!this.hasNext) { + this.c.close(); + } + + return this.hasNext; + } + + public StoredMessage next() { + String messageId = this.c.getString(this.c.getColumnIndex(MqttServiceConstants.MESSAGE_ID)); + String clientHandlex = this.c.getString(this.c.getColumnIndex(MqttServiceConstants.CLIENT_HANDLE)); + String topic = this.c.getString(this.c.getColumnIndex(MqttServiceConstants.DESTINATION_NAME)); + byte[] payload = this.c.getBlob(this.c.getColumnIndex(MqttServiceConstants.PAYLOAD)); + int qos = this.c.getInt(this.c.getColumnIndex(MqttServiceConstants.QOS)); + boolean retained = Boolean.parseBoolean(this.c.getString(this.c.getColumnIndex(MqttServiceConstants.RETAINED))); + boolean dup = Boolean.parseBoolean(this.c.getString(this.c.getColumnIndex(MqttServiceConstants.DUPLICATE))); + MqttMessageHack message = DatabaseMessageStore.this.new MqttMessageHack(payload); + message.setQos(qos); + message.setRetained(retained); + message.setDuplicate(dup); + this.hasNext = this.c.moveToNext(); + return DatabaseMessageStore.this.new DbStoredData(messageId, clientHandlex, topic, message); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + protected void finalize() throws Throwable { + this.c.close(); + super.finalize(); + } + }; + } + + public void clearArrivedMessages(String clientHandle) { + this.db = this.mqttDb.getWritableDatabase(); + String[] selectionArgs = new String[]{clientHandle}; +// int rows = false; + int rows; + if(clientHandle == null) { + this.traceHandler.traceDebug(TAG, "clearArrivedMessages: clearing the table"); + rows = this.db.delete(ARRIVED_MESSAGE_TABLE_NAME, (String)null, (String[])null); + } else { + this.traceHandler.traceDebug(TAG, "clearArrivedMessages: clearing the table of " + clientHandle + " messages"); + rows = this.db.delete(ARRIVED_MESSAGE_TABLE_NAME, "clientHandle=?", selectionArgs); + } + + this.traceHandler.traceDebug(TAG, "clearArrivedMessages: rows affected = " + rows); + } + + public void close() { + if(this.db != null) { + this.db.close(); + } + + } + + private class MqttMessageHack extends MqttMessage { + public MqttMessageHack(byte[] payload) { + super(payload); + } + + protected void setDuplicate(boolean dup) { + super.setDuplicate(dup); + } + } + + private class DbStoredData implements StoredMessage { + private String messageId; + private String clientHandle; + private String topic; + private MqttMessage message; + + DbStoredData(String messageId, String clientHandle, String topic, MqttMessage message) { + this.messageId = messageId; + this.topic = topic; + this.message = message; + } + + public String getMessageId() { + return this.messageId; + } + + public String getClientHandle() { + return this.clientHandle; + } + + public String getTopic() { + return this.topic; + } + + public MqttMessage getMessage() { + return this.message; + } + } + + private static class MQTTDatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "MQTTDatabaseHelper"; + private static final String DATABASE_NAME = "mqttAndroidService.db"; + private static final int DATABASE_VERSION = 1; + private MqttTraceHandler traceHandler = null; + + public MQTTDatabaseHelper(MqttTraceHandler traceHandler, Context context) { + super(context, DATABASE_NAME, (CursorFactory)null, DATABASE_VERSION); + this.traceHandler = traceHandler; + } + + public void onCreate(SQLiteDatabase database) { + String createArrivedTableStatement = "CREATE TABLE MqttArrivedMessageTable(messageId TEXT PRIMARY KEY, clientHandle TEXT, destinationName TEXT, payload BLOB, qos INTEGER, retained TEXT, duplicate TEXT, mtimestamp INTEGER);"; + this.traceHandler.traceDebug(TAG, "onCreate {" + createArrivedTableStatement + "}"); + + try { + database.execSQL(createArrivedTableStatement); + this.traceHandler.traceDebug(TAG, "created the table"); + } catch (SQLException var4) { + this.traceHandler.traceException(TAG, "onCreate", var4); + throw var4; + } + } + + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + this.traceHandler.traceDebug(TAG, "onUpgrade"); + + try { + db.execSQL("DROP TABLE IF EXISTS MqttArrivedMessageTable"); + } catch (SQLException var5) { + this.traceHandler.traceException(TAG, "onUpgrade", var5); + throw var5; + } + + this.onCreate(db); + this.traceHandler.traceDebug(TAG, "onUpgrade complete"); + } + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MessageStore.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MessageStore.java new file mode 100644 index 0000000..50027e7 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MessageStore.java @@ -0,0 +1,31 @@ +package com.sw.st.utils.mqtt.service; + +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.util.Iterator; + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public interface MessageStore { + String storeArrived(String var1, String var2, MqttMessage var3); + + boolean discardArrived(String var1, String var2); + + Iterator getAllArrivedMessages(String var1); + + void clearArrivedMessages(String var1); + + void close(); + + public interface StoredMessage { + String getMessageId(); + + String getClientHandle(); + + String getTopic(); + + MqttMessage getMessage(); + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttActionListener.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttActionListener.java new file mode 100644 index 0000000..d36d89a --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttActionListener.java @@ -0,0 +1,67 @@ +package com.sw.st.utils.mqtt.service; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; + + +/** + * Created by ZhangHs on 2018/4/19. + * 执行动作的监听:有发布主题,订阅主题,链接等 + */ + +public class MqttActionListener implements IMqttActionListener { + private boolean isConnect = false; + private TYPE type; + + public enum TYPE { + CONNECTMQTT, + PUSHTOPIC, + SUBCRIBE + } + + public MqttActionListener(TYPE type) { + this.type = type; + } + + @Override + public void onSuccess(IMqttToken iMqttToken) { + switch (type) { + case CONNECTMQTT: + System.out.println("链接成功"); + isConnect = true; + break; + case PUSHTOPIC: + System.out.println("发布成功"); + break; + case SUBCRIBE: + System.out.println("订阅成功"); + break; + default: + break; + } + } + + @Override + public void onFailure(IMqttToken iMqttToken, Throwable throwable) { + switch (type) { + case CONNECTMQTT: + System.out.println("链接失败" + " " + throwable.getMessage() + " "); + if (throwable.getCause() != null) { + System.out.println("链接失败" + " " + throwable.getCause().getMessage() + " "); + } + break; + case PUSHTOPIC: + System.out.println("发布失败"); + break; + case SUBCRIBE: + System.out.println("订阅失败"); + break; + default: + break; + } + } + + public boolean isConnect() { + return isConnect; + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttAndroidClient.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttAndroidClient.java new file mode 100644 index 0000000..1ea8c54 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttAndroidClient.java @@ -0,0 +1,638 @@ +package com.sw.st.utils.mqtt.service; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.util.SparseArray; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions; +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; +import org.eclipse.paho.client.mqttv3.MqttClientPersistence; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.MqttPersistenceException; +import org.eclipse.paho.client.mqttv3.MqttSecurityException; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + + +/** + * Created by ZhangHs on 2018/4/20. + */ +public class MqttAndroidClient extends BroadcastReceiver implements IMqttAsyncClient { + private static final String SERVICE_NAME = "com.sw.st.utils.mqtt.service.MqttService"; + private static final int BIND_SERVICE_FLAG = 0; + private static final ExecutorService pool = Executors.newCachedThreadPool(); + private final MyServiceConnection serviceConnection; + private MqttService mqttService; + private String clientHandle; + private Context myContext; + private final SparseArray tokenMap; + private int tokenNumber; + private final String serverURI; + private final String clientId; + private MqttClientPersistence persistence; + private MqttConnectOptions connectOptions; + private IMqttToken connectToken; + private MqttCallback callback; + private MqttTraceHandler traceCallback; + private final Ack messageAck; + private boolean traceEnabled; + private volatile boolean receiverRegistered; + private volatile boolean bindedService; + + public MqttAndroidClient(Context context, String serverURI, String clientId) { + this(context, serverURI, clientId, (MqttClientPersistence) null, Ack.AUTO_ACK); + } + + public MqttAndroidClient(Context ctx, String serverURI, String clientId, Ack ackType) { + this(ctx, serverURI, clientId, (MqttClientPersistence) null, ackType); + } + + public MqttAndroidClient(Context ctx, String serverURI, String clientId, MqttClientPersistence persistence) { + this(ctx, serverURI, clientId, persistence, Ack.AUTO_ACK); + } + + public MqttAndroidClient(Context context, String serverURI, String clientId, MqttClientPersistence persistence, Ack ackType) { + this.serviceConnection = new MyServiceConnection(null); + this.tokenMap = new SparseArray(); + this.tokenNumber = 0; + this.persistence = null; + this.traceEnabled = false; + this.receiverRegistered = false; + this.bindedService = false; + this.myContext = context; + this.serverURI = serverURI; + this.clientId = clientId; + this.persistence = persistence; + this.messageAck = ackType; + } + + public boolean isConnected() { + return this.clientHandle != null && this.mqttService != null && this.mqttService.isConnected(this.clientHandle); + } + + public String getClientId() { + return this.clientId; + } + + public String getServerURI() { + return this.serverURI; + } + + public void close() { + if (this.mqttService != null) { + if (this.clientHandle == null) { + this.clientHandle = this.mqttService.getClient(this.serverURI, this.clientId, this.myContext.getApplicationInfo().packageName, this.persistence); + } + + this.mqttService.close(this.clientHandle); + } + + } + + public IMqttToken connect() throws MqttException { + return this.connect((Object) null, (IMqttActionListener) null); + } + + public IMqttToken connect(MqttConnectOptions options) throws MqttException { + return this.connect(options, (Object) null, (IMqttActionListener) null); + } + + public IMqttToken connect(Object userContext, IMqttActionListener callback) throws MqttException { + return this.connect(new MqttConnectOptions(), userContext, callback); + } + + + @SuppressLint("WrongConstant") + public IMqttToken connect(MqttConnectOptions options, Object userContext, IMqttActionListener callback) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, userContext, callback); + this.connectOptions = options; + this.connectToken = token; + if (this.mqttService == null) { + Intent serviceStartIntent = new Intent(); + serviceStartIntent.setClassName(this.myContext, SERVICE_NAME); + Object service = this.myContext.startService(serviceStartIntent); + if (service == null) { + IMqttActionListener listener = token.getActionCallback(); + if (listener != null) { + listener.onFailure(token, new RuntimeException("cannot start service MqttService")); + } + } + + this.myContext.bindService(serviceStartIntent, this.serviceConnection, 1); + if (!this.receiverRegistered) { + this.registerReceiver(this); + } + } else { + pool.execute(new Runnable() { + public void run() { + MqttAndroidClient.this.doConnect(); + if (!MqttAndroidClient.this.receiverRegistered) { + MqttAndroidClient.this.registerReceiver(MqttAndroidClient.this); + } + + } + }); + } + + return token; + } + + private void registerReceiver(BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter(); + filter.addAction(MqttServiceConstants.CALLBACK_TO_ACTIVITY); + LocalBroadcastManager.getInstance(this.myContext).registerReceiver(receiver, filter); + this.receiverRegistered = true; + } + + private void doConnect() { + if (this.clientHandle == null) { + this.clientHandle = this.mqttService.getClient(this.serverURI, this.clientId, this.myContext.getApplicationInfo().packageName, this.persistence); + } + + this.mqttService.setTraceEnabled(this.traceEnabled); + this.mqttService.setTraceCallbackId(this.clientHandle); + String activityToken = this.storeToken(this.connectToken); + + try { + this.mqttService.connect(this.clientHandle, this.connectOptions, (String) null, activityToken); + } catch (MqttException var4) { + IMqttActionListener listener = this.connectToken.getActionCallback(); + if (listener != null) { + listener.onFailure(this.connectToken, var4); + } + } + + } + + public IMqttToken disconnect() throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, (Object) null, (IMqttActionListener) null); + String activityToken = this.storeToken(token); + this.mqttService.disconnect(this.clientHandle, (String) null, activityToken); + return token; + } + + public IMqttToken disconnect(long quiesceTimeout) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, (Object) null, (IMqttActionListener) null); + String activityToken = this.storeToken(token); + this.mqttService.disconnect(this.clientHandle, quiesceTimeout, (String) null, activityToken); + return token; + } + + public IMqttToken disconnect(Object userContext, IMqttActionListener callback) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, userContext, callback); + String activityToken = this.storeToken(token); + this.mqttService.disconnect(this.clientHandle, (String) null, activityToken); + return token; + } + + public IMqttToken disconnect(long quiesceTimeout, Object userContext, IMqttActionListener callback) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, userContext, callback); + String activityToken = this.storeToken(token); + this.mqttService.disconnect(this.clientHandle, quiesceTimeout, (String) null, activityToken); + return token; + } + + public IMqttDeliveryToken publish(String topic, byte[] payload, int qos, boolean retained) throws MqttException, MqttPersistenceException { + return this.publish(topic, payload, qos, retained, (Object) null, (IMqttActionListener) null); + } + + public IMqttDeliveryToken publish(String topic, MqttMessage message) throws MqttException, MqttPersistenceException { + return this.publish(topic, message, (Object) null, (IMqttActionListener) null); + } + + public IMqttDeliveryToken publish(String topic, byte[] payload, int qos, boolean retained, Object userContext, IMqttActionListener callback) throws MqttException, MqttPersistenceException { + MqttMessage message = new MqttMessage(payload); + message.setQos(qos); + message.setRetained(retained); + MqttDeliveryTokenAndroid token = new MqttDeliveryTokenAndroid(this, userContext, callback, message); + String activityToken = this.storeToken(token); + IMqttDeliveryToken internalToken = this.mqttService.publish(this.clientHandle, topic, payload, qos, retained, (String) null, activityToken); + token.setDelegate(internalToken); + return token; + } + + public IMqttDeliveryToken publish(String topic, MqttMessage message, Object userContext, IMqttActionListener callback) throws MqttException, MqttPersistenceException { + MqttDeliveryTokenAndroid token = new MqttDeliveryTokenAndroid(this, userContext, callback, message); + String activityToken = this.storeToken(token); + IMqttDeliveryToken internalToken = this.mqttService.publish(this.clientHandle, topic, message, (String) null, activityToken); + token.setDelegate(internalToken); + return token; + } + + public IMqttToken subscribe(String topic, int qos) throws MqttException, MqttSecurityException { + return this.subscribe(topic, qos, (Object) null, (IMqttActionListener) null); + } + + public IMqttToken subscribe(String[] topic, int[] qos) throws MqttException, MqttSecurityException { + return this.subscribe(topic, qos, (Object) null, (IMqttActionListener) null); + } + + public IMqttToken subscribe(String topic, int qos, Object userContext, IMqttActionListener callback) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, userContext, callback, new String[]{topic}); + String activityToken = this.storeToken(token); + this.mqttService.subscribe(this.clientHandle, topic, qos, (String) null, activityToken); + return token; + } + + public IMqttToken subscribe(String[] topic, int[] qos, Object userContext, IMqttActionListener callback) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, userContext, callback, topic); + String activityToken = this.storeToken(token); + this.mqttService.subscribe(this.clientHandle, topic, qos, (String) null, activityToken); + return token; + } + + public IMqttToken subscribe(String topicFilter, int qos, Object userContext, IMqttActionListener callback, IMqttMessageListener messageListener) throws MqttException { + return this.subscribe(new String[]{topicFilter}, new int[]{qos}, userContext, callback, new IMqttMessageListener[]{messageListener}); + } + + public IMqttToken subscribe(String topicFilter, int qos, IMqttMessageListener messageListener) throws MqttException { + return this.subscribe(topicFilter, qos, (Object) null, (IMqttActionListener) null, messageListener); + } + + public IMqttToken subscribe(String[] topicFilters, int[] qos, IMqttMessageListener[] messageListeners) throws MqttException { + return this.subscribe(topicFilters, qos, (Object) null, (IMqttActionListener) null, messageListeners); + } + + public IMqttToken subscribe(String[] topicFilters, int[] qos, Object userContext, IMqttActionListener callback, IMqttMessageListener[] messageListeners) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, userContext, callback, topicFilters); + String activityToken = this.storeToken(token); + this.mqttService.subscribe(this.clientHandle, topicFilters, qos, (String) null, activityToken, messageListeners); + return null; + } + + public IMqttToken unsubscribe(String topic) throws MqttException { + return this.unsubscribe((String) topic, (Object) null, (IMqttActionListener) null); + } + + public IMqttToken unsubscribe(String[] topic) throws MqttException { + return this.unsubscribe((String[]) topic, (Object) null, (IMqttActionListener) null); + } + + public IMqttToken unsubscribe(String topic, Object userContext, IMqttActionListener callback) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, userContext, callback); + String activityToken = this.storeToken(token); + this.mqttService.unsubscribe(this.clientHandle, topic, (String) null, activityToken); + return token; + } + + public IMqttToken unsubscribe(String[] topic, Object userContext, IMqttActionListener callback) throws MqttException { + IMqttToken token = new MqttTokenAndroid(this, userContext, callback); + String activityToken = this.storeToken(token); + this.mqttService.unsubscribe(this.clientHandle, topic, (String) null, activityToken); + return token; + } + + @Override + public boolean removeMessage(IMqttDeliveryToken iMqttDeliveryToken) throws MqttException { + return false; + } + + public IMqttDeliveryToken[] getPendingDeliveryTokens() { + return this.mqttService.getPendingDeliveryTokens(this.clientHandle); + } + + public void setCallback(MqttCallback callback) { + this.callback = callback; + } + + public void setTraceCallback(MqttTraceHandler traceCallback) { + this.traceCallback = traceCallback; + } + + public void setTraceEnabled(boolean traceEnabled) { + this.traceEnabled = traceEnabled; + if (this.mqttService != null) { + this.mqttService.setTraceEnabled(traceEnabled); + } + + } + + public void onReceive(Context context, Intent intent) { + Bundle data = intent.getExtras(); + String handleFromIntent = data.getString(MqttServiceConstants.CALLBACK_CLIENT_HANDLE); + if (handleFromIntent != null && handleFromIntent.equals(this.clientHandle)) { + String action = data.getString(MqttServiceConstants.CALLBACK_ACTION); + if ("connect".equals(action)) { + this.connectAction(data); + } else if ("connectExtended".equals(action)) { + this.connectExtendedAction(data); + } else if ("messageArrived".equals(action)) { + this.messageArrivedAction(data); + } else if ("subscribe".equals(action)) { + this.subscribeAction(data); + } else if ("unsubscribe".equals(action)) { + this.unSubscribeAction(data); + } else if ("send".equals(action)) { + this.sendAction(data); + } else if ("messageDelivered".equals(action)) { + this.messageDeliveredAction(data); + } else if ("onConnectionLost".equals(action)) { + this.connectionLostAction(data); + } else if ("disconnect".equals(action)) { + this.disconnected(data); + } else if ("trace".equals(action)) { + this.traceAction(data); + } else { + this.mqttService.traceError("MqttService", "Callback action doesn't exist."); + } + + } + } + + public boolean acknowledgeMessage(String messageId) { + if (this.messageAck == Ack.MANUAL_ACK) { + Status status = this.mqttService.acknowledgeMessageArrival(this.clientHandle, messageId); + return status == Status.OK; + } else { + return false; + } + } + + public void messageArrivedComplete(int messageId, int qos) throws MqttException { + throw new UnsupportedOperationException(); + } + + public void setManualAcks(boolean manualAcks) { + throw new UnsupportedOperationException(); + } + + @Override + public void reconnect() throws MqttException { + if (this.mqttService == null) { + doConnect(); + } else + this.mqttService.reconnect(); + } + + private void connectAction(Bundle data) { + IMqttToken token = this.connectToken; + this.removeMqttToken(data); + this.simpleAction(token, data); + } + + private void disconnected(Bundle data) { + this.clientHandle = null; + IMqttToken token = this.removeMqttToken(data); + if (token != null) { + ((MqttTokenAndroid) token).notifyComplete(); + } + + if (this.callback != null) { + this.callback.connectionLost((Throwable) null); + } + + } + + private void connectionLostAction(Bundle data) { + if (this.callback != null) { + Exception reason = (Exception) data.getSerializable(MqttServiceConstants.CALLBACK_EXCEPTION); + this.callback.connectionLost(reason); + } + + } + + private void connectExtendedAction(Bundle data) { + if (this.callback instanceof MqttCallbackExtended) { + boolean reconnect = data.getBoolean(MqttServiceConstants.CALLBACK_RECONNECT, false); + String serverURI = data.getString(MqttServiceConstants.CALLBACK_SERVER_URI); + ((MqttCallbackExtended) this.callback).connectComplete(reconnect, serverURI); + } + + } + + private void simpleAction(IMqttToken token, Bundle data) { + if (token != null) { + Status status = (Status) data.getSerializable(MqttServiceConstants.CALLBACK_STATUS); + if (status == Status.OK) { + ((MqttTokenAndroid) token).notifyComplete(); + } else { + Exception exceptionThrown = (Exception) data.getSerializable(MqttServiceConstants.CALLBACK_EXCEPTION); + ((MqttTokenAndroid) token).notifyFailure(exceptionThrown); + } + } else { + this.mqttService.traceError("MqttService", "simpleAction : token is null"); + } + + } + + private void sendAction(Bundle data) { + IMqttToken token = this.getMqttToken(data); + this.simpleAction(token, data); + } + + private void subscribeAction(Bundle data) { + IMqttToken token = this.removeMqttToken(data); + this.simpleAction(token, data); + } + + private void unSubscribeAction(Bundle data) { + IMqttToken token = this.removeMqttToken(data); + this.simpleAction(token, data); + } + + private void messageDeliveredAction(Bundle data) { + IMqttToken token = this.removeMqttToken(data); + if (token != null && this.callback != null) { + Status status = (Status) data.getSerializable(MqttServiceConstants.CALLBACK_STATUS); + if (status == Status.OK && token instanceof IMqttDeliveryToken) { + this.callback.deliveryComplete((IMqttDeliveryToken) token); + } + } + + } + + private void messageArrivedAction(Bundle data) { + if (this.callback != null) { + String messageId = data.getString(MqttServiceConstants.CALLBACK_MESSAGE_ID); + String destinationName = data.getString(MqttServiceConstants.CALLBACK_DESTINATION_NAME); + ParcelableMqttMessage message = (ParcelableMqttMessage) data.getParcelable(MqttServiceConstants.CALLBACK_MESSAGE_PARCEL); + + try { + if (this.messageAck == Ack.AUTO_ACK) { + this.callback.messageArrived(destinationName, message); + this.mqttService.acknowledgeMessageArrival(this.clientHandle, messageId); + } else { + message.messageId = messageId; + this.callback.messageArrived(destinationName, message); + } + } catch (Exception var6) { + ; + } + } + + } + + private void traceAction(Bundle data) { + if (this.traceCallback != null) { + String severity = data.getString(MqttServiceConstants.CALLBACK_TRACE_SEVERITY); + String message = data.getString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE); + String tag = data.getString(MqttServiceConstants.CALLBACK_TRACE_TAG); + if ("debug".equals(severity)) { + this.traceCallback.traceDebug(tag, message); + } else if ("error".equals(severity)) { + this.traceCallback.traceError(tag, message); + } else { + Exception e = (Exception) data.getSerializable(MqttServiceConstants.CALLBACK_EXCEPTION); + this.traceCallback.traceException(tag, message, e); + } + } + + } + + private synchronized String storeToken(IMqttToken token) { + this.tokenMap.put(this.tokenNumber, token); + return Integer.toString(this.tokenNumber++); + } + + private synchronized IMqttToken removeMqttToken(Bundle data) { + String activityToken = data.getString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN); + if (activityToken != null) { + int tokenNumber = Integer.parseInt(activityToken); + IMqttToken token = (IMqttToken) this.tokenMap.get(tokenNumber); + this.tokenMap.delete(tokenNumber); + return token; + } else { + return null; + } + } + + private synchronized IMqttToken getMqttToken(Bundle data) { + String activityToken = data.getString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN); + return (IMqttToken) this.tokenMap.get(Integer.parseInt(activityToken)); + } + + public void setBufferOpts(DisconnectedBufferOptions bufferOpts) { + this.mqttService.setBufferOpts(this.clientHandle, bufferOpts); + } + + public int getBufferedMessageCount() { + return this.mqttService.getBufferedMessageCount(this.clientHandle); + } + + public MqttMessage getBufferedMessage(int bufferIndex) { + return this.mqttService.getBufferedMessage(this.clientHandle, bufferIndex); + } + + public void deleteBufferedMessage(int bufferIndex) { + this.mqttService.deleteBufferedMessage(this.clientHandle, bufferIndex); + } + + @Override + public int getInFlightMessageCount() { + return 0; + } + + public SSLSocketFactory getSSLSocketFactory(InputStream keyStore, String password) throws MqttSecurityException { + try { + SSLContext ctx = null; + SSLSocketFactory sslSockFactory = null; + KeyStore ts = KeyStore.getInstance("BKS"); + ts.load(keyStore, password.toCharArray()); + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init(ts); + TrustManager[] tm = tmf.getTrustManagers(); + ctx = SSLContext.getInstance("TLSv1"); + ctx.init((KeyManager[]) null, tm, (SecureRandom) null); + sslSockFactory = ctx.getSocketFactory(); + return sslSockFactory; + } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyManagementException | KeyStoreException var8) { + throw new MqttSecurityException(var8); + } + } + + public void disconnectForcibly() throws MqttException { + throw new UnsupportedOperationException(); + } + + public void disconnectForcibly(long disconnectTimeout) throws MqttException { + throw new UnsupportedOperationException(); + } + + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout) throws MqttException { + throw new UnsupportedOperationException(); + } + + public void unregisterResources() { + if (this.myContext != null && this.receiverRegistered) { + synchronized (this) { + LocalBroadcastManager.getInstance(this.myContext).unregisterReceiver(this); + this.receiverRegistered = false; + } + + if (this.bindedService) { + try { + this.myContext.unbindService(this.serviceConnection); + this.bindedService = false; + } catch (IllegalArgumentException var3) { + ; + } + } + } + + } + + public void registerResources(Context context) { + if (context != null) { + this.myContext = context; + if (!this.receiverRegistered) { + this.registerReceiver(this); + } + } + + } + + private final class MyServiceConnection implements ServiceConnection { + private MyServiceConnection(Object o) { + } + + public void onServiceConnected(ComponentName name, IBinder binder) { + MqttAndroidClient.this.mqttService = ((MqttServiceBinder) binder).getService(); + MqttAndroidClient.this.bindedService = true; + MqttAndroidClient.this.doConnect(); + } + + public void onServiceDisconnected(ComponentName name) { + MqttAndroidClient.this.mqttService = null; + } + } + + public static enum Ack { + AUTO_ACK, + MANUAL_ACK; + + private Ack() { + } + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttCallBackListener.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttCallBackListener.java new file mode 100644 index 0000000..6fe1e2a --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttCallBackListener.java @@ -0,0 +1,84 @@ +package com.sw.st.utils.mqtt.service; + + +import com.sw.st.model.MessageEvent; + +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.greenrobot.eventbus.EventBus; +import org.json.JSONException; +import org.json.JSONObject; + + +/** + * Created by ZhangHs on 2018/4/19. + * 接收服务器推送过来的消息 + */ + +public class MqttCallBackListener implements MqttCallbackExtended { + /** + * 链接成功 + * + * @param b + * @param s + */ + @Override + public void connectComplete(boolean b, String s) { + + } + + /** + * 丢包 + * + * @param throwable + */ + @Override + public void connectionLost(Throwable throwable) { + + } + + /** + * 消息抵达 + * + * @param s + * @param mqttMessage + * @throws Exception + */ + @Override + public void messageArrived(String s, MqttMessage mqttMessage) throws Exception { + System.out.println("111111111111 " + s); + byte[] bb = mqttMessage.getPayload(); + String receiveMsg = new String(bb); + System.out.println("222222222222 " + receiveMsg); + + JSONObject jsonObject = new JSONObject(receiveMsg); + int type = jsonObject.optInt("type"); + if (type == 1) {//菜品相关 + EventBus.getDefault().post(new MessageEvent(MessageEvent.MESSAGE_UPDATE_FOOD, + jsonObject.optString("foodList"))); + } + + +// JSONObject jsonObject = new JSONObject(new String(bb)); +// long currentTime = System.currentTimeMillis(); +// long pushTime = jsonObject.optLong("message"); +// FileUtil.saveStrFile(jsonObject.optString("no") + +// "===" + +// pushTime + +// "===" + +// currentTime + +// "===" + +// (currentTime - pushTime) + "\n", "log.txt", FILE_STR_PATH, true); + } + + /** + * 消息被消费 + * + * @param iMqttDeliveryToken + */ + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttConnection.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttConnection.java new file mode 100644 index 0000000..bb17336 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttConnection.java @@ -0,0 +1,679 @@ +package com.sw.st.utils.mqtt.service; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; + +import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions; +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; +import org.eclipse.paho.client.mqttv3.MqttClientPersistence; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.MqttPersistenceException; +import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public class MqttConnection implements MqttCallbackExtended { + private static final String TAG = "MqttConnection"; + private static final String NOT_CONNECTED = "not connected"; + private String serverURI; + private String clientId; + private MqttClientPersistence persistence = null; + private MqttConnectOptions connectOptions; + private String clientHandle; + private String reconnectActivityToken = null; + private MqttAsyncClient myClient = null; + private AlarmPingSender alarmPingSender = null; + private MqttService service = null; + private volatile boolean disconnected = true; + private boolean cleanSession = true; + private volatile boolean isConnecting = false; + private Map savedTopics = new HashMap(); + private Map savedSentMessages = new HashMap(); + private Map savedActivityTokens = new HashMap(); + private Map savedInvocationContexts = new HashMap(); + private WakeLock wakelock = null; + private String wakeLockTag = null; + private DisconnectedBufferOptions bufferOpts = null; + + public String getServerURI() { + return this.serverURI; + } + + public void setServerURI(String serverURI) { + this.serverURI = serverURI; + } + + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public MqttConnectOptions getConnectOptions() { + return this.connectOptions; + } + + public void setConnectOptions(MqttConnectOptions connectOptions) { + this.connectOptions = connectOptions; + } + + public String getClientHandle() { + return this.clientHandle; + } + + public void setClientHandle(String clientHandle) { + this.clientHandle = clientHandle; + } + + MqttConnection(MqttService service, String serverURI, String clientId, MqttClientPersistence persistence, String clientHandle) { + this.serverURI = serverURI; + this.service = service; + this.clientId = clientId; + this.persistence = persistence; + this.clientHandle = clientHandle; + StringBuilder stringBuilder = new StringBuilder(this.getClass().getCanonicalName()); + stringBuilder.append(" "); + stringBuilder.append(clientId); + stringBuilder.append(" "); + stringBuilder.append("on host "); + stringBuilder.append(serverURI); + this.wakeLockTag = stringBuilder.toString(); + } + + public void connect(MqttConnectOptions options, String invocationContext, String activityToken) { + this.connectOptions = options; + this.reconnectActivityToken = activityToken; + if(options != null) { + this.cleanSession = options.isCleanSession(); + } + + if(this.connectOptions.isCleanSession()) { + this.service.messageStore.clearArrivedMessages(this.clientHandle); + } + + this.service.traceDebug(TAG, "Connecting {" + this.serverURI + "} as {" + this.clientId + "}"); + final Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, "connect"); + + try { + if(this.persistence == null) { + File myDir = this.service.getExternalFilesDir(TAG); + if(myDir == null) { + myDir = this.service.getDir(TAG, 0); + if(myDir == null) { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, "Error! No external and internal storage available"); + resultBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, new MqttPersistenceException()); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + return; + } + } + + this.persistence = new MqttDefaultFilePersistence(myDir.getAbsolutePath()); + } + + IMqttActionListener listener = new MqttConnectionListener(resultBundle) { + public void onSuccess(IMqttToken asyncActionToken) { + MqttConnection.this.doAfterConnectSuccess(resultBundle); + MqttConnection.this.service.traceDebug(TAG, "connect success!"); + } + + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, exception.getLocalizedMessage()); + resultBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, exception); + MqttConnection.this.service.traceError(TAG, "connect fail, call connect to reconnect.reason:" + exception.getMessage()); + MqttConnection.this.doAfterConnectFail(resultBundle); + } + }; + if(this.myClient != null) { + if(this.isConnecting) { + this.service.traceDebug(TAG, "myClient != null and the client is connecting. Connect return directly."); + this.service.traceDebug(TAG, "Connect return:isConnecting:" + this.isConnecting + ".disconnected:" + this.disconnected); + } else if(!this.disconnected) { + this.service.traceDebug(TAG, "myClient != null and the client is connected and notify!"); + this.doAfterConnectSuccess(resultBundle); + } else { + this.service.traceDebug(TAG, "myClient != null and the client is not connected"); + this.service.traceDebug(TAG, "Do Real connect!"); + this.setConnectingState(true); + this.myClient.connect(this.connectOptions, invocationContext, listener); + } + } else { + this.alarmPingSender = new AlarmPingSender(this.service); + this.myClient = new MqttAsyncClient(this.serverURI, this.clientId, this.persistence, this.alarmPingSender); + this.myClient.setCallback(this); + this.service.traceDebug(TAG, "Do Real connect!"); + this.setConnectingState(true); + this.myClient.connect(this.connectOptions, invocationContext, listener); + } + } catch (Exception var6) { + this.service.traceError(TAG, "Exception occurred attempting to connect: " + var6.getMessage()); + this.setConnectingState(false); + this.handleException(resultBundle, var6); + } + + } + + private void doAfterConnectSuccess(Bundle resultBundle) { + this.acquireWakeLock(); + this.service.callbackToActivity(this.clientHandle, Status.OK, resultBundle); + this.deliverBacklog(); + this.setConnectingState(false); + this.disconnected = false; + this.releaseWakeLock(); + } + + public void connectComplete(boolean reconnect, String serverURI) { + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.CONNECT_EXTENDED_ACTION); + resultBundle.putBoolean(MqttServiceConstants.CALLBACK_RECONNECT, reconnect); + resultBundle.putString(MqttServiceConstants.CALLBACK_SERVER_URI, serverURI); + this.service.callbackToActivity(this.clientHandle, Status.OK, resultBundle); + } + + private void doAfterConnectFail(Bundle resultBundle) { + this.acquireWakeLock(); + this.disconnected = true; + this.setConnectingState(false); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + this.releaseWakeLock(); + } + + private void handleException(Bundle resultBundle, Exception e) { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, e.getLocalizedMessage()); + resultBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, e); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + private void deliverBacklog() { + Iterator backlog = this.service.messageStore.getAllArrivedMessages(this.clientHandle); + + while(backlog.hasNext()) { + MessageStore.StoredMessage msgArrived = (MessageStore.StoredMessage)backlog.next(); + Bundle resultBundle = this.messageToBundle(msgArrived.getMessageId(), msgArrived.getTopic(), msgArrived.getMessage()); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.MESSAGE_ARRIVED_ACTION); + this.service.callbackToActivity(this.clientHandle, Status.OK, resultBundle); + } + + } + + private Bundle messageToBundle(String messageId, String topic, MqttMessage message) { + Bundle result = new Bundle(); + result.putString(MqttServiceConstants.CALLBACK_MESSAGE_ID, messageId); + result.putString(MqttServiceConstants.CALLBACK_DESTINATION_NAME, topic); + result.putParcelable(MqttServiceConstants.CALLBACK_MESSAGE_PARCEL, new ParcelableMqttMessage(message)); + return result; + } + + void close() { + this.service.traceDebug(TAG, "close()"); + + try { + if(this.myClient != null) { + this.myClient.close(); + } + } catch (MqttException var2) { + this.handleException(new Bundle(), var2); + } + + } + + void disconnect(long quiesceTimeout, String invocationContext, String activityToken) { + this.service.traceDebug(TAG, "disconnect()"); + this.disconnected = true; + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.DISCONNECT_ACTION); + if(this.myClient != null && this.myClient.isConnected()) { + MqttConnectionListener listener = new MqttConnectionListener(resultBundle, null); + + try { + this.myClient.disconnect(quiesceTimeout, invocationContext, listener); + } catch (Exception var8) { + this.handleException(resultBundle, var8); + } + } else { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.DISCONNECT_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + if(this.connectOptions != null && this.connectOptions.isCleanSession()) { + this.service.messageStore.clearArrivedMessages(this.clientHandle); + } + + this.releaseWakeLock(); + } + + void disconnect(String invocationContext, String activityToken) { + this.service.traceDebug(TAG, "disconnect()"); + this.disconnected = true; + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.DISCONNECT_ACTION); + if(this.myClient != null && this.myClient.isConnected()) { + MqttConnectionListener listener = new MqttConnectionListener(resultBundle, null); + + try { + this.myClient.disconnect(invocationContext, listener); + } catch (Exception var6) { + this.handleException(resultBundle, var6); + } + } else { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.DISCONNECT_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + if(this.connectOptions != null && this.connectOptions.isCleanSession()) { + this.service.messageStore.clearArrivedMessages(this.clientHandle); + } + + this.releaseWakeLock(); + } + + public boolean isConnected() { + return this.myClient != null && this.myClient.isConnected(); + } + + public IMqttDeliveryToken publish(String topic, byte[] payload, int qos, boolean retained, String invocationContext, String activityToken) { + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, "send"); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + IMqttDeliveryToken sendToken = null; + if(this.myClient != null && this.myClient.isConnected()) { + MqttConnectionListener listener = new MqttConnectionListener(resultBundle, null); + + try { + MqttMessage message = new MqttMessage(payload); + message.setQos(qos); + message.setRetained(retained); + sendToken = this.myClient.publish(topic, payload, qos, retained, invocationContext, listener); + this.storeSendDetails(topic, message, sendToken, invocationContext, activityToken); + } catch (Exception var11) { + this.handleException(resultBundle, var11); + } + } else { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.SEND_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + return sendToken; + } + + public IMqttDeliveryToken publish(String topic, MqttMessage message, String invocationContext, String activityToken) { + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.SEND_ACTION); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + IMqttDeliveryToken sendToken = null; + MqttConnectionListener listener; + if(this.myClient != null && this.myClient.isConnected()) { + listener = new MqttConnectionListener(resultBundle, null); + + try { + sendToken = this.myClient.publish(topic, message, invocationContext, listener); + this.storeSendDetails(topic, message, sendToken, invocationContext, activityToken); + } catch (Exception var10) { + this.handleException(resultBundle, var10); + } + } else if(this.myClient != null && this.bufferOpts != null && this.bufferOpts.isBufferEnabled()) { + listener = new MqttConnectionListener(resultBundle, null); + + try { + sendToken = this.myClient.publish(topic, message, invocationContext, listener); + this.storeSendDetails(topic, message, sendToken, invocationContext, activityToken); + } catch (Exception var9) { + this.handleException(resultBundle, var9); + } + } else { + Log.i("MqttConnection", "Client is not connected, so not sending message"); + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.SEND_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + return sendToken; + } + + public void subscribe(String topic, int qos, String invocationContext, String activityToken) { + this.service.traceDebug(TAG, "subscribe({" + topic + "}," + qos + ",{" + invocationContext + "}, {" + activityToken + "}"); + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.SUBSCRIBE_ACTION); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + if(this.myClient != null && this.myClient.isConnected()) { + MqttConnectionListener listener = new MqttConnectionListener(resultBundle, null); + + try { + this.myClient.subscribe(topic, qos, invocationContext, listener); + } catch (Exception var8) { + this.handleException(resultBundle, var8); + } + } else { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.SUBSCRIBE_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + } + + public void subscribe(String[] topic, int[] qos, String invocationContext, String activityToken) { + this.service.traceDebug(TAG, "subscribe({" + Arrays.toString(topic) + "}," + Arrays.toString(qos) + ",{" + invocationContext + "}, {" + activityToken + "}"); + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.SUBSCRIBE_ACTION); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + if(this.myClient != null && this.myClient.isConnected()) { + MqttConnectionListener listener = new MqttConnectionListener(resultBundle, null); + + try { + this.myClient.subscribe(topic, qos, invocationContext, listener); + } catch (Exception var8) { + this.handleException(resultBundle, var8); + } + } else { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE,NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.SUBSCRIBE_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + } + + public void subscribe(String[] topicFilters, int[] qos, String invocationContext, String activityToken, IMqttMessageListener[] messageListeners) { + this.service.traceDebug(TAG ,"subscribe({" + Arrays.toString(topicFilters) + "}," + Arrays.toString(qos) + ",{" + invocationContext + "}, {" + activityToken + "}"); + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, "subscribe"); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + if(this.myClient != null && this.myClient.isConnected()) { + new MqttConnectionListener(resultBundle, null); + + try { + this.myClient.subscribe(topicFilters, qos, messageListeners); + } catch (Exception var9) { + this.handleException(resultBundle, var9); + } + } else { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.SUBSCRIBE_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + } + + void unsubscribe(String topic, String invocationContext, String activityToken) { + this.service.traceDebug(TAG, "unsubscribe({" + topic + "},{" + invocationContext + "}, {" + activityToken + "})"); + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.UNSUBSCRIBE_ACTION); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + if(this.myClient != null && this.myClient.isConnected()) { + MqttConnectionListener listener = new MqttConnectionListener(resultBundle, null); + + try { + this.myClient.unsubscribe(topic, invocationContext, listener); + } catch (Exception var7) { + this.handleException(resultBundle, var7); + } + } else { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.SUBSCRIBE_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + } + + void unsubscribe(String[] topic, String invocationContext, String activityToken) { + this.service.traceDebug(TAG, "unsubscribe({" + Arrays.toString(topic) + "},{" + invocationContext + "}, {" + activityToken + "})"); + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.UNSUBSCRIBE_ACTION); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + if(this.myClient != null && this.myClient.isConnected()) { + MqttConnectionListener listener = new MqttConnectionListener(resultBundle, null); + + try { + this.myClient.unsubscribe(topic, invocationContext, listener); + } catch (Exception var7) { + this.handleException(resultBundle, var7); + } + } else { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, NOT_CONNECTED); + this.service.traceError(MqttServiceConstants.SUBSCRIBE_ACTION, NOT_CONNECTED); + this.service.callbackToActivity(this.clientHandle, Status.ERROR, resultBundle); + } + + } + + public IMqttDeliveryToken[] getPendingDeliveryTokens() { + return this.myClient.getPendingDeliveryTokens(); + } + + public void connectionLost(Throwable why) { + this.service.traceDebug(TAG, "connectionLost(" + why.getMessage() + ")"); + this.disconnected = true; + + try { + if(!this.connectOptions.isAutomaticReconnect()) { + this.myClient.disconnect((Object)null, new IMqttActionListener() { + public void onSuccess(IMqttToken asyncActionToken) { + } + + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + } + }); + } else { + this.alarmPingSender.schedule(100L); + } + } catch (Exception var3) { + ; + } + + Bundle resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.ON_CONNECTION_LOST_ACTION); + if(why != null) { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, why.getMessage()); + if(why instanceof MqttException) { + resultBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, why); + } + + resultBundle.putString(MqttServiceConstants.CALLBACK_EXCEPTION_STACK, Log.getStackTraceString(why)); + } + + this.service.callbackToActivity(this.clientHandle, Status.OK, resultBundle); + this.releaseWakeLock(); + } + + public void deliveryComplete(IMqttDeliveryToken messageToken) { + this.service.traceDebug(TAG, "deliveryComplete(" + messageToken + ")"); + MqttMessage message = (MqttMessage)this.savedSentMessages.remove(messageToken); + if(message != null) { + String topic = (String)this.savedTopics.remove(messageToken); + String activityToken = (String)this.savedActivityTokens.remove(messageToken); + String invocationContext = (String)this.savedInvocationContexts.remove(messageToken); + Bundle resultBundle = this.messageToBundle((String)null, topic, message); + if(activityToken != null) { + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.SEND_ACTION); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, activityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, invocationContext); + this.service.callbackToActivity(this.clientHandle, Status.OK, resultBundle); + } + + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.MESSAGE_DELIVERED_ACTION); + this.service.callbackToActivity(this.clientHandle, Status.OK, resultBundle); + } + + } + + public void messageArrived(String topic, MqttMessage message) throws Exception { + this.service.traceDebug(TAG, "messageArrived(" + topic + ",{" + message.toString() + "})"); + String messageId = this.service.messageStore.storeArrived(this.clientHandle, topic, message); + Bundle resultBundle = this.messageToBundle(messageId, topic, message); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.MESSAGE_ARRIVED_ACTION); + resultBundle.putString(MqttServiceConstants.CALLBACK_MESSAGE_ID, messageId); + this.service.callbackToActivity(this.clientHandle, Status.OK, resultBundle); + } + + private void storeSendDetails(String topic, MqttMessage msg, IMqttDeliveryToken messageToken, String invocationContext, String activityToken) { + this.savedTopics.put(messageToken, topic); + this.savedSentMessages.put(messageToken, msg); + this.savedActivityTokens.put(messageToken, activityToken); + this.savedInvocationContexts.put(messageToken, invocationContext); + } + @SuppressLint("WrongConstant") + private void acquireWakeLock() { + if(this.wakelock == null) { + PowerManager pm = (PowerManager)this.service.getSystemService("power"); + this.wakelock = pm.newWakeLock(1, this.wakeLockTag); + } + + this.wakelock.acquire(); + } + + private void releaseWakeLock() { + if(this.wakelock != null && this.wakelock.isHeld()) { + this.wakelock.release(); + } + + } + + void offline() { + if(!this.disconnected && !this.cleanSession) { + Exception e = new Exception("Android offline"); + this.connectionLost(e); + } + + } + + synchronized void reconnect() { + if(this.myClient == null) { + this.service.traceError(TAG, "Reconnect myClient = null. Will not do reconnect"); + } else if(this.isConnecting) { + this.service.traceDebug(TAG, "The client is connecting. Reconnect return directly."); + } else if(!this.service.isOnline()) { + this.service.traceDebug(TAG, "The network is not reachable. Will not do reconnect"); + } else { + final Bundle resultBundle; + if(this.connectOptions.isAutomaticReconnect()) { + Log.i(TAG, "Requesting Automatic reconnect using New Java AC"); + resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, this.reconnectActivityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, (String)null); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.CONNECT_ACTION); + + try { + this.myClient.reconnect(); + } catch (MqttException var6) { + Log.e(TAG, "Exception occurred attempting to reconnect: " + var6.getMessage()); + this.setConnectingState(false); + this.handleException(resultBundle, var6); + } + } else if(this.disconnected && !this.cleanSession) { + this.service.traceDebug(TAG, "Do Real Reconnect!"); + resultBundle = new Bundle(); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN, this.reconnectActivityToken); + resultBundle.putString(MqttServiceConstants.CALLBACK_INVOCATION_CONTEXT, (String)null); + resultBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.CONNECT_ACTION); + + try { + IMqttActionListener listener = new MqttConnectionListener(resultBundle) { + public void onSuccess(IMqttToken asyncActionToken) { + MqttConnection.this.service.traceDebug(TAG, "Reconnect Success!"); + MqttConnection.this.service.traceDebug(TAG, "DeliverBacklog when reconnect."); + MqttConnection.this.doAfterConnectSuccess(resultBundle); + } + + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, exception.getLocalizedMessage()); + resultBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, exception); + MqttConnection.this.service.callbackToActivity(MqttConnection.this.clientHandle, Status.ERROR, resultBundle); + MqttConnection.this.doAfterConnectFail(resultBundle); + } + }; + this.myClient.connect(this.connectOptions, (Object)null, listener); + this.setConnectingState(true); + } catch (MqttException var4) { + this.service.traceError(TAG, "Cannot reconnect to remote server." + var4.getMessage()); + this.setConnectingState(false); + this.handleException(resultBundle, var4); + } catch (Exception var5) { + this.service.traceError(TAG, "Cannot reconnect to remote server." + var5.getMessage()); + this.setConnectingState(false); + MqttException newEx = new MqttException(6, var5.getCause()); + this.handleException(resultBundle, newEx); + } + } + + } + } + + private synchronized void setConnectingState(boolean isConnecting) { + this.isConnecting = isConnecting; + } + + public void setBufferOpts(DisconnectedBufferOptions bufferOpts) { + this.bufferOpts = bufferOpts; + this.myClient.setBufferOpts(bufferOpts); + } + + public int getBufferedMessageCount() { + return this.myClient.getBufferedMessageCount(); + } + + public MqttMessage getBufferedMessage(int bufferIndex) { + return this.myClient.getBufferedMessage(bufferIndex); + } + + public void deleteBufferedMessage(int bufferIndex) { + this.myClient.deleteBufferedMessage(bufferIndex); + } + + private class MqttConnectionListener implements IMqttActionListener { + private final Bundle resultBundle; + + private MqttConnectionListener(Bundle resultBundle) { + this.resultBundle = resultBundle; + } + + public MqttConnectionListener(Bundle resultBundle, Object o) { + this.resultBundle = resultBundle; + } + + public void onSuccess(IMqttToken asyncActionToken) { + MqttConnection.this.service.callbackToActivity(MqttConnection.this.clientHandle, Status.OK, this.resultBundle); + } + + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + this.resultBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, exception.getLocalizedMessage()); + this.resultBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, exception); + MqttConnection.this.service.callbackToActivity(MqttConnection.this.clientHandle, Status.ERROR, this.resultBundle); + } + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttDeliveryTokenAndroid.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttDeliveryTokenAndroid.java new file mode 100644 index 0000000..a5c0b46 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttDeliveryTokenAndroid.java @@ -0,0 +1,33 @@ +package com.sw.st.utils.mqtt.service; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; + + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public class MqttDeliveryTokenAndroid extends MqttTokenAndroid implements IMqttDeliveryToken { + private MqttMessage message; + + MqttDeliveryTokenAndroid(MqttAndroidClient client, Object userContext, IMqttActionListener listener, MqttMessage message) { + super(client, userContext, listener); + this.message = message; + } + + public MqttMessage getMessage() throws MqttException { + return this.message; + } + + void setMessage(MqttMessage message) { + this.message = message; + } + + void notifyDelivery(MqttMessage delivered) { + this.message = delivered; + super.notifyComplete(); + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttService.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttService.java new file mode 100644 index 0000000..651ef06 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttService.java @@ -0,0 +1,364 @@ +package com.sw.st.utils.mqtt.service; + +import android.annotation.SuppressLint; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build.VERSION; +import android.os.Bundle; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.MqttClientPersistence; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.MqttPersistenceException; +import org.eclipse.paho.client.mqttv3.MqttSecurityException; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Created by ZhangHs on 2018/4/20. + */ +public class MqttService extends Service implements MqttTraceHandler { + static final String TAG = "MqttService"; + private String traceCallbackId; + private boolean traceEnabled = false; + MessageStore messageStore; + private NetworkConnectionIntentReceiver networkConnectionMonitor; + private BackgroundDataPreferenceReceiver backgroundDataPreferenceMonitor; + private volatile boolean backgroundDataEnabled = true; + private MqttServiceBinder mqttServiceBinder; + private Map connections = new ConcurrentHashMap(); + + public MqttService() { + } + + void callbackToActivity(String clientHandle, Status status, Bundle dataBundle) { + Intent callbackIntent = new Intent(MqttServiceConstants.CALLBACK_TO_ACTIVITY); + if (clientHandle != null) { + callbackIntent.putExtra(MqttServiceConstants.CALLBACK_CLIENT_HANDLE, clientHandle); + } + + callbackIntent.putExtra(MqttServiceConstants.CALLBACK_STATUS, status); + if (dataBundle != null) { + callbackIntent.putExtras(dataBundle); + } + + LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent); + } + + public String getClient(String serverURI, String clientId, String contextId, MqttClientPersistence persistence) { + String clientHandle = serverURI + ":" + clientId + ":" + contextId; + if (!this.connections.containsKey(clientHandle)) { + MqttConnection client = new MqttConnection(this, serverURI, clientId, persistence, clientHandle); + this.connections.put(clientHandle, client); + } + + return clientHandle; + } + + public void connect(String clientHandle, MqttConnectOptions connectOptions, String invocationContext, String activityToken) throws MqttSecurityException, MqttException { + MqttConnection client = this.getConnection(clientHandle); + client.connect(connectOptions, (String) null, activityToken); + } + + void reconnect() { + this.traceDebug(TAG, "Reconnect to server, client size=" + this.connections.size()); + Iterator var1 = this.connections.values().iterator(); + + while (var1.hasNext()) { + MqttConnection client = (MqttConnection) var1.next(); + this.traceDebug("Reconnect Client:", client.getClientId() + '/' + client.getServerURI()); + if (this.isOnline()) { + client.reconnect(); + } + } + + } + + public void close(String clientHandle) { + MqttConnection client = this.getConnection(clientHandle); + client.close(); + } + + public void disconnect(String clientHandle, String invocationContext, String activityToken) { + MqttConnection client = this.getConnection(clientHandle); + client.disconnect(invocationContext, activityToken); + this.connections.remove(clientHandle); + this.stopSelf(); + } + + public void disconnect(String clientHandle, long quiesceTimeout, String invocationContext, String activityToken) { + MqttConnection client = this.getConnection(clientHandle); + client.disconnect(quiesceTimeout, invocationContext, activityToken); + this.connections.remove(clientHandle); + this.stopSelf(); + } + + public boolean isConnected(String clientHandle) { + MqttConnection client = this.getConnection(clientHandle); + return client.isConnected(); + } + + public IMqttDeliveryToken publish(String clientHandle, String topic, byte[] payload, int qos, boolean retained, String invocationContext, String activityToken) throws MqttPersistenceException, MqttException { + MqttConnection client = this.getConnection(clientHandle); + return client.publish(topic, payload, qos, retained, invocationContext, activityToken); + } + + public IMqttDeliveryToken publish(String clientHandle, String topic, MqttMessage message, String invocationContext, String activityToken) throws MqttPersistenceException, MqttException { + MqttConnection client = this.getConnection(clientHandle); + return client.publish(topic, message, invocationContext, activityToken); + } + + public void subscribe(String clientHandle, String topic, int qos, String invocationContext, String activityToken) { + MqttConnection client = this.getConnection(clientHandle); + client.subscribe(topic, qos, invocationContext, activityToken); + } + + public void subscribe(String clientHandle, String[] topic, int[] qos, String invocationContext, String activityToken) { + MqttConnection client = this.getConnection(clientHandle); + client.subscribe(topic, qos, invocationContext, activityToken); + } + + public void subscribe(String clientHandle, String[] topicFilters, int[] qos, String invocationContext, String activityToken, IMqttMessageListener[] messageListeners) { + MqttConnection client = this.getConnection(clientHandle); + client.subscribe(topicFilters, qos, invocationContext, activityToken, messageListeners); + } + + public void unsubscribe(String clientHandle, String topic, String invocationContext, String activityToken) { + MqttConnection client = this.getConnection(clientHandle); + client.unsubscribe(topic, invocationContext, activityToken); + } + + public void unsubscribe(String clientHandle, String[] topic, String invocationContext, String activityToken) { + MqttConnection client = this.getConnection(clientHandle); + client.unsubscribe(topic, invocationContext, activityToken); + } + + public IMqttDeliveryToken[] getPendingDeliveryTokens(String clientHandle) { + MqttConnection client = this.getConnection(clientHandle); + return client.getPendingDeliveryTokens(); + } + + private MqttConnection getConnection(String clientHandle) { + MqttConnection client = (MqttConnection) this.connections.get(clientHandle); + if (client == null) { + throw new IllegalArgumentException("Invalid ClientHandle"); + } else { + return client; + } + } + + public Status acknowledgeMessageArrival(String clientHandle, String id) { + return this.messageStore.discardArrived(clientHandle, id) ? Status.OK : Status.ERROR; + } + + public void onCreate() { + super.onCreate(); + this.mqttServiceBinder = new MqttServiceBinder(this); + this.messageStore = new DatabaseMessageStore(this, this); + } + + public void onDestroy() { + Iterator var1 = this.connections.values().iterator(); + + while (var1.hasNext()) { + MqttConnection client = (MqttConnection) var1.next(); + client.disconnect((String) null, (String) null); + } + + if (this.mqttServiceBinder != null) { + this.mqttServiceBinder = null; + } + + this.unregisterBroadcastReceivers(); + if (this.messageStore != null) { + this.messageStore.close(); + } + + super.onDestroy(); + } + + public IBinder onBind(Intent intent) { + String activityToken = intent.getStringExtra(MqttServiceConstants.CALLBACK_ACTIVITY_TOKEN); + this.mqttServiceBinder.setActivityToken(activityToken); + return this.mqttServiceBinder; + } + + + @SuppressLint("WrongConstant") + public int onStartCommand(Intent intent, int flags, int startId) { + this.registerBroadcastReceivers(); + return 1; + } + + public void setTraceCallbackId(String traceCallbackId) { + this.traceCallbackId = traceCallbackId; + } + + public void setTraceEnabled(boolean traceEnabled) { + this.traceEnabled = traceEnabled; + } + + public boolean isTraceEnabled() { + return this.traceEnabled; + } + + public void traceDebug(String tag, String message) { + this.traceCallback(MqttServiceConstants.TRACE_DEBUG, tag, message); + } + + public void traceError(String tag, String message) { + this.traceCallback(MqttServiceConstants.TRACE_ERROR, tag, message); + } + + private void traceCallback(String severity, String tag, String message) { + if (this.traceCallbackId != null && this.traceEnabled) { + Bundle dataBundle = new Bundle(); + dataBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.TRACE_ACTION); + dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_SEVERITY, severity); + dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_TAG, tag); + dataBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, message); + this.callbackToActivity(this.traceCallbackId, Status.ERROR, dataBundle); + } + + } + + public void traceException(String tag, String message, Exception e) { + if (this.traceCallbackId != null) { + Bundle dataBundle = new Bundle(); + dataBundle.putString(MqttServiceConstants.CALLBACK_ACTION, MqttServiceConstants.TRACE_ACTION); + dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_SEVERITY, MqttServiceConstants.TRACE_EXCEPTION); + dataBundle.putString(MqttServiceConstants.CALLBACK_ERROR_MESSAGE, message); + dataBundle.putSerializable(MqttServiceConstants.CALLBACK_EXCEPTION, e); + dataBundle.putString(MqttServiceConstants.CALLBACK_TRACE_TAG, tag); + this.callbackToActivity(this.traceCallbackId, Status.ERROR, dataBundle); + } + + } + + @SuppressLint("WrongConstant") + private void registerBroadcastReceivers() { + if (this.networkConnectionMonitor == null) { + this.networkConnectionMonitor = new NetworkConnectionIntentReceiver(); + this.registerReceiver(this.networkConnectionMonitor, new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")); + } + + if (VERSION.SDK_INT < 14) { + ConnectivityManager cm = (ConnectivityManager) this.getSystemService("connectivity"); + this.backgroundDataEnabled = cm.getBackgroundDataSetting(); + if (this.backgroundDataPreferenceMonitor == null) { + this.backgroundDataPreferenceMonitor = new BackgroundDataPreferenceReceiver(); + this.registerReceiver(this.backgroundDataPreferenceMonitor, new IntentFilter("android.net.conn.BACKGROUND_DATA_SETTING_CHANGED")); + } + } + + } + + private void unregisterBroadcastReceivers() { + if (this.networkConnectionMonitor != null) { + this.unregisterReceiver(this.networkConnectionMonitor); + this.networkConnectionMonitor = null; + } + + if (VERSION.SDK_INT < 14 && this.backgroundDataPreferenceMonitor != null) { + this.unregisterReceiver(this.backgroundDataPreferenceMonitor); + } + + } + + @SuppressLint("WrongConstant") + public boolean isOnline() { + ConnectivityManager cm = (ConnectivityManager) this.getSystemService("connectivity"); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isAvailable() && networkInfo.isConnected() && this.backgroundDataEnabled; + } + + private void notifyClientsOffline() { + Iterator var1 = this.connections.values().iterator(); + + while (var1.hasNext()) { + MqttConnection connection = (MqttConnection) var1.next(); + connection.offline(); + } + + } + + public void setBufferOpts(String clientHandle, DisconnectedBufferOptions bufferOpts) { + MqttConnection client = this.getConnection(clientHandle); + client.setBufferOpts(bufferOpts); + } + + public int getBufferedMessageCount(String clientHandle) { + MqttConnection client = this.getConnection(clientHandle); + return client.getBufferedMessageCount(); + } + + public MqttMessage getBufferedMessage(String clientHandle, int bufferIndex) { + MqttConnection client = this.getConnection(clientHandle); + return client.getBufferedMessage(bufferIndex); + } + + public void deleteBufferedMessage(String clientHandle, int bufferIndex) { + MqttConnection client = this.getConnection(clientHandle); + client.deleteBufferedMessage(bufferIndex); + } + + @SuppressLint("WrongConstant") + private class BackgroundDataPreferenceReceiver extends BroadcastReceiver { + private BackgroundDataPreferenceReceiver() { + } + + public void onReceive(Context context, Intent intent) { + ConnectivityManager cm = (ConnectivityManager) MqttService.this.getSystemService("connectivity"); + MqttService.this.traceDebug(TAG, "Reconnect since BroadcastReceiver."); + if (cm.getBackgroundDataSetting()) { + if (!MqttService.this.backgroundDataEnabled) { + MqttService.this.backgroundDataEnabled = true; + MqttService.this.reconnect(); + } + } else { + MqttService.this.backgroundDataEnabled = false; + MqttService.this.notifyClientsOffline(); + } + + } + } + + @SuppressLint({"WrongConstant", "InvalidWakeLockTag"}) + private class NetworkConnectionIntentReceiver extends BroadcastReceiver { + private NetworkConnectionIntentReceiver() { + } + + public void onReceive(Context context, Intent intent) { + MqttService.this.traceDebug(TAG, "Internal network status receive."); + PowerManager pm = (PowerManager) MqttService.this.getSystemService("power"); + WakeLock wl = pm.newWakeLock(1, "MQTT"); + wl.acquire(); + MqttService.this.traceDebug(TAG, "Reconnect for Network recovery."); + if (MqttService.this.isOnline()) { + MqttService.this.traceDebug(TAG, "Online,reconnect."); + MqttService.this.reconnect(); + } else { + MqttService.this.notifyClientsOffline(); + } + + wl.release(); + } + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttServiceBinder.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttServiceBinder.java new file mode 100644 index 0000000..ee32f33 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttServiceBinder.java @@ -0,0 +1,29 @@ +package com.sw.st.utils.mqtt.service; + +import android.os.Binder; + + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public class MqttServiceBinder extends Binder { + private MqttService mqttService; + private String activityToken; + + MqttServiceBinder(MqttService mqttService) { + this.mqttService = mqttService; + } + + public MqttService getService() { + return this.mqttService; + } + + void setActivityToken(String activityToken) { + this.activityToken = activityToken; + } + + public String getActivityToken() { + return this.activityToken; + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttServiceConstants.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttServiceConstants.java new file mode 100644 index 0000000..769451f --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttServiceConstants.java @@ -0,0 +1,51 @@ +package com.sw.st.utils.mqtt.service; + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public interface MqttServiceConstants { + String VERSION = "v0"; + String DUPLICATE = "duplicate"; + String RETAINED = "retained"; + String QOS = "qos"; + String PAYLOAD = "payload"; + String DESTINATION_NAME = "destinationName"; + String CLIENT_HANDLE = "clientHandle"; + String MESSAGE_ID = "messageId"; + String SEND_ACTION = "send"; + String UNSUBSCRIBE_ACTION = "unsubscribe"; + String SUBSCRIBE_ACTION = "subscribe"; + String DISCONNECT_ACTION = "disconnect"; + String CONNECT_ACTION = "connect"; + String CONNECT_EXTENDED_ACTION = "connectExtended"; + String MESSAGE_ARRIVED_ACTION = "messageArrived"; + String MESSAGE_DELIVERED_ACTION = "messageDelivered"; + String ON_CONNECTION_LOST_ACTION = "onConnectionLost"; + String TRACE_ACTION = "trace"; + String CALLBACK_TO_ACTIVITY = "MqttService.callbackToActivity.v0"; + String CALLBACK_ACTION = "MqttService.callbackAction"; + String CALLBACK_STATUS = "MqttService.callbackStatus"; + String CALLBACK_CLIENT_HANDLE = "MqttService.clientHandle"; + String CALLBACK_ERROR_MESSAGE = "MqttService.errorMessage"; + String CALLBACK_EXCEPTION_STACK = "MqttService.exceptionStack"; + String CALLBACK_INVOCATION_CONTEXT = "MqttService.invocationContext"; + String CALLBACK_ACTIVITY_TOKEN = "MqttService.activityToken"; + String CALLBACK_DESTINATION_NAME = "MqttService.destinationName"; + String CALLBACK_MESSAGE_ID = "MqttService.messageId"; + String CALLBACK_RECONNECT = "MqttService.reconnect"; + String CALLBACK_SERVER_URI = "MqttService.serverURI"; + String CALLBACK_MESSAGE_PARCEL = "MqttService.PARCEL"; + String CALLBACK_TRACE_SEVERITY = "MqttService.traceSeverity"; + String CALLBACK_TRACE_TAG = "MqttService.traceTag"; + String CALLBACK_TRACE_ID = "MqttService.traceId"; + String CALLBACK_ERROR_NUMBER = "MqttService.ERROR_NUMBER"; + String CALLBACK_EXCEPTION = "MqttService.exception"; + String PING_SENDER = "MqttService.pingSender."; + String PING_WAKELOCK = "MqttService.client."; + String WAKELOCK_NETWORK_INTENT = "MqttService"; + String TRACE_ERROR = "error"; + String TRACE_DEBUG = "debug"; + String TRACE_EXCEPTION = "exception"; + int NON_MQTT_EXCEPTION = -1; +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttTokenAndroid.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttTokenAndroid.java new file mode 100644 index 0000000..806949c --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttTokenAndroid.java @@ -0,0 +1,163 @@ +package com.sw.st.utils.mqtt.service; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttSecurityException; +import org.eclipse.paho.client.mqttv3.internal.wire.MqttWireMessage; + + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public class MqttTokenAndroid implements IMqttToken { + private IMqttActionListener listener; + private volatile boolean isComplete; + private volatile MqttException lastException; + private Object waitObject; + private MqttAndroidClient client; + private Object userContext; + private String[] topics; + private IMqttToken delegate; + private MqttException pendingException; + + MqttTokenAndroid(MqttAndroidClient client, Object userContext, IMqttActionListener listener) { + this(client, userContext, listener, (String[])null); + } + + MqttTokenAndroid(MqttAndroidClient client, Object userContext, IMqttActionListener listener, String[] topics) { + this.waitObject = new Object(); + this.client = client; + this.userContext = userContext; + this.listener = listener; + this.topics = topics; + } + + public void waitForCompletion() throws MqttException, MqttSecurityException { + Object var1 = this.waitObject; + synchronized(this.waitObject) { + try { + this.waitObject.wait(); + } catch (InterruptedException var4) { + ; + } + } + + if(this.pendingException != null) { + throw this.pendingException; + } + } + + public void waitForCompletion(long timeout) throws MqttException, MqttSecurityException { + Object var3 = this.waitObject; + synchronized(this.waitObject) { + try { + this.waitObject.wait(timeout); + } catch (InterruptedException var6) { + ; + } + + if(!this.isComplete) { + throw new MqttException(32000); + } else if(this.pendingException != null) { + throw this.pendingException; + } + } + } + + void notifyComplete() { + Object var1 = this.waitObject; + synchronized(this.waitObject) { + this.isComplete = true; + this.waitObject.notifyAll(); + if(this.listener != null) { + this.listener.onSuccess(this); + } + + } + } + + void notifyFailure(Throwable exception) { + Object var2 = this.waitObject; + synchronized(this.waitObject) { + this.isComplete = true; + if(exception instanceof MqttException) { + this.pendingException = (MqttException)exception; + } else { + this.pendingException = new MqttException(exception); + } + + this.waitObject.notifyAll(); + if(exception instanceof MqttException) { + this.lastException = (MqttException)exception; + } + + if(this.listener != null) { + this.listener.onFailure(this, exception); + } + + } + } + + public boolean isComplete() { + return this.isComplete; + } + + void setComplete(boolean complete) { + this.isComplete = complete; + } + + public MqttException getException() { + return this.lastException; + } + + void setException(MqttException exception) { + this.lastException = exception; + } + + public IMqttAsyncClient getClient() { + return this.client; + } + + public void setActionCallback(IMqttActionListener listener) { + this.listener = listener; + } + + public IMqttActionListener getActionCallback() { + return this.listener; + } + + public String[] getTopics() { + return this.topics; + } + + public void setUserContext(Object userContext) { + this.userContext = userContext; + } + + public Object getUserContext() { + return this.userContext; + } + + void setDelegate(IMqttToken delegate) { + this.delegate = delegate; + } + + public int getMessageId() { + return this.delegate != null?this.delegate.getMessageId():0; + } + + public MqttWireMessage getResponse() { + return this.delegate.getResponse(); + } + + public boolean getSessionPresent() { + return this.delegate.getSessionPresent(); + } + + public int[] getGrantedQos() { + return this.delegate.getGrantedQos(); + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/MqttTraceHandler.java b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttTraceHandler.java new file mode 100644 index 0000000..2fff230 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/MqttTraceHandler.java @@ -0,0 +1,13 @@ +package com.sw.st.utils.mqtt.service; + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public interface MqttTraceHandler { + void traceDebug(String var1, String var2); + + void traceError(String var1, String var2); + + void traceException(String var1, String var2, Exception var3); +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/ParcelableMqttMessage.java b/app/src/main/java/com/sw/st/utils/mqtt/service/ParcelableMqttMessage.java new file mode 100644 index 0000000..75254dc --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/ParcelableMqttMessage.java @@ -0,0 +1,54 @@ +package com.sw.st.utils.mqtt.service; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * Created by ZhangHs on 2018/4/20. + */ + +public class ParcelableMqttMessage extends MqttMessage implements Parcelable { + String messageId = null; + public static final Creator CREATOR = new Creator() { + public ParcelableMqttMessage createFromParcel(Parcel parcel) { + return new ParcelableMqttMessage(parcel); + } + + public ParcelableMqttMessage[] newArray(int size) { + return new ParcelableMqttMessage[size]; + } + }; + + ParcelableMqttMessage(MqttMessage original) { + super(original.getPayload()); + this.setQos(original.getQos()); + this.setRetained(original.isRetained()); + this.setDuplicate(original.isDuplicate()); + } + + ParcelableMqttMessage(Parcel parcel) { + super(parcel.createByteArray()); + this.setQos(parcel.readInt()); + boolean[] flags = parcel.createBooleanArray(); + this.setRetained(flags[0]); + this.setDuplicate(flags[1]); + this.messageId = parcel.readString(); + } + + public String getMessageId() { + return this.messageId; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeByteArray(this.getPayload()); + parcel.writeInt(this.getQos()); + parcel.writeBooleanArray(new boolean[]{this.isRetained(), this.isDuplicate()}); + parcel.writeString(this.messageId); + } +} diff --git a/app/src/main/java/com/sw/st/utils/mqtt/service/Status.java b/app/src/main/java/com/sw/st/utils/mqtt/service/Status.java new file mode 100644 index 0000000..c134603 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/mqtt/service/Status.java @@ -0,0 +1,14 @@ +package com.sw.st.utils.mqtt.service; + +/** + * Created by ZhangHs on 2018/4/20. + */ + +enum Status { + OK, + ERROR, + NO_RESULT; + + private Status() { + } +} diff --git a/app/src/main/java/com/sw/st/utils/update/AppUpdater.java b/app/src/main/java/com/sw/st/utils/update/AppUpdater.java new file mode 100644 index 0000000..607324b --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/update/AppUpdater.java @@ -0,0 +1,30 @@ +package com.sw.st.utils.update; + + + +public class AppUpdater { + + private static AppUpdater sInstance = new AppUpdater(); + + public static AppUpdater getInstance() { + return sInstance; + } + + /** + * 默认的网络访问方式:OkHttpNetManager + */ + private static INetManager sINetManager = new OkHttpNetManager(); + + public INetManager getINetManager() { + return sINetManager; + } + + /** + * 指定网络访问方式 + * + * @param netManager + */ + public void setINetManager(INetManager netManager) { + sINetManager = netManager; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/update/AppVersionInfoBean.java b/app/src/main/java/com/sw/st/utils/update/AppVersionInfoBean.java new file mode 100644 index 0000000..0bf18ad --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/update/AppVersionInfoBean.java @@ -0,0 +1,73 @@ +package com.sw.st.utils.update; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.Serializable; + +public class AppVersionInfoBean implements Serializable { + + private String title; + private String content; + private String url; + private String md5; + private String versionCode; + private int type; + + public AppVersionInfoBean(String title, String content, String url, String md5, String versionCode, int type) { + this.title = title; + this.content = content; + this.url = url; + this.md5 = md5; + this.versionCode = versionCode; + this.type = type; + } + + /** + * 把response转换为AppVersionInfoBean。 + * + * @param response + * @return + */ + public static AppVersionInfoBean parse(String response) { + try { + JSONObject responseJson = new JSONObject(response); + String title = "更新提示"; + String content = responseJson.optString("upgradePoint"); + String url = responseJson.optString("apkUrl"); + String md5 = ""; + String versionCode = responseJson.optString("versionId"); + int type = responseJson.optInt("type"); + + return new AppVersionInfoBean(title, content, url, md5, versionCode, type); + } catch (JSONException e) { + e.printStackTrace(); + } + + return null; + } + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public String getUrl() { + return url; + } + + public String getMd5() { + return md5; + } + + public String getVersionCode() { + return versionCode; + } + + public int getType() { + return type; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/update/IDownloadCallback.java b/app/src/main/java/com/sw/st/utils/update/IDownloadCallback.java new file mode 100644 index 0000000..56967a0 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/update/IDownloadCallback.java @@ -0,0 +1,23 @@ +package com.sw.st.utils.update; + +import java.io.File; + +public interface IDownloadCallback { + /** + * 下载成功,在此处理 + * @param apkFile + */ + void onSuccess(File apkFile); + + /** + * 下载进度,在此处理 + * @param progress + */ + void progress(int progress); + + /** + * 下载失败,在此处理 + * @param throwable + */ + void onFailure(Throwable throwable); +} diff --git a/app/src/main/java/com/sw/st/utils/update/INetCallback.java b/app/src/main/java/com/sw/st/utils/update/INetCallback.java new file mode 100644 index 0000000..77fdcc1 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/update/INetCallback.java @@ -0,0 +1,15 @@ +package com.sw.st.utils.update; + +public interface INetCallback { + /** + * 请求成功,再此进行处理 + * @param response + */ + void onSuccess(String response); + + /** + * 请求失败,在此进行处理 + * @param throwable + */ + void onFailed(Throwable throwable); +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/update/INetManager.java b/app/src/main/java/com/sw/st/utils/update/INetManager.java new file mode 100644 index 0000000..0f89be5 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/update/INetManager.java @@ -0,0 +1,32 @@ +package com.sw.st.utils.update; + +import java.io.File; + +public interface INetManager { + + /** + * 发起请求 + * + * @param url 地址 + * @param netCallback 处理返回的结果 + * @param tag 标识当前的请求 + */ + void get(String url, INetCallback netCallback, Object tag); + + /** + * 下载 + * + * @param url 资源地址 + * @param targetFile 保存到:targetFile + * @param downloadCallback 下载结果回调 + * @param tag 标识当前的下载请求 + */ + void download(String url, File targetFile, IDownloadCallback downloadCallback, Object tag); + + /** + * 取消数据请求 + * + * @param tag 标识要取消的请求 + */ + void cancel(Object tag); +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/update/OkHttpNetManager.java b/app/src/main/java/com/sw/st/utils/update/OkHttpNetManager.java new file mode 100644 index 0000000..6bce7eb --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/update/OkHttpNetManager.java @@ -0,0 +1,183 @@ +package com.sw.st.utils.update; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class OkHttpNetManager implements INetManager { + private static final String TAG = "OkHttpNetManager"; + + private static OkHttpClient sOkHttpClient; + + private static Handler sHandler = new Handler(Looper.getMainLooper()); + + static { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + builder.connectTimeout(15, TimeUnit.SECONDS); + sOkHttpClient = builder.build(); + } + + @Override + public void get(String url, final INetCallback netCallback, Object tag) { + Request.Builder builder = new Request.Builder(); + Request request = builder.url(url).get().tag(tag).build(); + + Call call = sOkHttpClient.newCall(request); + + call.enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull final IOException e) { + sHandler.post(new Runnable() { + @Override + public void run() { + netCallback.onFailed(e); + } + }); + } + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try { + final String string = response.body().string(); + sHandler.post(new Runnable() { + @Override + public void run() { + netCallback.onSuccess(string); + } + }); + } catch (final IOException e) { + e.printStackTrace(); + sHandler.post(new Runnable() { + @Override + public void run() { + netCallback.onFailed(e); + } + }); + } + } + }); + } + + @Override + public void download(String url, final File targetFile, final IDownloadCallback downloadCallback, Object tag) { + if (!targetFile.exists()) { + targetFile.getParentFile().mkdirs(); + } + + //发起请求 + Request.Builder builder = new Request.Builder(); + final Request request = builder.url(url).get().tag(tag).build(); + Call call = sOkHttpClient.newCall(request); + call.enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull final IOException e) { + sHandler.post(new Runnable() { + @Override + public void run() { + downloadCallback.onFailure(e); + } + }); + } + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + InputStream is = null; + OutputStream os = null; + + try { + final long totalLen = response.body().contentLength(); + + is = response.body().byteStream(); + os = new FileOutputStream(targetFile); + + byte[] buffer = new byte[8 * 1024]; + int bufferLen; + int curLen = 0; + while (!call.isCanceled() && (bufferLen = is.read(buffer)) != -1) { + os.write(buffer, 0, bufferLen); + os.flush(); + curLen += bufferLen; + + final int finalCurLen = curLen; + sHandler.post(new Runnable() { + @Override + public void run() { + downloadCallback.progress((int) (finalCurLen * 1.0f / totalLen * 100)); + } + }); + } + + if (call.isCanceled()){ + return; + } + + sHandler.post(new Runnable() { + @Override + public void run() { + downloadCallback.onSuccess(targetFile); + } + }); + + } catch (final FileNotFoundException e) { + if (call.isCanceled()){ + return; + } + e.printStackTrace(); + sHandler.post(new Runnable() { + @Override + public void run() { + downloadCallback.onFailure(e); + } + }); + } finally { + if (is != null) { + is.close(); + } + if (os != null) { + os.close(); + } + } + } + }); + } + + @Override + public void cancel(Object tag) { + List queuedCalls = sOkHttpClient.dispatcher().queuedCalls(); + if (queuedCalls != null) { + for (Call call : queuedCalls) { + if (tag.equals(call.request().tag())) { + Log.d("cancel", "find call = " + tag); + call.cancel(); + } + } + } + + List runningCalls = sOkHttpClient.dispatcher().runningCalls(); + if (runningCalls != null) { + for (Call call : runningCalls) { + if (tag.equals(call.request().tag())) { + Log.d("cancel", "find call = " + tag); + call.cancel(); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/update/UpdateVersionShowDialog.java b/app/src/main/java/com/sw/st/utils/update/UpdateVersionShowDialog.java new file mode 100644 index 0000000..1507d75 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/update/UpdateVersionShowDialog.java @@ -0,0 +1,181 @@ +package com.sw.st.utils.update; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; + +import com.blankj.utilcode.util.GsonUtils; +import com.blankj.utilcode.util.Utils; +import com.sw.st.R; +import com.sw.st.application.App; +import com.sw.st.utils.AppUtil; +import com.sw.st.utils.L; +import com.sw.st.utils.SystemCtrlUtil; + +import java.io.File; + +public class UpdateVersionShowDialog extends DialogFragment { + private static final String TAG = "UpdateVersionShowDialog"; + + private static final String KEY_APP_VERSION_INFO_BEAN = "app_version_info_bean"; + + /** + * 版本更新信息bean,由show方法传入 + */ + private AppVersionInfoBean appVersionInfoBean; + + private Context mContext; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = this.getActivity(); + Bundle arguments = getArguments(); + if (arguments != null) { + appVersionInfoBean = (AppVersionInfoBean) arguments.getSerializable(KEY_APP_VERSION_INFO_BEAN); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.dialog_update_app, container, false); + bindView(view); + + getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); + getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + + getDialog().setCanceledOnTouchOutside(false); + return view; + } + + private void bindView(View view) { + TextView tvTitle = view.findViewById(R.id.tv_title); + TextView tvContent = view.findViewById(R.id.tv_content); + TextView tvUpdate = view.findViewById(R.id.tv_update); + LinearLayout progressLayout = view.findViewById(R.id.progressLayout); + TextView updateProgress = view.findViewById(R.id.update_progress); + View bottomLayoutDiv = view.findViewById(R.id.bottomLayoutDiv); + LinearLayout bottomLayout = view.findViewById(R.id.bottomLayout); + TextView tvCancel = view.findViewById(R.id.tv_cancel); + View backgroundLayoutDiv = view.findViewById(R.id.backgroundLayoutDiv); + LinearLayout backgroundLayout = view.findViewById(R.id.backgroundLayout); + + String title = ""; + String content = ""; + if (appVersionInfoBean != null) { + title = appVersionInfoBean.getTitle(); + content = appVersionInfoBean.getContent(); + if (appVersionInfoBean.getType() == 2) { + tvCancel.setText("退出"); + } + } + tvTitle.setText(AppUtil.isEmpty(title) ? "标题" : title); + tvContent.setText(AppUtil.isEmpty(content) ? "内容" : content); + + tvCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (tvCancel.getText().toString().equals("退出")) { + getActivity().finish(); + } else { + dismiss(); + } + } + }); + view.findViewById(R.id.backgroundUpdate).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + tvUpdate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + v.setEnabled(false); + tvContent.setVisibility(View.GONE); + bottomLayoutDiv.setVisibility(View.GONE); + bottomLayout.setVisibility(View.GONE); + progressLayout.setVisibility(View.VISIBLE); +// backgroundLayoutDiv.setVisibility(View.VISIBLE); +// backgroundLayout.setVisibility(View.VISIBLE); + //安装包的下载地址,选择getCacheDir路径,可以避免存储权限的处理 +// final File targetFile = new File(getActivity().getCacheDir(), "target.apk"); + String filename = appVersionInfoBean.getUrl().substring((appVersionInfoBean.getUrl().lastIndexOf('/') + 1)); + + final File targetFile = new File(getActivity().getExternalCacheDir().getAbsolutePath(), filename); + AppUpdater.getInstance().getINetManager().download(appVersionInfoBean.getUrl(), targetFile, new IDownloadCallback() { + @Override + public void onSuccess(File apkFile) { + v.setEnabled(true); + + dismiss(); + + //下载成功 + Log.d(TAG, "success = " + apkFile.getAbsolutePath()); + +// AppUtil.installApk(mContext, apkFile); + + AppUtil.clientInstall(apkFile.getAbsolutePath()); + + } + + @Override + public void progress(int progress) { + Log.d(TAG, "progress = " + progress); + + updateProgress.setText(progress + "%"); + } + + @Override + public void onFailure(Throwable throwable) { + v.setEnabled(true); + + throwable.printStackTrace(); + Toast.makeText(getActivity(), "文件下载失败", Toast.LENGTH_SHORT).show(); + } + }, UpdateVersionShowDialog.this); + } + }); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + + Log.d("tag", "onDismiss: "); +// AppUpdater.getInstance().getINetManager().cancel(this); + } + + public static void show(FragmentActivity fragmentActivity, AppVersionInfoBean appVersionInfoBean) { + Bundle bundle = new Bundle(); + bundle.putSerializable(KEY_APP_VERSION_INFO_BEAN, appVersionInfoBean); + + UpdateVersionShowDialog updateVersionShowDialog = new UpdateVersionShowDialog(); + updateVersionShowDialog.setArguments(bundle); + + updateVersionShowDialog.show(fragmentActivity.getSupportFragmentManager(), "updateVersionShowDialog"); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/utils/usb/USBHelper.java b/app/src/main/java/com/sw/st/utils/usb/USBHelper.java new file mode 100644 index 0000000..113c05e --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/usb/USBHelper.java @@ -0,0 +1,262 @@ +package com.sw.st.utils.usb; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +/** + * Created by YuanYe on 18/6/21. + * Description: 封装Usb接口通信的工具类 + *

+ * 使用USB设备: + * 1.添加权限: + * + * + * 2.Manifest中添加以下,获取USB操作的通知: + * + * 3.添加设备过滤信息,气筒usb_xml可以自由修改: + * + * 4.根据目标设备的vendorId和productId过滤USB设备,拿到UsbDevice操作对象 + * 5.获取设备通讯通道 + * 6.连接 + */ +public class USBHelper { + + private static final String TAG = "USBDeviceUtil"; + private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; + private static USBHelper util; + private static Context mContext; + + private UsbDevice usbDevice; //目标USB设备 + private UsbManager usbManager; + /** + * 块输出端点 + */ + private UsbEndpoint epBulkOut; + private UsbEndpoint epBulkIn; + /** + * 控制端点 + */ + private UsbEndpoint epControl; + /** + * 中断端点 + */ + private UsbEndpoint epIntEndpointOut; + private UsbEndpoint epIntEndpointIn; + + private PendingIntent intent; //意图 + private UsbDeviceConnection conn = null; + + private OnFindListener listener; + + private int statue = USBInterface.usb_ok; + + private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (ACTION_USB_PERMISSION.equals(action)) { + statue = USBInterface.usb_permission_ok; + } else { + statue = USBInterface.usb_permission_fail; + } + } + }; + + public static USBHelper getInstance(Context _context) { + if (util == null) util = new USBHelper(_context); + mContext = _context; + return util; + } + + private USBHelper(Context _context) { + intent = PendingIntent.getBroadcast(_context, 0, new Intent(ACTION_USB_PERMISSION), 0); + _context.registerReceiver(broadcastReceiver, new IntentFilter(ACTION_USB_PERMISSION)); + } + + public UsbDevice getUsbDevice() { + return usbDevice; + } + + public UsbManager getUsbManager() { + return usbManager; + } + + /** + * 找到自定设备 + */ + public UsbDevice getUsbDevice(int vendorId, int productId) { + //1)创建usbManager + if (usbManager == null) + usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE); + //2)获取到所有设备 选择出满足的设备 + HashMap deviceList = usbManager.getDeviceList(); + Iterator deviceIterator = deviceList.values().iterator(); + while (deviceIterator.hasNext()) { + UsbDevice device = deviceIterator.next(); + Log.i(TAG, "vendorID--" + device.getVendorId() + "ProductId--" + device.getProductId()); + if (device.getVendorId() == vendorId && device.getProductId() == productId) { + return device; // 获取USBDevice + } + } + statue = USBInterface.usb_find_this_fail; + return null; + } + + /** + * 查找本机所有的USB设备 + */ + public List getUsbDevices() { + //1)创建usbManager + if (usbManager == null) + usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE); + //2)获取到所有设备 选择出满足的设备 + HashMap deviceList = usbManager.getDeviceList(); + Iterator deviceIterator = deviceList.values().iterator(); + //创建返回数据 + List lists = new ArrayList<>(); + while (deviceIterator.hasNext()) { + UsbDevice device = deviceIterator.next(); + Log.i(TAG, "vendorID--" + device.getVendorId() + "ProductId--" + device.getProductId()); + lists.add(device); + } + return lists; + } + + + /** + * 根据指定的vendorId和productId连接USB设备 + * + * @param vendorId 产商id + * @param productId 产品id + */ + public int connection(int vendorId, int productId) { + usbDevice = getUsbDevice(vendorId, productId); + //3)查找设备接口 + if (usbDevice == null) { + Log.e(TAG, "未找到目标设备,请确保供应商ID" + vendorId + "和产品ID" + productId + "是否配置正确"); + return statue; + } + UsbInterface usbInterface = null; + for (int i = 0; i < usbDevice.getInterfaceCount(); i++) { + //一个设备上面一般只有一个接口,有两个端点,分别接受和发送数据 + usbInterface = usbDevice.getInterface(i); + break; + } + //4)获取usb设备的通信通道endpoint + for (int i = 0; i < usbInterface.getEndpointCount(); i++) { + UsbEndpoint ep = usbInterface.getEndpoint(i); + switch (ep.getType()) { + case UsbConstants.USB_ENDPOINT_XFER_BULK://USB端口传输 + if (UsbConstants.USB_DIR_OUT == ep.getDirection()) {//输出 + epBulkOut = ep; + Log.e(TAG, "获取发送数据的端点"); + } else { + epBulkIn = ep; + Log.e(TAG, "获取接受数据的端点"); + } + break; + case UsbConstants.USB_ENDPOINT_XFER_CONTROL://控制 + epControl = ep; + Log.e(TAG, "find the ControlEndPoint:" + "index:" + i + "," + epControl.getEndpointNumber()); + break; + case UsbConstants.USB_ENDPOINT_XFER_INT://中断 + if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {//输出 + epIntEndpointOut = ep; + Log.e(TAG, "find the InterruptEndpointOut:" + "index:" + i + "," + epIntEndpointOut.getEndpointNumber()); + } + if (ep.getDirection() == UsbConstants.USB_DIR_IN) { + epIntEndpointIn = ep; + Log.e(TAG, "find the InterruptEndpointIn:" + "index:" + i + "," + epIntEndpointIn.getEndpointNumber()); + } + break; + default: + break; + } + } + //5)打开conn连接通道 + if (usbManager.hasPermission(usbDevice)) { + //有权限,那么打开 + conn = usbManager.openDevice(usbDevice); + } else { + usbManager.requestPermission(usbDevice, intent); + if (usbManager.hasPermission(usbDevice)) { //权限获取成功 + conn = usbManager.openDevice(usbDevice); + } else { + Log.e(TAG, "没有权限"); + statue = USBInterface.usb_permission_fail; + } + } + if (null == conn) { + Log.e(TAG, "不能连接到设备"); + statue = USBInterface.usb_open_fail; + return statue; + } + //打开设备 + if (conn.claimInterface(usbInterface, true)) { + if (conn != null)// 到此你的android设备已经连上zigbee设备 + Log.i(TAG, "open设备成功!"); + final String mySerial = conn.getSerial(); + Log.i(TAG, "设备serial number:" + mySerial); + statue = USBInterface.usb_ok; + } else { + Log.i(TAG, "无法打开连接通道。"); + statue = USBInterface.usb_passway_fail; + conn.close(); + } + return statue; + } + + /** + * 通过USB发送数据 + */ + public void sendData(byte[] buffer) { + if (conn == null || epBulkOut == null) return; + if (conn.bulkTransfer(epBulkOut, buffer, buffer.length, 0) >= 0) { + //0 或者正数表示成功 + Log.i(TAG, "发送成功"); + statue = USBInterface.usb_permission_ok; + } else { + Log.i(TAG, "发送失败的"); + statue = USBInterface.usb_permission_fail; + } + } + + /** + * 关闭USB连接 + */ + public void close() { + if (conn != null) { //关闭USB设备 + conn.close(); + conn = null; + } + if (mContext != null && broadcastReceiver != null) { + mContext.unregisterReceiver(broadcastReceiver); + } + } + + /** + * 是否找到设备回调 + */ + public interface OnFindListener { + void onFind(UsbDevice usbDevice, UsbManager usbManager); + + void onFail(String error); + } +} + diff --git a/app/src/main/java/com/sw/st/utils/usb/USBInterface.java b/app/src/main/java/com/sw/st/utils/usb/USBInterface.java new file mode 100644 index 0000000..87ba6e4 --- /dev/null +++ b/app/src/main/java/com/sw/st/utils/usb/USBInterface.java @@ -0,0 +1,19 @@ +package com.sw.st.utils.usb; + +/** + * Created by YuanYe on 2018/9/19. + * USBHelper连接时状态说明 + */ +public interface USBInterface { + + int usb_ok = 0;//usb正常打开 + + int usb_permission_ok = 10001; //USB授权成功 + int usb_permission_fail = 10002;//USB授权失败 + int usb_find_this_fail = 10003;//没有找到指定设备 + int usb_find_all_fail = 10004;//没有找到任何设备 + int usb_open_fail = 10005;//USB设备打开失败 + int usb_passway_fail = 10006;//USB通道打开失败 + int usb_send_data_ok = 10007;//USB发送数据成功 + int usb_send_data_fail = 10008;//USB发送数据失败 +} diff --git a/app/src/main/java/com/sw/st/view/BindDiningRoomPopup.java b/app/src/main/java/com/sw/st/view/BindDiningRoomPopup.java new file mode 100644 index 0000000..439dc6f --- /dev/null +++ b/app/src/main/java/com/sw/st/view/BindDiningRoomPopup.java @@ -0,0 +1,160 @@ +package com.sw.st.view; + +import android.content.Context; +import android.view.KeyEvent; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import com.lxj.xpopup.core.CenterPopupView; +import com.sw.st.R; +import com.sw.st.api.SwService; +import com.sw.st.net.helper.rxjavahelper.RxObserver; +import com.sw.st.net.helper.rxjavahelper.RxResultHelper; +import com.sw.st.net.helper.rxjavahelper.RxSchedulersHelper; +import com.sw.st.utils.AppUtil; +import com.sw.st.utils.InputMethod; +import com.sw.st.utils.PrefUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashSet; + + +import static com.sw.st.ui.setting.FoodSettingActivity.REST_NAME; +import static com.sw.st.ui.setting.FoodSettingActivity.REST_NUM; + + +public class BindDiningRoomPopup extends CenterPopupView { + private Context context; + + private LinearLayout diningRoomLayout; + private LinearLayout bindDiningRoomLayout; + private EditText diningRoomNumEdit; + private TextView diningRoomNameTv1, diningRoomNameTv2, ok; + + public BindDiningRoomPopup(@NonNull Context context) { + super(context); + this.context = context; + } + + @Override + protected int getImplLayoutId() { + return R.layout.popup_bind_diningroom; + } + + @Override + protected void onCreate() { + super.onCreate(); + diningRoomNumEdit = findViewById(R.id.diningRoomNumEdit); + diningRoomNameTv1 = findViewById(R.id.diningRoomNameTv1); + diningRoomNameTv2 = findViewById(R.id.diningRoomNameTv2); + ok = findViewById(R.id.ok); + diningRoomLayout = findViewById(R.id.diningRoomLayout); + bindDiningRoomLayout = findViewById(R.id.bindDiningRoomLayout); + findViewById(R.id.closeImg).setOnClickListener(v -> { + dismiss(); + }); + findViewById(R.id.editImg).setOnClickListener(v -> { + diningRoomLayout.setVisibility(GONE); + bindDiningRoomLayout.setVisibility(VISIBLE); + }); + diningRoomNumEdit.setOnKeyListener(new OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER + && event.getAction() == KeyEvent.ACTION_DOWN) { + InputMethod.closeInputMethod(context, v); + String num = diningRoomNumEdit.getText().toString(); + if (AppUtil.isEmpty(num)) { + return false; + } + getRestInfoByRestNo(num); + } + return false; + } + }); + findViewById(R.id.searchImg).setOnClickListener(v -> { + InputMethod.closeInputMethod(context, v); + String num = diningRoomNumEdit.getText().toString(); + if (AppUtil.isEmpty(num)) { + return; + } + getRestInfoByRestNo(num); + }); + ok.setOnClickListener(v -> { + PrefUtils.setString(context, REST_NAME, diningRoomNameTv2.getText().toString()); + PrefUtils.setString(context, REST_NUM, diningRoomNameTv2.getTag().toString()); + +// JPushInterface.cleanTags(context, 99); +// HashSet hashSet = new HashSet<>(); +// hashSet.add(diningRoomNameTv2.getTag().toString()); +// hashSet.add("TAG_ALL"); +// JPushInterface.filterValidTags(hashSet); +// JPushInterface.setTags(context, 100, JPushInterface.filterValidTags(hashSet)); + dismiss(); + }); + + String restName = PrefUtils.getString(context, "restName", ""); + if (AppUtil.isEmpty(restName)) { + diningRoomLayout.setVisibility(GONE); + bindDiningRoomLayout.setVisibility(VISIBLE); + } else { + diningRoomNameTv1.setText(restName); + } + } + + protected void onShow() { + super.onShow(); + } + + + @Override + protected int getMaxHeight() { + return 0; + } + + //返回0表示让宽度撑满window,或者你可以返回一个任意宽度 + @Override + protected int getMaxWidth() { + return 0; + } + + public void getRestInfoByRestNo(String id) { + SwService.getRestInfoByRestNo(id) + .compose(RxSchedulersHelper.io_main()) + .compose(RxResultHelper.handleJsonResponse()) + .subscribe(new RxObserver() { + @Override + public void _onNext(String info) { + try { + JSONObject jsonObject = new JSONObject(info); + if (jsonObject.optInt("code") == 200) { + JSONObject result = jsonObject.optJSONObject("result"); + diningRoomNameTv2.setText(result.optString("restName")); + diningRoomNameTv2.setTag(result.optString("id")); + diningRoomNameTv2.setVisibility(VISIBLE); + ok.setVisibility(VISIBLE); + } else { + String message = jsonObject.optString("message"); + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } + } catch (JSONException e) { + e.printStackTrace(); + + } + + } + + @Override + public void _onError(String errorMessage) { + + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/view/CustomDialog.java b/app/src/main/java/com/sw/st/view/CustomDialog.java new file mode 100644 index 0000000..e02da26 --- /dev/null +++ b/app/src/main/java/com/sw/st/view/CustomDialog.java @@ -0,0 +1,83 @@ +package com.sw.st.view; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import com.sw.st.R; + +public class CustomDialog extends Dialog { + + + /** + * 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决) + */ + public CustomDialog(Context context, View layout, int style) { + super(context, R.style.MyDialog); + setContentView(layout); + Window window = getWindow(); + WindowManager.LayoutParams params = window.getAttributes(); + params.gravity = Gravity.CENTER; + window.setAttributes(params); + } + + + /** + * 宽高由该方法的参数设置 + */ + public CustomDialog(Context context, int width, int height, View layout, + int style) { + super(context, style); + // 设置内容 + setContentView(layout); + // 设置窗口属性 + Window window = getWindow(); + WindowManager.LayoutParams params = window.getAttributes(); + // 设置宽度、高度、密度、对齐方式 + float density = getDensity(context); + params.width = (int) (width * density); + params.height = (int) (height * density); + params.gravity = Gravity.CENTER; + window.setAttributes(params); + + } + + /** + * 获取显示密度 + * + * @param context + * @return + */ + public float getDensity(Context context) { + Resources res = context.getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); + return dm.density; + } + + @Override + public void show() { + this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + super.show(); + fullScreenImmersive(getWindow().getDecorView()); + this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + } + + private void fullScreenImmersive(View view) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_FULLSCREEN; + view.setSystemUiVisibility(uiOptions); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/view/CustomImageView.java b/app/src/main/java/com/sw/st/view/CustomImageView.java new file mode 100644 index 0000000..36e7b39 --- /dev/null +++ b/app/src/main/java/com/sw/st/view/CustomImageView.java @@ -0,0 +1,76 @@ +package com.sw.st.view; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; + +public class CustomImageView extends androidx.appcompat.widget.AppCompatImageView { + private AlphaAnimation alphaAnimation; + + public CustomImageView(Context context) { + super(context); + + } + + public CustomImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + // 用于设置自定义高度的方法 + public void setCustomHeight(int height, boolean isAnimation) { + ViewGroup.LayoutParams layoutParams = getLayoutParams(); + int height1 = layoutParams.height; + if (height1 != height) { + if (isAnimation) { + ValueAnimator animator = ValueAnimator.ofInt(height1, height); + animator.addUpdateListener(animation -> { + // 更新视图的高度 + int val = (Integer) animation.getAnimatedValue(); + ViewGroup.LayoutParams params = getLayoutParams(); + params.height = dpToPixels(val); + setLayoutParams(params); + }); + animator.setDuration(100); // 动画持续时间500毫秒 + animator.start(); // 开始动画 + } else { + if (layoutParams == null) { + layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height); + } else { + layoutParams.height = dpToPixels(height); + } + setLayoutParams(layoutParams); + } + + } + } + + private boolean isStartAlphaAnimation = false; + + public void startAlphaAnimation() { + if (!isStartAlphaAnimation) { + alphaAnimation = new AlphaAnimation(1.0f, 0.2f); // 从不透明渐变到完全透明 + alphaAnimation.setDuration(420); // 设置动画持续时间,单位为毫秒 + alphaAnimation.setRepeatCount(AlphaAnimation.INFINITE); // 设置动画重复次数为无限(可选) + startAnimation(alphaAnimation); + isStartAlphaAnimation = true; + } + } + + public void cancelAlphaAnimation() { + if (alphaAnimation != null) + alphaAnimation.cancel(); + isStartAlphaAnimation = false; + } + + private int dpToPixels(int dp) { + float scale = getResources().getDisplayMetrics().density; + return (int) (dp * scale + 0.5f); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/view/CustomSeekBar.java b/app/src/main/java/com/sw/st/view/CustomSeekBar.java new file mode 100644 index 0000000..9313d53 --- /dev/null +++ b/app/src/main/java/com/sw/st/view/CustomSeekBar.java @@ -0,0 +1,190 @@ +package com.sw.st.view; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.SeekBar; + +import com.sw.st.R; + +@SuppressLint("AppCompatCustomView") +public class CustomSeekBar extends SeekBar { + + + /** + * 文本的颜色 + */ + private int mTitleTextColor; + /** + * 文本的大小 + */ + private float mTitleTextSize; + private String mTitleText = "0Kcal";//文字的内容 + + /** + * 背景图片 + */ + private int img; + private Bitmap map; + //bitmap对应的宽高 + private float img_width, img_height; + Paint paint; + + private float numTextWidth; + //测量seekbar的规格 + private Rect rect_seek; + private Paint.FontMetrics fm; + + public static final int TEXT_ALIGN_LEFT = 0x00000001; + public static final int TEXT_ALIGN_RIGHT = 0x00000010; + public static final int TEXT_ALIGN_CENTER_VERTICAL = 0x00000100; + public static final int TEXT_ALIGN_CENTER_HORIZONTAL = 0x00001000; + public static final int TEXT_ALIGN_TOP = 0x00010000; + public static final int TEXT_ALIGN_BOTTOM = 0x00100000; + /** + * 文本中轴线X坐标 + */ + private float textCenterX; + /** + * 文本baseline线Y坐标 + */ + private float textBaselineY; + /** + * 文字的方位 + */ + private int textAlign; + + public CustomSeekBar(Context context) { + this(context, null); + } + + public CustomSeekBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CustomSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MySeekBar, defStyleAttr, 0); + int n = array.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = array.getIndex(i); + switch (attr) { + case R.styleable.MySeekBar_textsize: + mTitleTextSize = array.getDimension(attr, 15f); + break; + case R.styleable.MySeekBar_textcolor: + mTitleTextColor = array.getColor(attr, Color.WHITE); + break; + case R.styleable.MySeekBar_img: + img = array.getResourceId(attr, R.mipmap.ic_launcher); + break; + } + } + array.recycle(); + getImgWH(); + paint = new Paint(); + paint.setAntiAlias(true);//设置抗锯齿 + paint.setTextSize(mTitleTextSize);//设置文字大小 + paint.setColor(mTitleTextColor);//设置文字颜色 + //设置控件的padding 给提示文字留出位置 + setPadding((int) Math.ceil(img_width) / 2, 32, (int) Math.ceil(img_width) / 2, (int) Math.ceil(img_height) + 10); + textAlign = TEXT_ALIGN_CENTER_HORIZONTAL | TEXT_ALIGN_CENTER_VERTICAL; + } + + /** + * 获取图片的宽高 + */ + private void getImgWH() { + map = BitmapFactory.decodeResource(getResources(), img); + img_width = map.getWidth(); + img_height = map.getHeight(); + + + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + super.onDraw(canvas); + setTextLocation();//定位文本绘制的位置 + rect_seek = this.getProgressDrawable().getBounds(); + //定位文字背景图片的位置 + float bm_x = rect_seek.width() * getProgress() / getMax(); + float bm_y = rect_seek.height() - 12; +// //计算文字的中心位置在bitmap + float text_x = rect_seek.width() * getProgress() / getMax() + (img_width - numTextWidth) / 2; + canvas.drawBitmap(map, bm_x, bm_y, paint);//画背景图 + // canvas.drawRoundRect(); + canvas.drawText(mTitleText, text_x, (float) (textBaselineY + bm_y + (0.16 * img_height / 2)) - 4, paint);//画文字 + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + invalidate();//监听手势滑动,不断重绘文字和背景图的显示位置 + return super.onTouchEvent(event); + } + + public void setTextInfo(String info) { + mTitleText = info; + } + + + /** + * 定位文本绘制的位置 + */ + private void setTextLocation() { + + fm = paint.getFontMetrics(); + //文本的宽度 +// mTitleText = "0Kcal"; + + numTextWidth = paint.measureText(mTitleText); + + float textCenterVerticalBaselineY = img_height / 2 - fm.descent + (fm.descent - fm.ascent) / 2; + switch (textAlign) { + case TEXT_ALIGN_CENTER_HORIZONTAL | TEXT_ALIGN_CENTER_VERTICAL: + textCenterX = img_width / 2; + textBaselineY = textCenterVerticalBaselineY; + break; + case TEXT_ALIGN_LEFT | TEXT_ALIGN_CENTER_VERTICAL: + textCenterX = numTextWidth / 2; + textBaselineY = textCenterVerticalBaselineY; + break; + case TEXT_ALIGN_RIGHT | TEXT_ALIGN_CENTER_VERTICAL: + textCenterX = img_width - numTextWidth / 2; + textBaselineY = textCenterVerticalBaselineY; + break; + case TEXT_ALIGN_BOTTOM | TEXT_ALIGN_CENTER_HORIZONTAL: + textCenterX = img_width / 2; + textBaselineY = img_height - fm.bottom; + break; + case TEXT_ALIGN_TOP | TEXT_ALIGN_CENTER_HORIZONTAL: + textCenterX = img_width / 2; + textBaselineY = -fm.ascent; + break; + case TEXT_ALIGN_TOP | TEXT_ALIGN_LEFT: + textCenterX = numTextWidth / 2; + textBaselineY = -fm.ascent; + break; + case TEXT_ALIGN_BOTTOM | TEXT_ALIGN_LEFT: + textCenterX = numTextWidth / 2; + textBaselineY = img_height - fm.bottom; + break; + case TEXT_ALIGN_TOP | TEXT_ALIGN_RIGHT: + textCenterX = img_width - numTextWidth / 2; + textBaselineY = -fm.ascent; + break; + case TEXT_ALIGN_BOTTOM | TEXT_ALIGN_RIGHT: + textCenterX = img_width - numTextWidth / 2; + textBaselineY = img_height - fm.bottom; + break; + } + } +} diff --git a/app/src/main/java/com/sw/st/view/IPEditText.java b/app/src/main/java/com/sw/st/view/IPEditText.java new file mode 100644 index 0000000..934b2fe --- /dev/null +++ b/app/src/main/java/com/sw/st/view/IPEditText.java @@ -0,0 +1,286 @@ +package com.sw.st.view; + +import android.content.Context; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Toast; + +import com.sw.st.R; +import com.sw.st.utils.AppUtil; + +public class IPEditText extends LinearLayout { + + private EditText mFirstIP; + private EditText mSecondIP; + private EditText mThirdIP; + private EditText mFourthIP; + + private String mText1; + private String mText2; + private String mText3; + private String mText4; + + + public IPEditText(Context context, AttributeSet attrs) { + super(context, attrs); + + /** + * 初始化控件 + */ + View view = LayoutInflater.from(context).inflate( + R.layout.custom_my_edittext, this); + mFirstIP = (EditText) findViewById(R.id.ip_first); + mSecondIP = (EditText) findViewById(R.id.ip_second); + mThirdIP = (EditText) findViewById(R.id.ip_third); + mFourthIP = (EditText) findViewById(R.id.ip_fourth); + OperatingEditText(context); + } + + /** + * 获得EditText中的内容,当每个Edittext的字符达到三位时,自动跳转到下一个EditText,当用户点击.时, + * 下一个EditText获得焦点 + */ + private void OperatingEditText(final Context context) { + mFirstIP.addTextChangedListener(new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + /** + * 获得EditTe输入内容,做判断,如果大于255,提示不合法,当数字为合法的三位数下一个EditText获得焦点, + * 用户点击啊.时,下一个EditText获得焦点 + */ + if (s != null && s.length() > 0) { + if (s.length() > 2 || s.toString().trim().contains(".")) { + if (s.toString().trim().contains(".")) { + mText1 = s.toString().substring(0, s.length() - 1); + mFirstIP.setText(mText1); + } else { + mText1 = s.toString().trim(); + } + + if (AppUtil.isEmpty(mText1)) { + return; + } + if (Integer.parseInt(mText1) > 255) { + Toast.makeText(context, "请输入合法的ip地址", + Toast.LENGTH_LONG).show(); + mFirstIP.getText().delete(mText1.length() - 1, mText1.length()); + return; + } + + mSecondIP.setFocusable(true); + mSecondIP.requestFocus(); + } + } + + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + mSecondIP.addTextChangedListener(new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + /** + * 获得EditTe输入内容,做判断,如果大于255,提示不合法,当数字为合法的三位数下一个EditText获得焦点, + * 用户点击啊.时,下一个EditText获得焦点 + */ + if (s != null && s.length() > 0) { + if (s.length() > 2 || s.toString().trim().contains(".")) { + if (s.toString().trim().contains(".")) { + mText2 = s.toString().substring(0, s.length() - 1); + mSecondIP.setText(mText2); + } else { + mText2 = s.toString().trim(); + } + + if (AppUtil.isEmpty(mText2)) { + return; + } + if (Integer.parseInt(mText2) > 255) { + Toast.makeText(context, "请输入合法的ip地址", + Toast.LENGTH_LONG).show(); + mSecondIP.getText().delete(mText2.length() - 1, mText2.length()); + return; + } + + mThirdIP.setFocusable(true); + mThirdIP.requestFocus(); + } + } + + /** + * 当用户需要删除时,此时的EditText为空时,上一个EditText获得焦点 + */ + if (start == 0 && s.length() == 0) { + mFirstIP.setFocusable(true); + mFirstIP.requestFocus(); + mFirstIP.setSelection(mText1.length()); + } + + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + mThirdIP.addTextChangedListener(new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + /** + * 获得EditTe输入内容,做判断,如果大于255,提示不合法,当数字为合法的三位数下一个EditText获得焦点, + * 用户点击啊.时,下一个EditText获得焦点 + */ + if (s != null && s.length() > 0) { + if (s.length() > 2 || s.toString().trim().contains(".")) { + if (s.toString().trim().contains(".")) { + mText3 = s.toString().substring(0, s.length() - 1); + mThirdIP.setText(mText3); + } else { + mText3 = s.toString().trim(); + } + + if (AppUtil.isEmpty(mText3)) { + return; + } + if (Integer.parseInt(mText3) > 255) { + Toast.makeText(context, "请输入合法的ip地址", + Toast.LENGTH_LONG).show(); + mThirdIP.getText().delete(mText3.length() - 1, mText3.length()); + return; + } + + mFourthIP.setFocusable(true); + mFourthIP.requestFocus(); + } + } + + /** + * 当用户需要删除时,此时的EditText为空时,上一个EditText获得焦点 + */ + if (start == 0 && s.length() == 0) { + mSecondIP.setFocusable(true); + mSecondIP.requestFocus(); + mSecondIP.setSelection(mText2.length()); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + mFourthIP.addTextChangedListener(new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + /** + * 获得EditTe输入内容,做判断,如果大于255,提示不合法,当数字为合法的三位数下一个EditText获得焦点, + * 用户点击啊.时,下一个EditText获得焦点 + */ + if (s != null && s.length() > 0) { + mText4 = s.toString().trim(); + + if (AppUtil.isEmpty(mText4)) { + return; + } + if (Integer.parseInt(mText4) > 255) { + Toast.makeText(context, "请输入合法的ip地址", Toast.LENGTH_LONG) + .show(); + mFourthIP.getText().delete(mText4.length() - 1, mText4.length()); + return; + } + + } + + /** + * 当用户需要删除时,此时的EditText为空时,上一个EditText获得焦点 + */ + if (start == 0 && s.length() == 0) { + mThirdIP.setFocusable(true); + mThirdIP.requestFocus(); + mThirdIP.setSelection(mText3.length()); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + } + + public void setText(String ip) { + if (AppUtil.isEmpty(ip)) { + return; + } + String[] arrStr = ip.split("\\."); + if (arrStr.length == 4) { + mText1 = arrStr[0]; + mText2 = arrStr[1]; + mText3 = arrStr[2]; + mText4 = arrStr[3]; + mFirstIP.setText(mText1); + mSecondIP.setText(mText2); + mThirdIP.setText(mText3); + mFourthIP.setText(mText4); + } + } + + public String getText(Context context) { + mText1 = mFirstIP.getText().toString(); + mText2 = mSecondIP.getText().toString(); + mText3 = mThirdIP.getText().toString(); + mText4 = mFourthIP.getText().toString(); + if (TextUtils.isEmpty(mText1) || + TextUtils.isEmpty(mText2) || + TextUtils.isEmpty(mText3) || + TextUtils.isEmpty(mText4)) { + Toast.makeText(context, "请输入合法的ip地址", Toast.LENGTH_LONG).show(); + return ""; + } + return mText1 + "." + mText2 + "." + mText3 + "." + mText4; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/st/view/TextDrawable.java b/app/src/main/java/com/sw/st/view/TextDrawable.java new file mode 100644 index 0000000..198b8b2 --- /dev/null +++ b/app/src/main/java/com/sw/st/view/TextDrawable.java @@ -0,0 +1,141 @@ +package com.sw.st.view; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import com.sw.st.R; + +@SuppressLint("AppCompatCustomView") +public class TextDrawable extends TextView { + + private Drawable drawableLeft; + private Drawable drawableRight; + private Drawable drawableTop; + private int leftWidth; + private int rightWidth; + private int topWidth; + private int leftHeight; + private int rightHeight; + private int topHeight; + private Context mContext; + + + public TextDrawable(Context context) { + super(context); + this.mContext = context; + init(context, null); + } + + public TextDrawable(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + this.mContext = context; + init(context, attrs); + } + + public TextDrawable(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.mContext = context; + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextDrawable); + drawableLeft = typedArray.getDrawable(R.styleable.TextDrawable_leftDrawable); + drawableRight = typedArray.getDrawable(R.styleable.TextDrawable_rightDrawable); + drawableTop = typedArray.getDrawable(R.styleable.TextDrawable_topDrawable); + if (drawableLeft != null) { + leftWidth = typedArray.getDimensionPixelOffset(R.styleable.TextDrawable_leftDrawableWidth, dip2px(context, 20)); + leftHeight = typedArray.getDimensionPixelOffset(R.styleable.TextDrawable_leftDrawableHeight, dip2px(context, 20)); + } + if (drawableRight != null) { + rightWidth = typedArray.getDimensionPixelOffset(R.styleable.TextDrawable_rightDrawableWidth, dip2px(context, 20)); + rightHeight = typedArray.getDimensionPixelOffset(R.styleable.TextDrawable_rightDrawableHeight, dip2px(context, 20)); + } + if (drawableTop != null) { + topWidth = typedArray.getDimensionPixelOffset(R.styleable.TextDrawable_topDrawableWidth, dip2px(context, 20)); + topHeight = typedArray.getDimensionPixelOffset(R.styleable.TextDrawable_topDrawableHeight, dip2px(context, 20)); + } + } + + + public int dip2px(Context context, float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (drawableLeft != null) { + drawableLeft.setBounds(0, 0, leftWidth, leftHeight); + } + if (drawableRight != null) { + drawableRight.setBounds(0, 0, rightWidth, rightHeight); + } + if (drawableTop != null) { + drawableTop.setBounds(0, 0, topWidth, topHeight); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + this.setCompoundDrawables(drawableLeft, drawableTop, drawableRight, null); + + } + + /** + * 设置左侧图片并重绘 + */ + public void setDrawableLeft(Drawable drawableLeft) { + this.drawableLeft = drawableLeft; + invalidate(); + } + + /** + * 设置左侧图片并重绘 + */ + public void setDrawableLeft(int drawableLeftRes) { + this.drawableLeft = mContext.getResources().getDrawable(drawableLeftRes); + invalidate(); + } + + /** + * 设置右侧图片并重绘 + */ + public void setDrawableRight(Drawable drawableRight) { + this.drawableRight = drawableLeft; + invalidate(); + } + + /** + * 设置右侧图片并重绘 + */ + public void setDrawableRight(int drawableRightRes) { + this.drawableRight = mContext.getResources().getDrawable(drawableRightRes); + invalidate(); + } + + /** + * 设置上部图片并重绘 + */ + public void setDrawable(Drawable drawableTop) { + this.drawableTop = drawableTop; + invalidate(); + } + + /** + * 设置右侧图片并重绘 + */ + public void setDrawableTop(int drawableTopRes) { + this.drawableTop = mContext.getResources().getDrawable(drawableTopRes); + invalidate(); + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/rotate_anim.xml b/app/src/main/res/anim/rotate_anim.xml new file mode 100644 index 0000000..9da1cb1 --- /dev/null +++ b/app/src/main/res/anim/rotate_anim.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_left_in.xml b/app/src/main/res/anim/slide_left_in.xml new file mode 100644 index 0000000..74d62a9 --- /dev/null +++ b/app/src/main/res/anim/slide_left_in.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_left_out.xml b/app/src/main/res/anim/slide_left_out.xml new file mode 100644 index 0000000..092d3f5 --- /dev/null +++ b/app/src/main/res/anim/slide_left_out.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_right_in.xml b/app/src/main/res/anim/slide_right_in.xml new file mode 100644 index 0000000..be3d25b --- /dev/null +++ b/app/src/main/res/anim/slide_right_in.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_right_out.xml b/app/src/main/res/anim/slide_right_out.xml new file mode 100644 index 0000000..ea7330e --- /dev/null +++ b/app/src/main/res/anim/slide_right_out.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/bg_cheng_item.xml b/app/src/main/res/drawable-v24/bg_cheng_item.xml new file mode 100644 index 0000000..30e617b --- /dev/null +++ b/app/src/main/res/drawable-v24/bg_cheng_item.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/bg_flow_tv.xml b/app/src/main/res/drawable-v24/bg_flow_tv.xml new file mode 100644 index 0000000..364a1e0 --- /dev/null +++ b/app/src/main/res/drawable-v24/bg_flow_tv.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/bg_food_tag_red_tv.xml b/app/src/main/res/drawable-v24/bg_food_tag_red_tv.xml new file mode 100644 index 0000000..621a506 --- /dev/null +++ b/app/src/main/res/drawable-v24/bg_food_tag_red_tv.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/flow_tv_color.xml b/app/src/main/res/drawable-v24/flow_tv_color.xml new file mode 100644 index 0000000..1800b2e --- /dev/null +++ b/app/src/main/res/drawable-v24/flow_tv_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_back.xml b/app/src/main/res/drawable-v24/ic_back.xml new file mode 100644 index 0000000..24a87d0 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_back.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/seekbar_style.xml b/app/src/main/res/drawable-v24/seekbar_style.xml new file mode 100644 index 0000000..8d950a6 --- /dev/null +++ b/app/src/main/res/drawable-v24/seekbar_style.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg.png b/app/src/main/res/drawable/bg.png new file mode 100644 index 0000000..0d94bb4 Binary files /dev/null and b/app/src/main/res/drawable/bg.png differ diff --git a/app/src/main/res/drawable/bg_big.9.png b/app/src/main/res/drawable/bg_big.9.png new file mode 100644 index 0000000..6ff5da2 Binary files /dev/null and b/app/src/main/res/drawable/bg_big.9.png differ diff --git a/app/src/main/res/drawable/bg_big_concave.9.png b/app/src/main/res/drawable/bg_big_concave.9.png new file mode 100644 index 0000000..e83984c Binary files /dev/null and b/app/src/main/res/drawable/bg_big_concave.9.png differ diff --git a/app/src/main/res/drawable/bg_border_stroke.xml b/app/src/main/res/drawable/bg_border_stroke.xml new file mode 100644 index 0000000..87c6a5b --- /dev/null +++ b/app/src/main/res/drawable/bg_border_stroke.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_bottom.xml b/app/src/main/res/drawable/bg_bottom.xml new file mode 100644 index 0000000..2a1d8d9 --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_bottom_left.9.png b/app/src/main/res/drawable/bg_bottom_left.9.png new file mode 100644 index 0000000..6077c07 Binary files /dev/null and b/app/src/main/res/drawable/bg_bottom_left.9.png differ diff --git a/app/src/main/res/drawable/bg_bottom_right.9.png b/app/src/main/res/drawable/bg_bottom_right.9.png new file mode 100644 index 0000000..3c67cad Binary files /dev/null and b/app/src/main/res/drawable/bg_bottom_right.9.png differ diff --git a/app/src/main/res/drawable/bg_button.xml b/app/src/main/res/drawable/bg_button.xml new file mode 100644 index 0000000..b5e5536 --- /dev/null +++ b/app/src/main/res/drawable/bg_button.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_radius4.xml b/app/src/main/res/drawable/bg_button_radius4.xml new file mode 100644 index 0000000..3063872 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_radius4.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_concave.9.png b/app/src/main/res/drawable/bg_concave.9.png new file mode 100644 index 0000000..3100f79 Binary files /dev/null and b/app/src/main/res/drawable/bg_concave.9.png differ diff --git a/app/src/main/res/drawable/bg_content.xml b/app/src/main/res/drawable/bg_content.xml new file mode 100644 index 0000000..9fe3d53 --- /dev/null +++ b/app/src/main/res/drawable/bg_content.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_convex.9.png b/app/src/main/res/drawable/bg_convex.9.png new file mode 100644 index 0000000..851dca9 Binary files /dev/null and b/app/src/main/res/drawable/bg_convex.9.png differ diff --git a/app/src/main/res/drawable/bg_current_food.xml b/app/src/main/res/drawable/bg_current_food.xml new file mode 100644 index 0000000..0a2985f --- /dev/null +++ b/app/src/main/res/drawable/bg_current_food.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_current_weight.xml b/app/src/main/res/drawable/bg_current_weight.xml new file mode 100644 index 0000000..55494da --- /dev/null +++ b/app/src/main/res/drawable/bg_current_weight.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_flow_tv.xml b/app/src/main/res/drawable/bg_flow_tv.xml new file mode 100644 index 0000000..364a1e0 --- /dev/null +++ b/app/src/main/res/drawable/bg_flow_tv.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_food_lable.xml b/app/src/main/res/drawable/bg_food_lable.xml new file mode 100644 index 0000000..315813d --- /dev/null +++ b/app/src/main/res/drawable/bg_food_lable.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_food_tag_blue.xml b/app/src/main/res/drawable/bg_food_tag_blue.xml new file mode 100644 index 0000000..dcb7ada --- /dev/null +++ b/app/src/main/res/drawable/bg_food_tag_blue.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_food_tag_normal.xml b/app/src/main/res/drawable/bg_food_tag_normal.xml new file mode 100644 index 0000000..077c91a --- /dev/null +++ b/app/src/main/res/drawable/bg_food_tag_normal.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_food_tag_red.xml b/app/src/main/res/drawable/bg_food_tag_red.xml new file mode 100644 index 0000000..d3e1747 --- /dev/null +++ b/app/src/main/res/drawable/bg_food_tag_red.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_food_tag_select.xml b/app/src/main/res/drawable/bg_food_tag_select.xml new file mode 100644 index 0000000..bf432fe --- /dev/null +++ b/app/src/main/res/drawable/bg_food_tag_select.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_food_tag_yellow.xml b/app/src/main/res/drawable/bg_food_tag_yellow.xml new file mode 100644 index 0000000..e15f33d --- /dev/null +++ b/app/src/main/res/drawable/bg_food_tag_yellow.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_health_tip.9.png b/app/src/main/res/drawable/bg_health_tip.9.png new file mode 100644 index 0000000..ccb47a7 Binary files /dev/null and b/app/src/main/res/drawable/bg_health_tip.9.png differ diff --git a/app/src/main/res/drawable/bg_init_button.xml b/app/src/main/res/drawable/bg_init_button.xml new file mode 100644 index 0000000..735d3df --- /dev/null +++ b/app/src/main/res/drawable/bg_init_button.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_member_price.9.png b/app/src/main/res/drawable/bg_member_price.9.png new file mode 100644 index 0000000..6ae0ac1 Binary files /dev/null and b/app/src/main/res/drawable/bg_member_price.9.png differ diff --git a/app/src/main/res/drawable/bg_name.xml b/app/src/main/res/drawable/bg_name.xml new file mode 100644 index 0000000..229644c --- /dev/null +++ b/app/src/main/res/drawable/bg_name.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_new_button_down.9.png b/app/src/main/res/drawable/bg_new_button_down.9.png new file mode 100644 index 0000000..25f18c8 Binary files /dev/null and b/app/src/main/res/drawable/bg_new_button_down.9.png differ diff --git a/app/src/main/res/drawable/bg_new_button_normal.9.png b/app/src/main/res/drawable/bg_new_button_normal.9.png new file mode 100644 index 0000000..308ad94 Binary files /dev/null and b/app/src/main/res/drawable/bg_new_button_normal.9.png differ diff --git a/app/src/main/res/drawable/bg_nutrition_tag.9.png b/app/src/main/res/drawable/bg_nutrition_tag.9.png new file mode 100644 index 0000000..367f82d Binary files /dev/null and b/app/src/main/res/drawable/bg_nutrition_tag.9.png differ diff --git a/app/src/main/res/drawable/bg_price.9.png b/app/src/main/res/drawable/bg_price.9.png new file mode 100644 index 0000000..74819b0 Binary files /dev/null and b/app/src/main/res/drawable/bg_price.9.png differ diff --git a/app/src/main/res/drawable/bg_search_edit.xml b/app/src/main/res/drawable/bg_search_edit.xml new file mode 100644 index 0000000..34267fe --- /dev/null +++ b/app/src/main/res/drawable/bg_search_edit.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_small.9.png b/app/src/main/res/drawable/bg_small.9.png new file mode 100644 index 0000000..acd4ffe Binary files /dev/null and b/app/src/main/res/drawable/bg_small.9.png differ diff --git a/app/src/main/res/drawable/bg_small_concave.9.png b/app/src/main/res/drawable/bg_small_concave.9.png new file mode 100644 index 0000000..1fa371f Binary files /dev/null and b/app/src/main/res/drawable/bg_small_concave.9.png differ diff --git a/app/src/main/res/drawable/bg_tag.xml b/app/src/main/res/drawable/bg_tag.xml new file mode 100644 index 0000000..999877b --- /dev/null +++ b/app/src/main/res/drawable/bg_tag.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_green.xml b/app/src/main/res/drawable/bg_text_green.xml new file mode 100644 index 0000000..1de3a10 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_green.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_price.xml b/app/src/main/res/drawable/bg_text_price.xml new file mode 100644 index 0000000..9ecc5b9 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_price.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_red.xml b/app/src/main/res/drawable/bg_text_red.xml new file mode 100644 index 0000000..4bbc656 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_red.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_vip_price.xml b/app/src/main/res/drawable/bg_text_vip_price.xml new file mode 100644 index 0000000..b9ed6f3 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_vip_price.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_yellow1.xml b/app/src/main/res/drawable/bg_text_yellow1.xml new file mode 100644 index 0000000..f5d4a51 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_yellow1.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_text_yellow2.xml b/app/src/main/res/drawable/bg_text_yellow2.xml new file mode 100644 index 0000000..6004b88 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_yellow2.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_tips_blue.xml b/app/src/main/res/drawable/bg_tips_blue.xml new file mode 100644 index 0000000..d6ee7cb --- /dev/null +++ b/app/src/main/res/drawable/bg_tips_blue.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_tips_red.xml b/app/src/main/res/drawable/bg_tips_red.xml new file mode 100644 index 0000000..901ceb0 --- /dev/null +++ b/app/src/main/res/drawable/bg_tips_red.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_tips_yellow.xml b/app/src/main/res/drawable/bg_tips_yellow.xml new file mode 100644 index 0000000..0b2f7e9 --- /dev/null +++ b/app/src/main/res/drawable/bg_tips_yellow.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_top.xml b/app/src/main/res/drawable/bg_top.xml new file mode 100644 index 0000000..8c2a7d0 --- /dev/null +++ b/app/src/main/res/drawable/bg_top.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_warning.xml b/app/src/main/res/drawable/bg_warning.xml new file mode 100644 index 0000000..e75b550 --- /dev/null +++ b/app/src/main/res/drawable/bg_warning.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_warning_img.9.png b/app/src/main/res/drawable/bg_warning_img.9.png new file mode 100644 index 0000000..8d2844f Binary files /dev/null and b/app/src/main/res/drawable/bg_warning_img.9.png differ diff --git a/app/src/main/res/drawable/bg_white.xml b/app/src/main/res/drawable/bg_white.xml new file mode 100644 index 0000000..1c2ba76 --- /dev/null +++ b/app/src/main/res/drawable/bg_white.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/content_bg.9.png b/app/src/main/res/drawable/content_bg.9.png new file mode 100644 index 0000000..c780ccc Binary files /dev/null and b/app/src/main/res/drawable/content_bg.9.png differ diff --git a/app/src/main/res/drawable/cursor.xml b/app/src/main/res/drawable/cursor.xml new file mode 100644 index 0000000..080e4eb --- /dev/null +++ b/app/src/main/res/drawable/cursor.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/custom_progress_blue.xml b/app/src/main/res/drawable/custom_progress_blue.xml new file mode 100644 index 0000000..1c01dc3 --- /dev/null +++ b/app/src/main/res/drawable/custom_progress_blue.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/custom_progress_gray.xml b/app/src/main/res/drawable/custom_progress_gray.xml new file mode 100644 index 0000000..71825c2 --- /dev/null +++ b/app/src/main/res/drawable/custom_progress_gray.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/custom_progress_red.xml b/app/src/main/res/drawable/custom_progress_red.xml new file mode 100644 index 0000000..8171be7 --- /dev/null +++ b/app/src/main/res/drawable/custom_progress_red.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/custom_progress_yellow.xml b/app/src/main/res/drawable/custom_progress_yellow.xml new file mode 100644 index 0000000..0a974c8 --- /dev/null +++ b/app/src/main/res/drawable/custom_progress_yellow.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_error.xml b/app/src/main/res/drawable/dialog_error.xml new file mode 100644 index 0000000..56700e5 --- /dev/null +++ b/app/src/main/res/drawable/dialog_error.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_update.xml b/app/src/main/res/drawable/dialog_update.xml new file mode 100644 index 0000000..953019e --- /dev/null +++ b/app/src/main/res/drawable/dialog_update.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_waiting.xml b/app/src/main/res/drawable/dialog_waiting.xml new file mode 100644 index 0000000..e15a0f3 --- /dev/null +++ b/app/src/main/res/drawable/dialog_waiting.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_blue.xml b/app/src/main/res/drawable/dot_blue.xml new file mode 100644 index 0000000..58f2a44 --- /dev/null +++ b/app/src/main/res/drawable/dot_blue.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_red.xml b/app/src/main/res/drawable/dot_red.xml new file mode 100644 index 0000000..efc3503 --- /dev/null +++ b/app/src/main/res/drawable/dot_red.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dot_yellow.xml b/app/src/main/res/drawable/dot_yellow.xml new file mode 100644 index 0000000..8df04ab --- /dev/null +++ b/app/src/main/res/drawable/dot_yellow.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/flow_tv_color.xml b/app/src/main/res/drawable/flow_tv_color.xml new file mode 100644 index 0000000..1800b2e --- /dev/null +++ b/app/src/main/res/drawable/flow_tv_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_cancel_24.xml b/app/src/main/res/drawable/ic_baseline_cancel_24.xml new file mode 100644 index 0000000..04a7524 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_cancel_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_search_24.xml b/app/src/main/res/drawable/ic_baseline_search_24.xml new file mode 100644 index 0000000..5fb5944 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_search_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..5ab3fbe --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000..634f729 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher.png b/app/src/main/res/drawable/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/app/src/main/res/drawable/ic_launcher.png differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..baf3435 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/img_gray_column.9.png b/app/src/main/res/drawable/img_gray_column.9.png new file mode 100644 index 0000000..07898c2 Binary files /dev/null and b/app/src/main/res/drawable/img_gray_column.9.png differ diff --git a/app/src/main/res/drawable/img_green_column.9.png b/app/src/main/res/drawable/img_green_column.9.png new file mode 100644 index 0000000..db4411e Binary files /dev/null and b/app/src/main/res/drawable/img_green_column.9.png differ diff --git a/app/src/main/res/drawable/img_orange_column.9.png b/app/src/main/res/drawable/img_orange_column.9.png new file mode 100644 index 0000000..8f5b833 Binary files /dev/null and b/app/src/main/res/drawable/img_orange_column.9.png differ diff --git a/app/src/main/res/drawable/img_red_column.9.png b/app/src/main/res/drawable/img_red_column.9.png new file mode 100644 index 0000000..a4afd65 Binary files /dev/null and b/app/src/main/res/drawable/img_red_column.9.png differ diff --git a/app/src/main/res/drawable/img_yellow_column.9.png b/app/src/main/res/drawable/img_yellow_column.9.png new file mode 100644 index 0000000..e98d9df Binary files /dev/null and b/app/src/main/res/drawable/img_yellow_column.9.png differ diff --git a/app/src/main/res/drawable/init_bg_button.xml b/app/src/main/res/drawable/init_bg_button.xml new file mode 100644 index 0000000..3c1dc11 --- /dev/null +++ b/app/src/main/res/drawable/init_bg_button.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ip_input_shape.xml b/app/src/main/res/drawable/ip_input_shape.xml new file mode 100644 index 0000000..ed0aae2 --- /dev/null +++ b/app/src/main/res/drawable/ip_input_shape.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/launch_screen.xml b/app/src/main/res/drawable/launch_screen.xml new file mode 100644 index 0000000..961f077 --- /dev/null +++ b/app/src/main/res/drawable/launch_screen.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/my_rating_blue.xml b/app/src/main/res/drawable/my_rating_blue.xml new file mode 100644 index 0000000..16c096e --- /dev/null +++ b/app/src/main/res/drawable/my_rating_blue.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/my_rating_red.xml b/app/src/main/res/drawable/my_rating_red.xml new file mode 100644 index 0000000..64ddb31 --- /dev/null +++ b/app/src/main/res/drawable/my_rating_red.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/my_rating_yellow.xml b/app/src/main/res/drawable/my_rating_yellow.xml new file mode 100644 index 0000000..2a30991 --- /dev/null +++ b/app/src/main/res/drawable/my_rating_yellow.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/new_bg_button.xml b/app/src/main/res/drawable/new_bg_button.xml new file mode 100644 index 0000000..fc11dac --- /dev/null +++ b/app/src/main/res/drawable/new_bg_button.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/new_button_tv_color.xml b/app/src/main/res/drawable/new_button_tv_color.xml new file mode 100644 index 0000000..cd6cb2c --- /dev/null +++ b/app/src/main/res/drawable/new_button_tv_color.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/new_ok_button.xml b/app/src/main/res/drawable/new_ok_button.xml new file mode 100644 index 0000000..e086ddf --- /dev/null +++ b/app/src/main/res/drawable/new_ok_button.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/radio_init1.xml b/app/src/main/res/drawable/radio_init1.xml new file mode 100644 index 0000000..371942c --- /dev/null +++ b/app/src/main/res/drawable/radio_init1.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/radio_init2.xml b/app/src/main/res/drawable/radio_init2.xml new file mode 100644 index 0000000..0579a44 --- /dev/null +++ b/app/src/main/res/drawable/radio_init2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rectangle.xml b/app/src/main/res/drawable/shape_rectangle.xml new file mode 100644 index 0000000..b82579f --- /dev/null +++ b/app/src/main/res/drawable/shape_rectangle.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/font/dakai.TTF b/app/src/main/res/font/dakai.TTF new file mode 100644 index 0000000..2af05b2 Binary files /dev/null and b/app/src/main/res/font/dakai.TTF differ diff --git a/app/src/main/res/font/simkai.ttf b/app/src/main/res/font/simkai.ttf new file mode 100644 index 0000000..d7c3b90 Binary files /dev/null and b/app/src/main/res/font/simkai.ttf differ diff --git a/app/src/main/res/layout/activity_add_food.xml b/app/src/main/res/layout/activity_add_food.xml new file mode 100644 index 0000000..dfb7059 --- /dev/null +++ b/app/src/main/res/layout/activity_add_food.xml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +