的
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="lecho.lib.hellocharts"
|
||||
android:versionCode="13"
|
||||
android:versionName="1.5.8" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="8"
|
||||
android:targetSdkVersion="22" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,104 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle'
|
||||
|
||||
//To upload to maven central
|
||||
//http://zserge.com/blog/gradle-maven-publish.html
|
||||
//gradle uploadArchives
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//compile fileTree(dir: 'libs', include: '*.jar')
|
||||
compile 'com.android.support:support-v4:23.4.0'
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION)
|
||||
buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION)
|
||||
targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION)
|
||||
versionCode Integer.parseInt(project.VERSION_CODE)
|
||||
versionName project.VERSION_NAME
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['src']
|
||||
aidl.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['assets']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Execute "gradle clean jarRelease" to cook jar.
|
||||
android.libraryVariants.all { variant ->
|
||||
def name = variant.buildType.name
|
||||
if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {
|
||||
return; // Skip debug builds.
|
||||
}
|
||||
def task = project.tasks.create "jar${name.capitalize()}", Jar
|
||||
task.dependsOn variant.javaCompile
|
||||
task.from variant.javaCompile.destinationDir
|
||||
artifacts.add('archives', task);
|
||||
}
|
||||
|
||||
// To publish to maven local execute "gradle clean build publishToMavenLocal"
|
||||
// To publish to nexus execute "gradle clean build publish"
|
||||
android.libraryVariants
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
artifact "${project.buildDir}/outputs/aar/${project.name}-release.aar"
|
||||
artifactId = POM_ARTIFACT_ID
|
||||
groupId = GROUP
|
||||
version = VERSION_NAME
|
||||
|
||||
// Task androidSourcesJar is provided by gradle-mvn-push.gradle
|
||||
//artifact androidSourcesJar {
|
||||
// classifier "sources"
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
credentials {
|
||||
username 'nexusUser'
|
||||
password 'nexusPass'
|
||||
}
|
||||
url "http://your-nexus-url/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
POM_NAME=HelloCharts Library for Android
|
||||
POM_ARTIFACT_ID=hellocharts-library
|
||||
POM_PACKAGING=aar
|
||||
Binary file not shown.
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>lecho.lib.hellocharts</groupId>
|
||||
<artifactId>hellocharts-library</artifactId>
|
||||
<name>HelloCharts Android</name>
|
||||
<version>1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<android.version>4.4</android.version>
|
||||
<support.version>19</support.version>
|
||||
</properties>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<!--Upload android.jar(sdk\platforms\android-19 with version 4.4 in pom.xml) and android-support-v4.jar(sdk\extras\android\support\v4 with version 19 in pom.xml)-->
|
||||
<!--to your local maven repository to use maven build.-->
|
||||
<dependency>
|
||||
<groupId>com.google.android</groupId>
|
||||
<artifactId>android</artifactId>
|
||||
<version>${android.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.android.support</groupId>
|
||||
<artifactId>support-v4</artifactId>
|
||||
<version>${support.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.jayway.maven.plugins.android.generation2</groupId>
|
||||
<artifactId>android-maven-plugin</artifactId>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache License Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
</project>
|
||||
@@ -0,0 +1,20 @@
|
||||
# To enable ProGuard in your project, edit project.properties
|
||||
# to define the proguard.config property as described in that file.
|
||||
#
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in ${sdk.dir}/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the ProGuard
|
||||
# include property in project.properties.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# 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 *;
|
||||
#}
|
||||
@@ -0,0 +1,15 @@
|
||||
# This file is automatically generated by Android Tools.
|
||||
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||
#
|
||||
# This file must be checked in Version Control Systems.
|
||||
#
|
||||
# To customize properties used by the Ant build system edit
|
||||
# "ant.properties", and override values to adapt the script to your
|
||||
# project structure.
|
||||
#
|
||||
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
|
||||
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
||||
|
||||
# Project target.
|
||||
target=android-21
|
||||
android.library=true
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
import java.util.EventListener;
|
||||
|
||||
/**
|
||||
* Listener used to listen for chart animation start and stop events. Implementations of this interface can be used for
|
||||
* all types of chart animations(data, viewport, PieChart rotation).
|
||||
*/
|
||||
public interface ChartAnimationListener extends EventListener {
|
||||
|
||||
public void onAnimationStarted();
|
||||
|
||||
public void onAnimationFinished();
|
||||
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
public interface ChartDataAnimator {
|
||||
|
||||
public static final long DEFAULT_ANIMATION_DURATION = 500;
|
||||
|
||||
public void startAnimation(long duration);
|
||||
|
||||
public void cancelAnimation();
|
||||
|
||||
public boolean isAnimationStarted();
|
||||
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener);
|
||||
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.Animator.AnimatorListener;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public class ChartDataAnimatorV14 implements ChartDataAnimator, AnimatorListener, AnimatorUpdateListener {
|
||||
private final Chart chart;
|
||||
private ValueAnimator animator;
|
||||
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
|
||||
|
||||
public ChartDataAnimatorV14(Chart chart) {
|
||||
this.chart = chart;
|
||||
animator = ValueAnimator.ofFloat(0.0f, 1.0f);
|
||||
animator.addListener(this);
|
||||
animator.addUpdateListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnimation(long duration) {
|
||||
if (duration >= 0) {
|
||||
animator.setDuration(duration);
|
||||
} else {
|
||||
animator.setDuration(DEFAULT_ANIMATION_DURATION);
|
||||
}
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAnimation() {
|
||||
animator.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
chart.animationDataUpdate(animation.getAnimatedFraction());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
chart.animationDataFinished();
|
||||
animationListener.onAnimationFinished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
animationListener.onAnimationStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnimationStarted() {
|
||||
return animator.isStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener) {
|
||||
if (null == animationListener) {
|
||||
this.animationListener = new DummyChartAnimationListener();
|
||||
} else {
|
||||
this.animationListener = animationListener;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
public class ChartDataAnimatorV8 implements ChartDataAnimator {
|
||||
|
||||
final Chart chart;
|
||||
final Handler handler;
|
||||
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
|
||||
long start;
|
||||
boolean isAnimationStarted = false;
|
||||
long duration;
|
||||
private final Runnable runnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long elapsed = SystemClock.uptimeMillis() - start;
|
||||
if (elapsed > duration) {
|
||||
isAnimationStarted = false;
|
||||
handler.removeCallbacks(runnable);
|
||||
chart.animationDataFinished();
|
||||
return;
|
||||
}
|
||||
float scale = Math.min(interpolator.getInterpolation((float) elapsed / duration), 1);
|
||||
chart.animationDataUpdate(scale);
|
||||
handler.postDelayed(this, 16);
|
||||
|
||||
}
|
||||
};
|
||||
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
|
||||
|
||||
public ChartDataAnimatorV8(Chart chart) {
|
||||
this.chart = chart;
|
||||
this.handler = new Handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnimation(long duration) {
|
||||
if (duration >= 0) {
|
||||
this.duration = duration;
|
||||
} else {
|
||||
this.duration = DEFAULT_ANIMATION_DURATION;
|
||||
}
|
||||
|
||||
isAnimationStarted = true;
|
||||
animationListener.onAnimationStarted();
|
||||
start = SystemClock.uptimeMillis();
|
||||
handler.post(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAnimation() {
|
||||
isAnimationStarted = false;
|
||||
handler.removeCallbacks(runnable);
|
||||
chart.animationDataFinished();
|
||||
animationListener.onAnimationFinished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnimationStarted() {
|
||||
return isAnimationStarted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener) {
|
||||
if (null == animationListener) {
|
||||
this.animationListener = new DummyChartAnimationListener();
|
||||
} else {
|
||||
this.animationListener = animationListener;
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
|
||||
public interface ChartViewportAnimator {
|
||||
|
||||
public static final int FAST_ANIMATION_DURATION = 300;
|
||||
|
||||
public void startAnimation(Viewport startViewport, Viewport targetViewport);
|
||||
|
||||
public void startAnimation(Viewport startViewport, Viewport targetViewport, long duration);
|
||||
|
||||
public void cancelAnimation();
|
||||
|
||||
public boolean isAnimationStarted();
|
||||
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener);
|
||||
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.Animator.AnimatorListener;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public class ChartViewportAnimatorV14 implements ChartViewportAnimator, AnimatorListener, AnimatorUpdateListener {
|
||||
private final Chart chart;
|
||||
private ValueAnimator animator;
|
||||
private Viewport startViewport = new Viewport();
|
||||
private Viewport targetViewport = new Viewport();
|
||||
private Viewport newViewport = new Viewport();
|
||||
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
|
||||
|
||||
public ChartViewportAnimatorV14(Chart chart) {
|
||||
this.chart = chart;
|
||||
animator = ValueAnimator.ofFloat(0.0f, 1.0f);
|
||||
animator.addListener(this);
|
||||
animator.addUpdateListener(this);
|
||||
animator.setDuration(FAST_ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnimation(Viewport startViewport, Viewport targetViewport) {
|
||||
this.startViewport.set(startViewport);
|
||||
this.targetViewport.set(targetViewport);
|
||||
animator.setDuration(FAST_ANIMATION_DURATION);
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnimation(Viewport startViewport, Viewport targetViewport, long duration) {
|
||||
this.startViewport.set(startViewport);
|
||||
this.targetViewport.set(targetViewport);
|
||||
animator.setDuration(duration);
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAnimation() {
|
||||
animator.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
float scale = animation.getAnimatedFraction();
|
||||
float diffLeft = (targetViewport.left - startViewport.left) * scale;
|
||||
float diffTop = (targetViewport.top - startViewport.top) * scale;
|
||||
float diffRight = (targetViewport.right - startViewport.right) * scale;
|
||||
float diffBottom = (targetViewport.bottom - startViewport.bottom) * scale;
|
||||
newViewport.set(startViewport.left + diffLeft, startViewport.top + diffTop, startViewport.right + diffRight,
|
||||
startViewport.bottom + diffBottom);
|
||||
chart.setCurrentViewport(newViewport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
chart.setCurrentViewport(targetViewport);
|
||||
animationListener.onAnimationFinished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
animationListener.onAnimationStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnimationStarted() {
|
||||
return animator.isStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener) {
|
||||
if (null == animationListener) {
|
||||
this.animationListener = new DummyChartAnimationListener();
|
||||
} else {
|
||||
this.animationListener = animationListener;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
public class ChartViewportAnimatorV8 implements ChartViewportAnimator {
|
||||
|
||||
final Chart chart;
|
||||
final Handler handler;
|
||||
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
|
||||
long start;
|
||||
boolean isAnimationStarted = false;
|
||||
private Viewport startViewport = new Viewport();
|
||||
private Viewport targetViewport = new Viewport();
|
||||
private Viewport newViewport = new Viewport();
|
||||
private long duration;
|
||||
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
|
||||
private final Runnable runnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long elapsed = SystemClock.uptimeMillis() - start;
|
||||
if (elapsed > duration) {
|
||||
isAnimationStarted = false;
|
||||
handler.removeCallbacks(runnable);
|
||||
chart.setCurrentViewport(targetViewport);
|
||||
animationListener.onAnimationFinished();
|
||||
return;
|
||||
}
|
||||
float scale = Math.min(interpolator.getInterpolation((float) elapsed / duration), 1);
|
||||
float diffLeft = (targetViewport.left - startViewport.left) * scale;
|
||||
float diffTop = (targetViewport.top - startViewport.top) * scale;
|
||||
float diffRight = (targetViewport.right - startViewport.right) * scale;
|
||||
float diffBottom = (targetViewport.bottom - startViewport.bottom) * scale;
|
||||
newViewport.set(startViewport.left + diffLeft, startViewport.top + diffTop,
|
||||
startViewport.right + diffRight, startViewport.bottom + diffBottom);
|
||||
chart.setCurrentViewport(newViewport);
|
||||
|
||||
handler.postDelayed(this, 16);
|
||||
}
|
||||
};
|
||||
|
||||
public ChartViewportAnimatorV8(Chart chart) {
|
||||
this.chart = chart;
|
||||
this.duration = FAST_ANIMATION_DURATION;
|
||||
this.handler = new Handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnimation(Viewport startViewport, Viewport targetViewport) {
|
||||
this.startViewport.set(startViewport);
|
||||
this.targetViewport.set(targetViewport);
|
||||
duration = FAST_ANIMATION_DURATION;
|
||||
isAnimationStarted = true;
|
||||
animationListener.onAnimationStarted();
|
||||
start = SystemClock.uptimeMillis();
|
||||
handler.post(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnimation(Viewport startViewport, Viewport targetViewport, long duration) {
|
||||
this.startViewport.set(startViewport);
|
||||
this.targetViewport.set(targetViewport);
|
||||
this.duration = duration;
|
||||
isAnimationStarted = true;
|
||||
animationListener.onAnimationStarted();
|
||||
start = SystemClock.uptimeMillis();
|
||||
handler.post(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAnimation() {
|
||||
isAnimationStarted = false;
|
||||
handler.removeCallbacks(runnable);
|
||||
chart.setCurrentViewport(targetViewport);
|
||||
animationListener.onAnimationFinished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnimationStarted() {
|
||||
return isAnimationStarted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener) {
|
||||
if (null == animationListener) {
|
||||
this.animationListener = new DummyChartAnimationListener();
|
||||
} else {
|
||||
this.animationListener = animationListener;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
public class DummyChartAnimationListener implements ChartAnimationListener {
|
||||
|
||||
@Override
|
||||
public void onAnimationStarted() {
|
||||
// do nothing
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationFinished() {
|
||||
// do nothing
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
public interface PieChartRotationAnimator {
|
||||
|
||||
public static final int FAST_ANIMATION_DURATION = 200;
|
||||
|
||||
public void startAnimation(float startAngle, float angleToRotate);
|
||||
|
||||
public void cancelAnimation();
|
||||
|
||||
public boolean isAnimationStarted();
|
||||
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener);
|
||||
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.Animator.AnimatorListener;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import lecho.lib.hellocharts.view.PieChartView;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public class PieChartRotationAnimatorV14 implements PieChartRotationAnimator, AnimatorListener, AnimatorUpdateListener {
|
||||
private final PieChartView chart;
|
||||
private ValueAnimator animator;
|
||||
private float startRotation = 0;
|
||||
private float targetRotation = 0;
|
||||
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
|
||||
|
||||
public PieChartRotationAnimatorV14(PieChartView chart) {
|
||||
this(chart, FAST_ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
public PieChartRotationAnimatorV14(PieChartView chart, long duration) {
|
||||
this.chart = chart;
|
||||
animator = ValueAnimator.ofFloat(0.0f, 1.0f);
|
||||
animator.setDuration(duration);
|
||||
animator.addListener(this);
|
||||
animator.addUpdateListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnimation(float startRotation, float targetRotation) {
|
||||
this.startRotation = (startRotation % 360 + 360) % 360;
|
||||
this.targetRotation = (targetRotation % 360 + 360) % 360;
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAnimation() {
|
||||
animator.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
float scale = animation.getAnimatedFraction();
|
||||
float rotation = startRotation + (targetRotation - startRotation) * scale;
|
||||
rotation = (rotation % 360 + 360) % 360;
|
||||
chart.setChartRotation((int) rotation, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
chart.setChartRotation((int) targetRotation, false);
|
||||
animationListener.onAnimationFinished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
animationListener.onAnimationStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnimationStarted() {
|
||||
return animator.isStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener) {
|
||||
if (null == animationListener) {
|
||||
this.animationListener = new DummyChartAnimationListener();
|
||||
} else {
|
||||
this.animationListener = animationListener;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package lecho.lib.hellocharts.animation;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import lecho.lib.hellocharts.view.PieChartView;
|
||||
|
||||
public class PieChartRotationAnimatorV8 implements PieChartRotationAnimator {
|
||||
|
||||
final PieChartView chart;
|
||||
final long duration;
|
||||
final Handler handler;
|
||||
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
|
||||
long start;
|
||||
boolean isAnimationStarted = false;
|
||||
private float startRotation = 0;
|
||||
private float targetRotation = 0;
|
||||
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
|
||||
private final Runnable runnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long elapsed = SystemClock.uptimeMillis() - start;
|
||||
if (elapsed > duration) {
|
||||
isAnimationStarted = false;
|
||||
handler.removeCallbacks(runnable);
|
||||
chart.setChartRotation((int) targetRotation, false);
|
||||
animationListener.onAnimationFinished();
|
||||
return;
|
||||
}
|
||||
float scale = Math.min(interpolator.getInterpolation((float) elapsed / duration), 1);
|
||||
float rotation = startRotation + (targetRotation - startRotation) * scale;
|
||||
rotation = (rotation % 360 + 360) % 360;
|
||||
chart.setChartRotation((int) rotation, false);
|
||||
handler.postDelayed(this, 16);
|
||||
}
|
||||
};
|
||||
|
||||
public PieChartRotationAnimatorV8(PieChartView chart) {
|
||||
this(chart, FAST_ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
public PieChartRotationAnimatorV8(PieChartView chart, long duration) {
|
||||
this.chart = chart;
|
||||
this.duration = duration;
|
||||
this.handler = new Handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnimation(float startRotation, float targetRotation) {
|
||||
this.startRotation = (startRotation % 360 + 360) % 360;
|
||||
this.targetRotation = (targetRotation % 360 + 360) % 360;
|
||||
isAnimationStarted = true;
|
||||
animationListener.onAnimationStarted();
|
||||
start = SystemClock.uptimeMillis();
|
||||
handler.post(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAnimation() {
|
||||
isAnimationStarted = false;
|
||||
handler.removeCallbacks(runnable);
|
||||
chart.setChartRotation((int) targetRotation, false);
|
||||
animationListener.onAnimationFinished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnimationStarted() {
|
||||
return isAnimationStarted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChartAnimationListener(ChartAnimationListener animationListener) {
|
||||
if (null == animationListener) {
|
||||
this.animationListener = new DummyChartAnimationListener();
|
||||
} else {
|
||||
this.animationListener = animationListener;
|
||||
}
|
||||
}
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
package lecho.lib.hellocharts.computator;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import lecho.lib.hellocharts.listener.DummyVieportChangeListener;
|
||||
import lecho.lib.hellocharts.listener.ViewportChangeListener;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
|
||||
/**
|
||||
* Computes raw points coordinates(in pixels), holds content area dimensions and chart viewport.
|
||||
*/
|
||||
public class ChartComputator {
|
||||
|
||||
/**
|
||||
* Maximum chart zoom.
|
||||
*/
|
||||
protected static final float DEFAULT_MAXIMUM_ZOOM = 20f;
|
||||
protected float maxZoom = DEFAULT_MAXIMUM_ZOOM;
|
||||
protected int chartWidth;
|
||||
protected int chartHeight;
|
||||
//contentRectMinusAllMargins <= contentRectMinusAxesMargins <= maxContentRect
|
||||
protected Rect contentRectMinusAllMargins = new Rect();
|
||||
protected Rect contentRectMinusAxesMargins = new Rect();
|
||||
protected Rect maxContentRect = new Rect();
|
||||
/**
|
||||
* This rectangle represents the currently visible chart values ranges. The currently visible chart X values are
|
||||
* from this rectangle's left to its right. The currently visible chart Y values are from this rectangle's top to
|
||||
* its bottom.
|
||||
*/
|
||||
protected Viewport currentViewport = new Viewport();
|
||||
protected Viewport maxViewport = new Viewport();
|
||||
protected float minViewportWidth;
|
||||
protected float minViewportHeight;
|
||||
/**
|
||||
* Warning! Viewport listener is disabled for all charts beside preview charts to avoid additional method calls
|
||||
* during animations.
|
||||
*/
|
||||
protected ViewportChangeListener viewportChangeListener = new DummyVieportChangeListener();
|
||||
|
||||
/**
|
||||
* Calculates available width and height. Should be called when chart dimensions change. ContentRect is relative to
|
||||
* chart view not the device's screen.
|
||||
*/
|
||||
public void setContentRect(int width, int height, int paddingLeft, int paddingTop, int paddingRight,
|
||||
int paddingBottom) {
|
||||
chartWidth = width;
|
||||
chartHeight = height;
|
||||
maxContentRect.set(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom);
|
||||
contentRectMinusAxesMargins.set(maxContentRect);
|
||||
contentRectMinusAllMargins.set(maxContentRect);
|
||||
}
|
||||
|
||||
public void resetContentRect() {
|
||||
contentRectMinusAxesMargins.set(maxContentRect);
|
||||
contentRectMinusAllMargins.set(maxContentRect);
|
||||
}
|
||||
|
||||
public void insetContentRect(int deltaLeft, int deltaTop, int deltaRight, int deltaBottom) {
|
||||
contentRectMinusAxesMargins.left = contentRectMinusAxesMargins.left + deltaLeft;
|
||||
contentRectMinusAxesMargins.top = contentRectMinusAxesMargins.top + deltaTop;
|
||||
contentRectMinusAxesMargins.right = contentRectMinusAxesMargins.right - deltaRight;
|
||||
contentRectMinusAxesMargins.bottom = contentRectMinusAxesMargins.bottom - deltaBottom;
|
||||
|
||||
insetContentRectByInternalMargins(deltaLeft, deltaTop, deltaRight, deltaBottom);
|
||||
}
|
||||
|
||||
public void insetContentRectByInternalMargins(int deltaLeft, int deltaTop, int deltaRight, int deltaBottom) {
|
||||
contentRectMinusAllMargins.left = contentRectMinusAllMargins.left + deltaLeft;
|
||||
contentRectMinusAllMargins.top = contentRectMinusAllMargins.top + deltaTop;
|
||||
contentRectMinusAllMargins.right = contentRectMinusAllMargins.right - deltaRight;
|
||||
contentRectMinusAllMargins.bottom = contentRectMinusAllMargins.bottom - deltaBottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if new viewport doesn't exceed max available viewport.
|
||||
*/
|
||||
public void constrainViewport(float left, float top, float right, float bottom) {
|
||||
|
||||
if (right - left < minViewportWidth) {
|
||||
// Minimum width - constrain horizontal zoom!
|
||||
right = left + minViewportWidth;
|
||||
if (left < maxViewport.left) {
|
||||
left = maxViewport.left;
|
||||
right = left + minViewportWidth;
|
||||
} else if (right > maxViewport.right) {
|
||||
right = maxViewport.right;
|
||||
left = right - minViewportWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (top - bottom < minViewportHeight) {
|
||||
// Minimum height - constrain vertical zoom!
|
||||
bottom = top - minViewportHeight;
|
||||
if (top > maxViewport.top) {
|
||||
top = maxViewport.top;
|
||||
bottom = top - minViewportHeight;
|
||||
} else if (bottom < maxViewport.bottom) {
|
||||
bottom = maxViewport.bottom;
|
||||
top = bottom + minViewportHeight;
|
||||
}
|
||||
}
|
||||
|
||||
currentViewport.left = Math.max(maxViewport.left, left);
|
||||
currentViewport.top = Math.min(maxViewport.top, top);
|
||||
currentViewport.right = Math.min(maxViewport.right, right);
|
||||
currentViewport.bottom = Math.max(maxViewport.bottom, bottom);
|
||||
|
||||
viewportChangeListener.onViewportChanged(currentViewport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current viewport (defined by {@link #currentViewport}) to the given X and Y positions.
|
||||
*/
|
||||
public void setViewportTopLeft(float left, float top) {
|
||||
/**
|
||||
* Constrains within the scroll range. The scroll range is simply the viewport extremes (AXIS_X_MAX,
|
||||
* etc.) minus
|
||||
* the viewport size. For example, if the extrema were 0 and 10, and the viewport size was 2, the scroll range
|
||||
* would be 0 to 8.
|
||||
*/
|
||||
|
||||
final float curWidth = currentViewport.width();
|
||||
final float curHeight = currentViewport.height();
|
||||
|
||||
left = Math.max(maxViewport.left, Math.min(left, maxViewport.right - curWidth));
|
||||
top = Math.max(maxViewport.bottom + curHeight, Math.min(top, maxViewport.top));
|
||||
constrainViewport(left, top, left + curWidth, top - curHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates chart value into raw pixel value. Returned value is absolute pixel X coordinate. If this method
|
||||
* return
|
||||
* 0 that means left most pixel of the screen.
|
||||
*/
|
||||
public float computeRawX(float valueX) {
|
||||
// TODO: (contentRectMinusAllMargins.width() / currentViewport.width()) can be recalculated only when viewport
|
||||
// change.
|
||||
final float pixelOffset = (valueX - currentViewport.left) * (contentRectMinusAllMargins.width() /
|
||||
currentViewport.width());
|
||||
return contentRectMinusAllMargins.left + pixelOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates chart value into raw pixel value. Returned value is absolute pixel Y coordinate. If this method
|
||||
* return
|
||||
* 0 that means top most pixel of the screen.
|
||||
*/
|
||||
public float computeRawY(float valueY) {
|
||||
final float pixelOffset = (valueY - currentViewport.bottom) * (contentRectMinusAllMargins.height() /
|
||||
currentViewport.height());
|
||||
return contentRectMinusAllMargins.bottom - pixelOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates viewport distance int pixel distance for X coordinates.
|
||||
*/
|
||||
public float computeRawDistanceX(float distance) {
|
||||
return distance * (contentRectMinusAllMargins.width() / currentViewport.width());
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates viewport distance int pixel distance for X coordinates.
|
||||
*/
|
||||
public float computeRawDistanceY(float distance) {
|
||||
return distance * (contentRectMinusAllMargins.height() / currentViewport.height());
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the chart point (i.e. within the chart's domain and range) represented by the given pixel coordinates, if
|
||||
* that pixel is within the chart region described by {@link #contentRectMinusAllMargins}. If the point is found,
|
||||
* the "dest"
|
||||
* argument is set to the point and this function returns true. Otherwise, this function returns false and
|
||||
* "dest" is
|
||||
* unchanged.
|
||||
*/
|
||||
public boolean rawPixelsToDataPoint(float x, float y, PointF dest) {
|
||||
if (!contentRectMinusAllMargins.contains((int) x, (int) y)) {
|
||||
return false;
|
||||
}
|
||||
dest.set(currentViewport.left + (x - contentRectMinusAllMargins.left) * currentViewport.width() /
|
||||
contentRectMinusAllMargins.width(),
|
||||
currentViewport.bottom + (y - contentRectMinusAllMargins.bottom) * currentViewport.height() /
|
||||
-contentRectMinusAllMargins.height());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the current scrollable surface size, in pixels. For example, if the entire chart area is visible, this
|
||||
* is simply the current size of {@link #contentRectMinusAllMargins}. If the chart is zoomed in 200% in both
|
||||
* directions, the
|
||||
* returned size will be twice as large horizontally and vertically.
|
||||
*/
|
||||
public void computeScrollSurfaceSize(Point out) {
|
||||
out.set((int) (maxViewport.width() * contentRectMinusAllMargins.width() / currentViewport.width()),
|
||||
(int) (maxViewport.height() * contentRectMinusAllMargins.height() / currentViewport.height()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given coordinates lies inside contentRectMinusAllMargins.
|
||||
*/
|
||||
public boolean isWithinContentRect(float x, float y, float precision) {
|
||||
if (x >= contentRectMinusAllMargins.left - precision && x <= contentRectMinusAllMargins.right + precision) {
|
||||
if (y <= contentRectMinusAllMargins.bottom + precision && y >= contentRectMinusAllMargins.top -
|
||||
precision) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns content rectangle in pixels.
|
||||
*
|
||||
* @see #setContentRect(int, int, int, int, int, int)
|
||||
*/
|
||||
public Rect getContentRectMinusAllMargins() {
|
||||
return contentRectMinusAllMargins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns content rectangle with chart internal margins, for example for LineChart contentRectMinusAxesMargins is
|
||||
* bigger
|
||||
* than contentRectMinusAllMargins by point radius, thanks to that points are not cut on edges.
|
||||
*
|
||||
* @see #setContentRect(int, int, int, int, int, int)
|
||||
*/
|
||||
public Rect getContentRectMinusAxesMargins() {
|
||||
return contentRectMinusAxesMargins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current chart viewport, returned object is mutable but should not be modified.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Viewport getCurrentViewport() {
|
||||
return currentViewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current viewport to the same values as viewport passed in parameter. This method use deep copy so parameter
|
||||
* can be safely modified later. Current viewport must be equal or smaller than maximum viewport.
|
||||
*
|
||||
* @param viewport
|
||||
*/
|
||||
public void setCurrentViewport(Viewport viewport) {
|
||||
constrainViewport(viewport.left, viewport.top, viewport.right, viewport.bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new values for curent viewport, that will change what part of chart is visible. Current viewport must be
|
||||
* equal or smaller than maximum viewport.
|
||||
*/
|
||||
public void setCurrentViewport(float left, float top, float right, float bottom) {
|
||||
constrainViewport(left, top, right, bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns maximum viewport - values ranges extremes.
|
||||
*/
|
||||
public Viewport getMaximumViewport() {
|
||||
return maxViewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum viewport to the same values as viewport passed in parameter. This method use deep copy so parameter
|
||||
* can be safely modified later.
|
||||
*
|
||||
* @param maxViewport
|
||||
*/
|
||||
public void setMaxViewport(Viewport maxViewport) {
|
||||
setMaxViewport(maxViewport.left, maxViewport.top, maxViewport.right, maxViewport.bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new values for maximum viewport, that will change what part of chart is visible.
|
||||
*/
|
||||
public void setMaxViewport(float left, float top, float right, float bottom) {
|
||||
this.maxViewport.set(left, top, right, bottom);
|
||||
computeMinimumWidthAndHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns viewport for visible part of chart, for most charts it is equal to current viewport.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Viewport getVisibleViewport() {
|
||||
return currentViewport;
|
||||
}
|
||||
|
||||
public void setVisibleViewport(Viewport visibleViewport) {
|
||||
setCurrentViewport(visibleViewport);
|
||||
}
|
||||
|
||||
public float getMinimumViewportWidth() {
|
||||
return minViewportWidth;
|
||||
}
|
||||
|
||||
public float getMinimumViewportHeight() {
|
||||
return minViewportHeight;
|
||||
}
|
||||
|
||||
public void setViewportChangeListener(ViewportChangeListener viewportChangeListener) {
|
||||
if (null == viewportChangeListener) {
|
||||
this.viewportChangeListener = new DummyVieportChangeListener();
|
||||
} else {
|
||||
this.viewportChangeListener = viewportChangeListener;
|
||||
}
|
||||
}
|
||||
|
||||
public int getChartWidth() {
|
||||
return chartWidth;
|
||||
}
|
||||
|
||||
public int getChartHeight() {
|
||||
return chartHeight;
|
||||
}
|
||||
|
||||
public float getMaxZoom() {
|
||||
return maxZoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum zoom level, default is 20.
|
||||
*
|
||||
* @param maxZoom
|
||||
*/
|
||||
public void setMaxZoom(float maxZoom) {
|
||||
if (maxZoom < 1) {
|
||||
maxZoom = 1;
|
||||
}
|
||||
|
||||
this.maxZoom = maxZoom;
|
||||
|
||||
computeMinimumWidthAndHeight();
|
||||
|
||||
setCurrentViewport(currentViewport);
|
||||
|
||||
}
|
||||
|
||||
private void computeMinimumWidthAndHeight() {
|
||||
minViewportWidth = this.maxViewport.width() / maxZoom;
|
||||
minViewportHeight = this.maxViewport.height() / maxZoom;
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package lecho.lib.hellocharts.computator;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
|
||||
/**
|
||||
* Version of ChartComputator for preview charts. It always uses maxViewport as visible viewport and currentViewport as
|
||||
* preview area.
|
||||
*/
|
||||
public class PreviewChartComputator extends ChartComputator {
|
||||
|
||||
public float computeRawX(float valueX) {
|
||||
final float pixelOffset = (valueX - maxViewport.left) * (contentRectMinusAllMargins.width() / maxViewport
|
||||
.width());
|
||||
return contentRectMinusAllMargins.left + pixelOffset;
|
||||
}
|
||||
|
||||
public float computeRawY(float valueY) {
|
||||
final float pixelOffset = (valueY - maxViewport.bottom) * (contentRectMinusAllMargins.height() / maxViewport
|
||||
.height());
|
||||
return contentRectMinusAllMargins.bottom - pixelOffset;
|
||||
}
|
||||
|
||||
public Viewport getVisibleViewport() {
|
||||
return maxViewport;
|
||||
}
|
||||
|
||||
public void setVisibleViewport(Viewport visibleViewport) {
|
||||
setMaxViewport(visibleViewport);
|
||||
}
|
||||
|
||||
public void constrainViewport(float left, float top, float right, float bottom) {
|
||||
super.constrainViewport(left, top, right, bottom);
|
||||
viewportChangeListener.onViewportChanged(currentViewport);
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.AxisValue;
|
||||
|
||||
public interface AxisValueFormatter {
|
||||
|
||||
/**
|
||||
* Formats AxisValue for manual(custom) axis. Result is stored in (output) formattedValue array. Method
|
||||
* returns number of chars of formatted value. The formatted value starts at index [formattedValue.length -
|
||||
* charsNumber] and ends at index [formattedValue.length-1].
|
||||
*/
|
||||
public int formatValueForManualAxis(char[] formattedValue, AxisValue axisValue);
|
||||
|
||||
/**
|
||||
* Used only for auto-generated axes. If you are not going to use your implementation for aut-generated axes you can
|
||||
* skip implementation of this method and just return 0. SFormats values with given number of digits after
|
||||
* decimal separator. Result is stored in given array. Method returns number of chars for formatted value. The
|
||||
* formatted value starts at index [formattedValue.length - charsNumber] and ends at index [formattedValue
|
||||
* .length-1].
|
||||
*/
|
||||
public int formatValueForAutoGeneratedAxis(char[] formattedValue, float value, int autoDecimalDigits);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import lecho.lib.hellocharts.model.BubbleValue;
|
||||
|
||||
public interface BubbleChartValueFormatter {
|
||||
|
||||
public int formatChartValue(char[] formattedValue, BubbleValue value);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
|
||||
public interface ColumnChartValueFormatter {
|
||||
|
||||
public int formatChartValue(char[] formattedValue, SubcolumnValue value);
|
||||
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
|
||||
public interface LineChartValueFormatter {
|
||||
|
||||
public int formatChartValue(char[] formattedValue, PointValue value);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import lecho.lib.hellocharts.model.SliceValue;
|
||||
|
||||
public interface PieChartValueFormatter {
|
||||
|
||||
public int formatChartValue(char[] formattedValue, SliceValue value);
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import lecho.lib.hellocharts.model.AxisValue;
|
||||
|
||||
public class SimpleAxisValueFormatter implements AxisValueFormatter {
|
||||
|
||||
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
|
||||
|
||||
public SimpleAxisValueFormatter() {
|
||||
valueFormatterHelper.determineDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimpleAxisValueFormatter(int decimalDigitsNumber) {
|
||||
this();
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int formatValueForManualAxis(char[] formattedValue, AxisValue axisValue) {
|
||||
final int charsNumber = valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue,
|
||||
axisValue.getValue(), axisValue.getLabelAsChars());
|
||||
return charsNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int formatValueForAutoGeneratedAxis(char[] formattedValue, float value, int autoDecimalDigits) {
|
||||
final int charsNumber = valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue,
|
||||
value, autoDecimalDigits);
|
||||
return charsNumber;
|
||||
}
|
||||
|
||||
public int getDecimalDigitsNumber() {
|
||||
return valueFormatterHelper.getDecimalDigitsNumber();
|
||||
}
|
||||
|
||||
public SimpleAxisValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getAppendedText() {
|
||||
return valueFormatterHelper.getAppendedText();
|
||||
}
|
||||
|
||||
public SimpleAxisValueFormatter setAppendedText(char[] appendedText) {
|
||||
valueFormatterHelper.setAppendedText(appendedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getPrependedText() {
|
||||
return valueFormatterHelper.getPrependedText();
|
||||
}
|
||||
|
||||
public SimpleAxisValueFormatter setPrependedText(char[] prependedText) {
|
||||
valueFormatterHelper.setPrependedText(prependedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char getDecimalSeparator() {
|
||||
return valueFormatterHelper.getDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimpleAxisValueFormatter setDecimalSeparator(char decimalSeparator) {
|
||||
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import lecho.lib.hellocharts.model.BubbleValue;
|
||||
|
||||
|
||||
public class SimpleBubbleChartValueFormatter implements BubbleChartValueFormatter {
|
||||
|
||||
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
|
||||
|
||||
public SimpleBubbleChartValueFormatter() {
|
||||
valueFormatterHelper.determineDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimpleBubbleChartValueFormatter(int decimalDigitsNumber) {
|
||||
this();
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int formatChartValue(char[] formattedValue, BubbleValue value) {
|
||||
return valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue, value.getZ(), value
|
||||
.getLabelAsChars());
|
||||
}
|
||||
|
||||
public int getDecimalDigitsNumber() {
|
||||
return valueFormatterHelper.getDecimalDigitsNumber();
|
||||
}
|
||||
|
||||
public SimpleBubbleChartValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getAppendedText() {
|
||||
return valueFormatterHelper.getAppendedText();
|
||||
}
|
||||
|
||||
public SimpleBubbleChartValueFormatter setAppendedText(char[] appendedText) {
|
||||
valueFormatterHelper.setAppendedText(appendedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getPrependedText() {
|
||||
return valueFormatterHelper.getPrependedText();
|
||||
}
|
||||
|
||||
public SimpleBubbleChartValueFormatter setPrependedText(char[] prependedText) {
|
||||
valueFormatterHelper.setPrependedText(prependedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char getDecimalSeparator() {
|
||||
return valueFormatterHelper.getDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimpleBubbleChartValueFormatter setDecimalSeparator(char decimalSeparator) {
|
||||
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
|
||||
public class SimpleColumnChartValueFormatter implements ColumnChartValueFormatter {
|
||||
|
||||
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
|
||||
|
||||
public SimpleColumnChartValueFormatter() {
|
||||
valueFormatterHelper.determineDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimpleColumnChartValueFormatter(int decimalDigitsNumber) {
|
||||
this();
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int formatChartValue(char[] formattedValue, SubcolumnValue value) {
|
||||
return valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue, value.getValue(),
|
||||
value.getLabelAsChars());
|
||||
}
|
||||
|
||||
public int getDecimalDigitsNumber() {
|
||||
return valueFormatterHelper.getDecimalDigitsNumber();
|
||||
}
|
||||
|
||||
public SimpleColumnChartValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getAppendedText() {
|
||||
return valueFormatterHelper.getAppendedText();
|
||||
}
|
||||
|
||||
public SimpleColumnChartValueFormatter setAppendedText(char[] appendedText) {
|
||||
valueFormatterHelper.setAppendedText(appendedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getPrependedText() {
|
||||
return valueFormatterHelper.getPrependedText();
|
||||
}
|
||||
|
||||
public SimpleColumnChartValueFormatter setPrependedText(char[] prependedText) {
|
||||
valueFormatterHelper.setPrependedText(prependedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char getDecimalSeparator() {
|
||||
return valueFormatterHelper.getDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimpleColumnChartValueFormatter setDecimalSeparator(char decimalSeparator) {
|
||||
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
|
||||
public class SimpleLineChartValueFormatter implements LineChartValueFormatter {
|
||||
|
||||
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
|
||||
|
||||
public SimpleLineChartValueFormatter() {
|
||||
valueFormatterHelper.determineDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimpleLineChartValueFormatter(int decimalDigitsNumber) {
|
||||
this();
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int formatChartValue(char[] formattedValue, PointValue value) {
|
||||
return valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue, value.getY(), value
|
||||
.getLabelAsChars());
|
||||
}
|
||||
|
||||
public int getDecimalDigitsNumber() {
|
||||
return valueFormatterHelper.getDecimalDigitsNumber();
|
||||
}
|
||||
|
||||
public SimpleLineChartValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getAppendedText() {
|
||||
return valueFormatterHelper.getAppendedText();
|
||||
}
|
||||
|
||||
public SimpleLineChartValueFormatter setAppendedText(char[] appendedText) {
|
||||
valueFormatterHelper.setAppendedText(appendedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getPrependedText() {
|
||||
return valueFormatterHelper.getPrependedText();
|
||||
}
|
||||
|
||||
public SimpleLineChartValueFormatter setPrependedText(char[] prependedText) {
|
||||
valueFormatterHelper.setPrependedText(prependedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char getDecimalSeparator() {
|
||||
return valueFormatterHelper.getDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimpleLineChartValueFormatter setDecimalSeparator(char decimalSeparator) {
|
||||
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import lecho.lib.hellocharts.model.SliceValue;
|
||||
|
||||
|
||||
public class SimplePieChartValueFormatter implements PieChartValueFormatter {
|
||||
|
||||
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
|
||||
|
||||
public SimplePieChartValueFormatter() {
|
||||
valueFormatterHelper.determineDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimplePieChartValueFormatter(int decimalDigitsNumber) {
|
||||
this();
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int formatChartValue(char[] formattedValue, SliceValue value) {
|
||||
return valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue, value.getValue(),
|
||||
value.getLabelAsChars());
|
||||
}
|
||||
|
||||
public int getDecimalDigitsNumber() {
|
||||
return valueFormatterHelper.getDecimalDigitsNumber();
|
||||
}
|
||||
|
||||
public SimplePieChartValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
|
||||
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getAppendedText() {
|
||||
return valueFormatterHelper.getAppendedText();
|
||||
}
|
||||
|
||||
public SimplePieChartValueFormatter setAppendedText(char[] appendedText) {
|
||||
valueFormatterHelper.setAppendedText(appendedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getPrependedText() {
|
||||
return valueFormatterHelper.getPrependedText();
|
||||
}
|
||||
|
||||
public SimplePieChartValueFormatter setPrependedText(char[] prependedText) {
|
||||
valueFormatterHelper.setPrependedText(prependedText);
|
||||
return this;
|
||||
}
|
||||
|
||||
public char getDecimalSeparator() {
|
||||
return valueFormatterHelper.getDecimalSeparator();
|
||||
}
|
||||
|
||||
public SimplePieChartValueFormatter setDecimalSeparator(char decimalSeparator) {
|
||||
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package lecho.lib.hellocharts.formatter;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import lecho.lib.hellocharts.util.FloatUtils;
|
||||
|
||||
public class ValueFormatterHelper {
|
||||
public static final int DEFAULT_DIGITS_NUMBER = 0;
|
||||
private static final String TAG = "ValueFormatterHelper";
|
||||
private int decimalDigitsNumber = Integer.MIN_VALUE;
|
||||
private char[] appendedText = new char[0];
|
||||
private char[] prependedText = new char[0];
|
||||
private char decimalSeparator = '.';
|
||||
|
||||
public void determineDecimalSeparator() {
|
||||
NumberFormat numberFormat = NumberFormat.getInstance();
|
||||
if (numberFormat instanceof DecimalFormat) {
|
||||
decimalSeparator = ((DecimalFormat) numberFormat).getDecimalFormatSymbols().getDecimalSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
public int getDecimalDigitsNumber() {
|
||||
return decimalDigitsNumber;
|
||||
}
|
||||
|
||||
public ValueFormatterHelper setDecimalDigitsNumber(int decimalDigitsNumber) {
|
||||
this.decimalDigitsNumber = decimalDigitsNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getAppendedText() {
|
||||
return appendedText;
|
||||
}
|
||||
|
||||
public ValueFormatterHelper setAppendedText(char[] appendedText) {
|
||||
if (null != appendedText) {
|
||||
this.appendedText = appendedText;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getPrependedText() {
|
||||
return prependedText;
|
||||
}
|
||||
|
||||
public ValueFormatterHelper setPrependedText(char[] prependedText) {
|
||||
if (null != prependedText) {
|
||||
this.prependedText = prependedText;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public char getDecimalSeparator() {
|
||||
return decimalSeparator;
|
||||
}
|
||||
|
||||
public ValueFormatterHelper setDecimalSeparator(char decimalSeparator) {
|
||||
char nullChar = '\0';
|
||||
if (nullChar != decimalSeparator) {
|
||||
this.decimalSeparator = decimalSeparator;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats float value. Result is stored in (output) formattedValue array. Method
|
||||
* returns number of chars of formatted value. The formatted value starts at index [formattedValue.length -
|
||||
* charsNumber] and ends at index [formattedValue.length-1].
|
||||
* Note: If label is not null it will be used as formattedValue instead of float value.
|
||||
* Note: Parameter defaultDigitsNumber is used only if you didn't change decimalDigintsNumber value using
|
||||
* method {@link #setDecimalDigitsNumber(int)}.
|
||||
*/
|
||||
public int formatFloatValueWithPrependedAndAppendedText(char[] formattedValue, float value, int
|
||||
defaultDigitsNumber, char[] label) {
|
||||
if (null != label) {
|
||||
// If custom label is not null use only name characters as formatted value.
|
||||
// Copy label into formatted value array.
|
||||
int labelLength = label.length;
|
||||
if (labelLength > formattedValue.length) {
|
||||
Log.w(TAG, "Label length is larger than buffer size(64chars), some chars will be skipped!");
|
||||
labelLength = formattedValue.length;
|
||||
}
|
||||
System.arraycopy(label, 0, formattedValue, formattedValue.length - labelLength, labelLength);
|
||||
return labelLength;
|
||||
}
|
||||
|
||||
final int appliedDigitsNumber = getAppliedDecimalDigitsNumber(defaultDigitsNumber);
|
||||
final int charsNumber = formatFloatValue(formattedValue, value, appliedDigitsNumber);
|
||||
appendText(formattedValue);
|
||||
prependText(formattedValue, charsNumber);
|
||||
return charsNumber + getPrependedText().length + getAppendedText().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #formatFloatValueWithPrependedAndAppendedText(char[], float, int, char[])
|
||||
*/
|
||||
public int formatFloatValueWithPrependedAndAppendedText(char[] formattedValue, float value, char[] label) {
|
||||
return formatFloatValueWithPrependedAndAppendedText(formattedValue, value, DEFAULT_DIGITS_NUMBER, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #formatFloatValueWithPrependedAndAppendedText(char[], float, int, char[])
|
||||
*/
|
||||
public int formatFloatValueWithPrependedAndAppendedText(char[] formattedValue, float value, int
|
||||
defaultDigitsNumber) {
|
||||
return formatFloatValueWithPrependedAndAppendedText(formattedValue, value, defaultDigitsNumber, null);
|
||||
}
|
||||
|
||||
public int formatFloatValue(char[] formattedValue, float value, int decimalDigitsNumber) {
|
||||
return FloatUtils.formatFloat(formattedValue, value, formattedValue.length - appendedText.length,
|
||||
decimalDigitsNumber,
|
||||
decimalSeparator);
|
||||
}
|
||||
|
||||
public void appendText(char[] formattedValue) {
|
||||
if (appendedText.length > 0) {
|
||||
System.arraycopy(appendedText, 0, formattedValue, formattedValue.length - appendedText.length,
|
||||
appendedText.length);
|
||||
}
|
||||
}
|
||||
|
||||
public void prependText(char[] formattedValue, int charsNumber) {
|
||||
if (prependedText.length > 0) {
|
||||
System.arraycopy(prependedText, 0, formattedValue, formattedValue.length - charsNumber - appendedText.length
|
||||
- prependedText.length, prependedText.length);
|
||||
}
|
||||
}
|
||||
|
||||
public int getAppliedDecimalDigitsNumber(int defaultDigitsNumber) {
|
||||
final int appliedDecimalDigitsNumber;
|
||||
if (decimalDigitsNumber < 0) {
|
||||
//When decimalDigitsNumber < 0 that means that user didn't set that value and defaultDigitsNumber should
|
||||
// be used.
|
||||
appliedDecimalDigitsNumber = defaultDigitsNumber;
|
||||
} else {
|
||||
appliedDecimalDigitsNumber = decimalDigitsNumber;
|
||||
}
|
||||
return appliedDecimalDigitsNumber;
|
||||
}
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package lecho.lib.hellocharts.gesture;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.support.v4.widget.ScrollerCompat;
|
||||
|
||||
import lecho.lib.hellocharts.computator.ChartComputator;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
|
||||
/**
|
||||
* Encapsulates scrolling functionality.
|
||||
*/
|
||||
public class ChartScroller {
|
||||
|
||||
private Viewport scrollerStartViewport = new Viewport(); // Used only for zooms and flings
|
||||
private Point surfaceSizeBuffer = new Point();// Used for scroll and flings
|
||||
private ScrollerCompat scroller;
|
||||
|
||||
public ChartScroller(Context context) {
|
||||
scroller = ScrollerCompat.create(context);
|
||||
}
|
||||
|
||||
public boolean startScroll(ChartComputator computator) {
|
||||
scroller.abortAnimation();
|
||||
scrollerStartViewport.set(computator.getCurrentViewport());
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean scroll(ChartComputator computator, float distanceX, float distanceY, ScrollResult scrollResult) {
|
||||
|
||||
// Scrolling uses math based on the viewport (as opposed to math using pixels). Pixel offset is the offset in
|
||||
// screen pixels, while viewport offset is the offset within the current viewport. For additional
|
||||
// information on
|
||||
// surface sizes and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For additional
|
||||
// information about the viewport, see the comments for {@link mCurrentViewport}.
|
||||
|
||||
final Viewport maxViewport = computator.getMaximumViewport();
|
||||
final Viewport visibleViewport = computator.getVisibleViewport();
|
||||
final Viewport currentViewport = computator.getCurrentViewport();
|
||||
final Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
|
||||
final boolean canScrollLeft = currentViewport.left > maxViewport.left;
|
||||
final boolean canScrollRight = currentViewport.right < maxViewport.right;
|
||||
final boolean canScrollTop = currentViewport.top < maxViewport.top;
|
||||
final boolean canScrollBottom = currentViewport.bottom > maxViewport.bottom;
|
||||
|
||||
boolean canScrollX = false;
|
||||
boolean canScrollY = false;
|
||||
|
||||
if (canScrollLeft && distanceX <= 0) {
|
||||
canScrollX = true;
|
||||
} else if (canScrollRight && distanceX >= 0) {
|
||||
canScrollX = true;
|
||||
}
|
||||
|
||||
if (canScrollTop && distanceY <= 0) {
|
||||
canScrollY = true;
|
||||
} else if (canScrollBottom && distanceY >= 0) {
|
||||
canScrollY = true;
|
||||
}
|
||||
|
||||
if (canScrollX || canScrollY) {
|
||||
|
||||
computator.computeScrollSurfaceSize(surfaceSizeBuffer);
|
||||
|
||||
float viewportOffsetX = distanceX * visibleViewport.width() / contentRect.width();
|
||||
float viewportOffsetY = -distanceY * visibleViewport.height() / contentRect.height();
|
||||
|
||||
computator
|
||||
.setViewportTopLeft(currentViewport.left + viewportOffsetX, currentViewport.top + viewportOffsetY);
|
||||
}
|
||||
|
||||
scrollResult.canScrollX = canScrollX;
|
||||
scrollResult.canScrollY = canScrollY;
|
||||
|
||||
return canScrollX || canScrollY;
|
||||
}
|
||||
|
||||
public boolean computeScrollOffset(ChartComputator computator) {
|
||||
if (scroller.computeScrollOffset()) {
|
||||
// The scroller isn't finished, meaning a fling or programmatic pan operation is
|
||||
// currently active.
|
||||
|
||||
final Viewport maxViewport = computator.getMaximumViewport();
|
||||
|
||||
computator.computeScrollSurfaceSize(surfaceSizeBuffer);
|
||||
|
||||
final float currXRange = maxViewport.left + maxViewport.width() * scroller.getCurrX() /
|
||||
surfaceSizeBuffer.x;
|
||||
final float currYRange = maxViewport.top - maxViewport.height() * scroller.getCurrY() /
|
||||
surfaceSizeBuffer.y;
|
||||
|
||||
computator.setViewportTopLeft(currXRange, currYRange);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean fling(int velocityX, int velocityY, ChartComputator computator) {
|
||||
// Flings use math in pixels (as opposed to math based on the viewport).
|
||||
computator.computeScrollSurfaceSize(surfaceSizeBuffer);
|
||||
scrollerStartViewport.set(computator.getCurrentViewport());
|
||||
|
||||
int startX = (int) (surfaceSizeBuffer.x * (scrollerStartViewport.left - computator.getMaximumViewport().left)
|
||||
/ computator.getMaximumViewport().width());
|
||||
int startY = (int) (surfaceSizeBuffer.y * (computator.getMaximumViewport().top - scrollerStartViewport.top) /
|
||||
computator.getMaximumViewport().height());
|
||||
|
||||
// TODO probably should be mScroller.forceFinish but ScrollerCompat doesn't have that method.
|
||||
scroller.abortAnimation();
|
||||
|
||||
final int width = computator.getContentRectMinusAllMargins().width();
|
||||
final int height = computator.getContentRectMinusAllMargins().height();
|
||||
scroller.fling(startX, startY, velocityX, velocityY, 0, surfaceSizeBuffer.x - width + 1, 0,
|
||||
surfaceSizeBuffer.y - height + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class ScrollResult {
|
||||
public boolean canScrollX;
|
||||
public boolean canScrollY;
|
||||
}
|
||||
|
||||
}
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
package lecho.lib.hellocharts.gesture;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import lecho.lib.hellocharts.computator.ChartComputator;
|
||||
import lecho.lib.hellocharts.gesture.ChartScroller.ScrollResult;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.renderer.ChartRenderer;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Default touch handler for most charts. Handles value touch, scroll, fling and zoom.
|
||||
*/
|
||||
public class ChartTouchHandler {
|
||||
protected GestureDetector gestureDetector;
|
||||
protected ScaleGestureDetector scaleGestureDetector;
|
||||
protected ChartScroller chartScroller;
|
||||
protected ChartZoomer chartZoomer;
|
||||
protected Chart chart;
|
||||
protected ChartComputator computator;
|
||||
protected ChartRenderer renderer;
|
||||
|
||||
protected boolean isZoomEnabled = true;
|
||||
protected boolean isScrollEnabled = true;
|
||||
protected boolean isValueTouchEnabled = true;
|
||||
protected boolean isValueSelectionEnabled = false;
|
||||
|
||||
/**
|
||||
* Used only for selection mode to avoid calling listener multiple times for the same selection. Small thing but it
|
||||
* is more intuitive this way.
|
||||
*/
|
||||
protected SelectedValue selectionModeOldValue = new SelectedValue();
|
||||
|
||||
protected SelectedValue selectedValue = new SelectedValue();
|
||||
protected SelectedValue oldSelectedValue = new SelectedValue();
|
||||
|
||||
/**
|
||||
* ViewParent to disallow touch events interception if chart is within scroll container.
|
||||
*/
|
||||
protected ViewParent viewParent;
|
||||
|
||||
/**
|
||||
* Type of scroll of container, horizontal or vertical.
|
||||
*/
|
||||
protected ContainerScrollType containerScrollType;
|
||||
|
||||
public ChartTouchHandler(Context context, Chart chart) {
|
||||
this.chart = chart;
|
||||
this.computator = chart.getChartComputator();
|
||||
this.renderer = chart.getChartRenderer();
|
||||
gestureDetector = new GestureDetector(context, new ChartGestureListener());
|
||||
scaleGestureDetector = new ScaleGestureDetector(context, new ChartScaleGestureListener());
|
||||
chartScroller = new ChartScroller(context);
|
||||
chartZoomer = new ChartZoomer(context, ZoomType.HORIZONTAL_AND_VERTICAL);
|
||||
}
|
||||
|
||||
public void resetTouchHandler() {
|
||||
this.computator = chart.getChartComputator();
|
||||
this.renderer = chart.getChartRenderer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes scroll and zoom using {@link ChartScroller} and {@link ChartZoomer}. This method returns true if
|
||||
* scroll/zoom was computed and chart needs to be invalidated.
|
||||
*/
|
||||
public boolean computeScroll() {
|
||||
boolean needInvalidate = false;
|
||||
if (isScrollEnabled && chartScroller.computeScrollOffset(computator)) {
|
||||
needInvalidate = true;
|
||||
}
|
||||
if (isZoomEnabled && chartZoomer.computeZoom(computator)) {
|
||||
needInvalidate = true;
|
||||
}
|
||||
return needInvalidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle chart touch event(gestures, clicks). Return true if gesture was handled and chart needs to be
|
||||
* invalidated.
|
||||
*/
|
||||
public boolean handleTouchEvent(MotionEvent event) {
|
||||
boolean needInvalidate = false;
|
||||
|
||||
// TODO: detectors always return true, use class member needInvalidate instead local variable as workaround.
|
||||
// This flag should be computed inside gesture listeners methods to avoid invalidation.
|
||||
needInvalidate = gestureDetector.onTouchEvent(event);
|
||||
|
||||
needInvalidate = scaleGestureDetector.onTouchEvent(event) || needInvalidate;
|
||||
|
||||
if (isZoomEnabled && scaleGestureDetector.isInProgress()) {
|
||||
// Special case: if view is inside scroll container and user is scaling disable touch interception by
|
||||
// parent.
|
||||
disallowParentInterceptTouchEvent();
|
||||
}
|
||||
|
||||
if (isValueTouchEnabled) {
|
||||
needInvalidate = computeTouch(event) || needInvalidate;
|
||||
}
|
||||
|
||||
return needInvalidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle chart touch event(gestures, clicks). Return true if gesture was handled and chart needs to be
|
||||
* invalidated.
|
||||
* If viewParent and containerScrollType are not null chart can be scrolled and scaled within horizontal or
|
||||
* vertical
|
||||
* scroll container like ViewPager.
|
||||
*/
|
||||
public boolean handleTouchEvent(MotionEvent event, ViewParent viewParent,
|
||||
ContainerScrollType containerScrollType) {
|
||||
this.viewParent = viewParent;
|
||||
this.containerScrollType = containerScrollType;
|
||||
|
||||
return handleTouchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow parent view from intercepting touch events. Use it for chart that is within some scroll container i.e.
|
||||
* ViewPager.
|
||||
*/
|
||||
private void disallowParentInterceptTouchEvent() {
|
||||
if (null != viewParent) {
|
||||
viewParent.requestDisallowInterceptTouchEvent(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow parent view to intercept touch events if chart cannot be scroll horizontally or vertically according to
|
||||
* the
|
||||
* current value of {@link #containerScrollType}.
|
||||
*/
|
||||
private void allowParentInterceptTouchEvent(ScrollResult scrollResult) {
|
||||
if (null != viewParent) {
|
||||
if (ContainerScrollType.HORIZONTAL == containerScrollType && !scrollResult.canScrollX
|
||||
&& !scaleGestureDetector.isInProgress()) {
|
||||
viewParent.requestDisallowInterceptTouchEvent(false);
|
||||
} else if (ContainerScrollType.VERTICAL == containerScrollType && !scrollResult.canScrollY
|
||||
&& !scaleGestureDetector.isInProgress()) {
|
||||
viewParent.requestDisallowInterceptTouchEvent(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean computeTouch(MotionEvent event) {
|
||||
boolean needInvalidate = false;
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
boolean wasTouched = renderer.isTouched();
|
||||
boolean isTouched = checkTouch(event.getX(), event.getY());
|
||||
if (wasTouched != isTouched) {
|
||||
needInvalidate = true;
|
||||
|
||||
if (isValueSelectionEnabled) {
|
||||
selectionModeOldValue.clear();
|
||||
if (wasTouched && !renderer.isTouched()) {
|
||||
chart.callTouchListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (renderer.isTouched()) {
|
||||
if (checkTouch(event.getX(), event.getY())) {
|
||||
if (isValueSelectionEnabled) {
|
||||
// For selection mode call listener only if selected value changed,
|
||||
// that means that should be
|
||||
// first(selection) click on given value.
|
||||
if (!selectionModeOldValue.equals(selectedValue)) {
|
||||
selectionModeOldValue.set(selectedValue);
|
||||
chart.callTouchListener();
|
||||
}
|
||||
} else {
|
||||
chart.callTouchListener();
|
||||
renderer.clearTouch();
|
||||
}
|
||||
} else {
|
||||
renderer.clearTouch();
|
||||
}
|
||||
needInvalidate = true;
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// If value was touched and now touch point is outside of value area - clear touch and invalidate, user
|
||||
// probably moved finger away from given chart value.
|
||||
if (renderer.isTouched()) {
|
||||
if (!checkTouch(event.getX(), event.getY())) {
|
||||
renderer.clearTouch();
|
||||
needInvalidate = true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (renderer.isTouched()) {
|
||||
renderer.clearTouch();
|
||||
needInvalidate = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return needInvalidate;
|
||||
}
|
||||
|
||||
private boolean checkTouch(float touchX, float touchY) {
|
||||
oldSelectedValue.set(selectedValue);
|
||||
selectedValue.clear();
|
||||
|
||||
if (renderer.checkTouch(touchX, touchY)) {
|
||||
selectedValue.set(renderer.getSelectedValue());
|
||||
}
|
||||
|
||||
// Check if selection is still on the same value, if not return false.
|
||||
if (oldSelectedValue.isSet() && selectedValue.isSet() && !oldSelectedValue.equals(selectedValue)) {
|
||||
return false;
|
||||
} else {
|
||||
return renderer.isTouched();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isZoomEnabled() {
|
||||
return isZoomEnabled;
|
||||
}
|
||||
|
||||
public void setZoomEnabled(boolean isZoomEnabled) {
|
||||
this.isZoomEnabled = isZoomEnabled;
|
||||
|
||||
}
|
||||
|
||||
public boolean isScrollEnabled() {
|
||||
return isScrollEnabled;
|
||||
}
|
||||
|
||||
public void setScrollEnabled(boolean isScrollEnabled) {
|
||||
this.isScrollEnabled = isScrollEnabled;
|
||||
}
|
||||
|
||||
public ZoomType getZoomType() {
|
||||
return chartZoomer.getZoomType();
|
||||
}
|
||||
|
||||
public void setZoomType(ZoomType zoomType) {
|
||||
chartZoomer.setZoomType(zoomType);
|
||||
}
|
||||
|
||||
public boolean isValueTouchEnabled() {
|
||||
return isValueTouchEnabled;
|
||||
}
|
||||
|
||||
public void setValueTouchEnabled(boolean isValueTouchEnabled) {
|
||||
this.isValueTouchEnabled = isValueTouchEnabled;
|
||||
}
|
||||
|
||||
public boolean isValueSelectionEnabled() {
|
||||
return isValueSelectionEnabled;
|
||||
}
|
||||
|
||||
public void setValueSelectionEnabled(boolean isValueSelectionEnabled) {
|
||||
this.isValueSelectionEnabled = isValueSelectionEnabled;
|
||||
}
|
||||
|
||||
protected class ChartScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
if (isZoomEnabled) {
|
||||
float scale = 2.0f - detector.getScaleFactor();
|
||||
if (Float.isInfinite(scale)) {
|
||||
scale = 1;
|
||||
}
|
||||
return chartZoomer.scale(computator, detector.getFocusX(), detector.getFocusY(), scale);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected class ChartGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
|
||||
protected ScrollResult scrollResult = new ScrollResult();
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
if (isScrollEnabled) {
|
||||
|
||||
disallowParentInterceptTouchEvent();
|
||||
|
||||
return chartScroller.startScroll(computator);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
if (isZoomEnabled) {
|
||||
return chartZoomer.startZoom(e, computator);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
if (isScrollEnabled) {
|
||||
boolean canScroll = chartScroller
|
||||
.scroll(computator, distanceX, distanceY, scrollResult);
|
||||
|
||||
allowParentInterceptTouchEvent(scrollResult);
|
||||
|
||||
return canScroll;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
if (isScrollEnabled) {
|
||||
return chartScroller.fling((int) -velocityX, (int) -velocityY, computator);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package lecho.lib.hellocharts.gesture;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import lecho.lib.hellocharts.computator.ChartComputator;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
|
||||
/**
|
||||
* Encapsulates zooming functionality.
|
||||
*/
|
||||
public class ChartZoomer {
|
||||
public static final float ZOOM_AMOUNT = 0.25f;
|
||||
private ZoomerCompat zoomer;
|
||||
private ZoomType zoomType;
|
||||
private PointF zoomFocalPoint = new PointF();// Used for double tap zoom
|
||||
private PointF viewportFocus = new PointF();
|
||||
private Viewport scrollerStartViewport = new Viewport(); // Used only for zooms and flings
|
||||
|
||||
public ChartZoomer(Context context, ZoomType zoomType) {
|
||||
zoomer = new ZoomerCompat(context);
|
||||
this.zoomType = zoomType;
|
||||
}
|
||||
|
||||
public boolean startZoom(MotionEvent e, ChartComputator computator) {
|
||||
zoomer.forceFinished(true);
|
||||
scrollerStartViewport.set(computator.getCurrentViewport());
|
||||
if (!computator.rawPixelsToDataPoint(e.getX(), e.getY(), zoomFocalPoint)) {
|
||||
// Focus point is not within content area.
|
||||
return false;
|
||||
}
|
||||
zoomer.startZoom(ZOOM_AMOUNT);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean computeZoom(ChartComputator computator) {
|
||||
if (zoomer.computeZoom()) {
|
||||
// Performs the zoom since a zoom is in progress.
|
||||
final float newWidth = (1.0f - zoomer.getCurrZoom()) * scrollerStartViewport.width();
|
||||
final float newHeight = (1.0f - zoomer.getCurrZoom()) * scrollerStartViewport.height();
|
||||
final float pointWithinViewportX = (zoomFocalPoint.x - scrollerStartViewport.left)
|
||||
/ scrollerStartViewport.width();
|
||||
final float pointWithinViewportY = (zoomFocalPoint.y - scrollerStartViewport.bottom)
|
||||
/ scrollerStartViewport.height();
|
||||
|
||||
float left = zoomFocalPoint.x - newWidth * pointWithinViewportX;
|
||||
float top = zoomFocalPoint.y + newHeight * (1 - pointWithinViewportY);
|
||||
float right = zoomFocalPoint.x + newWidth * (1 - pointWithinViewportX);
|
||||
float bottom = zoomFocalPoint.y - newHeight * pointWithinViewportY;
|
||||
setCurrentViewport(computator, left, top, right, bottom);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean scale(ChartComputator computator, float focusX, float focusY, float scale) {
|
||||
/**
|
||||
* Smaller viewport means bigger zoom so for zoomIn scale should have value <1, for zoomOout >1
|
||||
*/
|
||||
final float newWidth = scale * computator.getCurrentViewport().width();
|
||||
final float newHeight = scale * computator.getCurrentViewport().height();
|
||||
if (!computator.rawPixelsToDataPoint(focusX, focusY, viewportFocus)) {
|
||||
// Focus point is not within content area.
|
||||
return false;
|
||||
}
|
||||
|
||||
float left = viewportFocus.x - (focusX - computator.getContentRectMinusAllMargins().left)
|
||||
* (newWidth / computator.getContentRectMinusAllMargins().width());
|
||||
float top = viewportFocus.y + (focusY - computator.getContentRectMinusAllMargins().top)
|
||||
* (newHeight / computator.getContentRectMinusAllMargins().height());
|
||||
float right = left + newWidth;
|
||||
float bottom = top - newHeight;
|
||||
setCurrentViewport(computator, left, top, right, bottom);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setCurrentViewport(ChartComputator computator, float left, float top, float right, float bottom) {
|
||||
Viewport currentViewport = computator.getCurrentViewport();
|
||||
if (ZoomType.HORIZONTAL_AND_VERTICAL == zoomType) {
|
||||
computator.setCurrentViewport(left, top, right, bottom);
|
||||
} else if (ZoomType.HORIZONTAL == zoomType) {
|
||||
computator.setCurrentViewport(left, currentViewport.top, right, currentViewport.bottom);
|
||||
} else if (ZoomType.VERTICAL == zoomType) {
|
||||
computator.setCurrentViewport(currentViewport.left, top, currentViewport.right, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
public ZoomType getZoomType() {
|
||||
return zoomType;
|
||||
}
|
||||
|
||||
public void setZoomType(ZoomType zoomType) {
|
||||
this.zoomType = zoomType;
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package lecho.lib.hellocharts.gesture;
|
||||
|
||||
/**
|
||||
* Enum used to inform chart in which type of container it exists.
|
||||
*/
|
||||
public enum ContainerScrollType {
|
||||
HORIZONTAL, VERTICAL
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package lecho.lib.hellocharts.gesture;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.RectF;
|
||||
import android.support.v4.widget.ScrollerCompat;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
import lecho.lib.hellocharts.view.PieChartView;
|
||||
|
||||
/**
|
||||
* Touch handler for PieChart. It doesn't handle zoom and scroll like default ChartTouchHandler. Instead it uses
|
||||
* Scroller(ScrollerCompat) directly to compute PieChart rotation when user scroll. ChartScroller and ChartZoomer are
|
||||
* not really used here.
|
||||
*/
|
||||
public class PieChartTouchHandler extends ChartTouchHandler {
|
||||
/**
|
||||
* The initial fling velocity is divided by this amount.
|
||||
*/
|
||||
public static final int FLING_VELOCITY_DOWNSCALE = 4;
|
||||
/**
|
||||
* PieChartTouchHandler uses its own instance of Scroller.
|
||||
*/
|
||||
protected ScrollerCompat scroller;
|
||||
/**
|
||||
* Reference to PieChartView to use some methods specific for that kind of chart.
|
||||
*/
|
||||
protected PieChartView pieChart;
|
||||
|
||||
private boolean isRotationEnabled = true;
|
||||
|
||||
public PieChartTouchHandler(Context context, PieChartView chart) {
|
||||
super(context, chart);
|
||||
pieChart = (PieChartView) chart;
|
||||
scroller = ScrollerCompat.create(context);
|
||||
gestureDetector = new GestureDetector(context, new ChartGestureListener());
|
||||
scaleGestureDetector = new ScaleGestureDetector(context, new ChartScaleGestureListener());
|
||||
isZoomEnabled = false;// Zoom is not supported by PieChart.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean computeScroll() {
|
||||
if (!isRotationEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (scroller.computeScrollOffset()) {
|
||||
pieChart.setChartRotation(scroller.getCurrY(), false);
|
||||
// pieChart.setChartRotation() will invalidate view so no need to return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleTouchEvent(MotionEvent event) {
|
||||
boolean needInvalidate = super.handleTouchEvent(event);
|
||||
|
||||
if (isRotationEnabled) {
|
||||
needInvalidate = gestureDetector.onTouchEvent(event) || needInvalidate;
|
||||
}
|
||||
return needInvalidate;
|
||||
}
|
||||
|
||||
public boolean isRotationEnabled() {
|
||||
return isRotationEnabled;
|
||||
}
|
||||
|
||||
public void setRotationEnabled(boolean isRotationEnabled) {
|
||||
this.isRotationEnabled = isRotationEnabled;
|
||||
}
|
||||
|
||||
private class ChartScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
// No scale for PieChart.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class ChartGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
if (isRotationEnabled) {
|
||||
scroller.abortAnimation();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
if (isRotationEnabled) {
|
||||
// Set the pie rotation directly.
|
||||
final RectF circleOval = pieChart.getCircleOval();
|
||||
final float centerX = circleOval.centerX();
|
||||
final float centerY = circleOval.centerY();
|
||||
float scrollTheta = vectorToScalarScroll(distanceX, distanceY, e2.getX() - centerX, e2.getY() -
|
||||
centerY);
|
||||
pieChart.setChartRotation(pieChart.getChartRotation() - (int) scrollTheta / FLING_VELOCITY_DOWNSCALE,
|
||||
false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
if (isRotationEnabled) {
|
||||
// Set up the Scroller for a fling
|
||||
final RectF circleOval = pieChart.getCircleOval();
|
||||
final float centerX = circleOval.centerX();
|
||||
final float centerY = circleOval.centerY();
|
||||
float scrollTheta = vectorToScalarScroll(velocityX, velocityY, e2.getX() - centerX, e2.getY() -
|
||||
centerY);
|
||||
scroller.abortAnimation();
|
||||
scroller.fling(0, (int) pieChart.getChartRotation(), 0, (int) scrollTheta / FLING_VELOCITY_DOWNSCALE,
|
||||
0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for translating (x,y) scroll vectors into scalar rotation of the pie.
|
||||
*
|
||||
* @param dx The x component of the current scroll vector.
|
||||
* @param dy The y component of the current scroll vector.
|
||||
* @param x The x position of the current touch, relative to the pie center.
|
||||
* @param y The y position of the current touch, relative to the pie center.
|
||||
* @return The scalar representing the change in angular position for this scroll.
|
||||
*/
|
||||
private float vectorToScalarScroll(float dx, float dy, float x, float y) {
|
||||
// get the length of the vector
|
||||
float l = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// decide if the scalar should be negative or positive by finding
|
||||
// the dot product of the vector perpendicular to (x,y).
|
||||
float crossX = -y;
|
||||
float crossY = x;
|
||||
|
||||
float dot = (crossX * dx + crossY * dy);
|
||||
float sign = Math.signum(dot);
|
||||
|
||||
return l * sign;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package lecho.lib.hellocharts.gesture;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Touch Handler for preview charts. It scroll and zoom only preview area, not all preview chart data.
|
||||
*/
|
||||
public class PreviewChartTouchHandler extends ChartTouchHandler {
|
||||
|
||||
public PreviewChartTouchHandler(Context context, Chart chart) {
|
||||
super(context, chart);
|
||||
gestureDetector = new GestureDetector(context, new PreviewChartGestureListener());
|
||||
scaleGestureDetector = new ScaleGestureDetector(context, new ChartScaleGestureListener());
|
||||
|
||||
// Disable value touch and selection mode, by default not needed for preview chart.
|
||||
isValueTouchEnabled = false;
|
||||
isValueSelectionEnabled = false;
|
||||
}
|
||||
|
||||
protected class ChartScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
if (isZoomEnabled) {
|
||||
float scale = detector.getCurrentSpan() / detector.getPreviousSpan();
|
||||
if (Float.isInfinite(scale)) {
|
||||
scale = 1;
|
||||
}
|
||||
return chartZoomer.scale(computator, detector.getFocusX(), detector.getFocusY(), scale);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected class PreviewChartGestureListener extends ChartGestureListener {
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
return super.onScroll(e1, e2, -distanceX, -distanceY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
return super.onFling(e1, e2, -velocityX, -velocityY);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package lecho.lib.hellocharts.gesture;
|
||||
|
||||
public enum ZoomType {
|
||||
|
||||
HORIZONTAL, VERTICAL, HORIZONTAL_AND_VERTICAL;
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2013 The Android Open Source Project
|
||||
*
|
||||
* 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 lecho.lib.hellocharts.gesture;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
/**
|
||||
* A simple class that animates double-touch zoom gestures. Functionally similar to a {@link android.widget.Scroller}.
|
||||
*/
|
||||
public class ZoomerCompat {
|
||||
private static final int DEFAULT_SHORT_ANIMATION_DURATION = 200;
|
||||
/**
|
||||
* The interpolator, used for making zooms animate 'naturally.'
|
||||
*/
|
||||
private Interpolator mInterpolator;
|
||||
|
||||
/**
|
||||
* The total animation duration for a zoom.
|
||||
*/
|
||||
private long mAnimationDurationMillis;
|
||||
|
||||
/**
|
||||
* Whether or not the current zoom has finished.
|
||||
*/
|
||||
private boolean mFinished = true;
|
||||
|
||||
/**
|
||||
* The current zoom value; computed by {@link #computeZoom()}.
|
||||
*/
|
||||
private float mCurrentZoom;
|
||||
|
||||
/**
|
||||
* The time the zoom started, computed using {@link android.os.SystemClock#elapsedRealtime()}.
|
||||
*/
|
||||
private long mStartRTC;
|
||||
|
||||
/**
|
||||
* The destination zoom factor.
|
||||
*/
|
||||
private float mEndZoom;
|
||||
|
||||
public ZoomerCompat(Context context) {
|
||||
mInterpolator = new DecelerateInterpolator();
|
||||
// TODO: use constant
|
||||
mAnimationDurationMillis = DEFAULT_SHORT_ANIMATION_DURATION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the zoom finished state to the given value. Unlike {@link #abortAnimation()}, the current zoom value isn't
|
||||
* set to the ending value.
|
||||
*
|
||||
* @see android.widget.Scroller#forceFinished(boolean)
|
||||
*/
|
||||
public void forceFinished(boolean finished) {
|
||||
mFinished = finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts the animation, setting the current zoom value to the ending value.
|
||||
*
|
||||
* @see android.widget.Scroller#abortAnimation()
|
||||
*/
|
||||
public void abortAnimation() {
|
||||
mFinished = true;
|
||||
mCurrentZoom = mEndZoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a zoom from 1.0 to (1.0 + endZoom). That is, to zoom from 100% to 125%, endZoom should by 0.25f.
|
||||
*
|
||||
* @see android.widget.Scroller#startScroll(int, int, int, int)
|
||||
*/
|
||||
public void startZoom(float endZoom) {
|
||||
mStartRTC = SystemClock.elapsedRealtime();
|
||||
mEndZoom = endZoom;
|
||||
|
||||
mFinished = false;
|
||||
mCurrentZoom = 1f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the current zoom level, returning true if the zoom is still active and false if the zoom has finished.
|
||||
*
|
||||
* @see android.widget.Scroller#computeScrollOffset()
|
||||
*/
|
||||
public boolean computeZoom() {
|
||||
if (mFinished) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long tRTC = SystemClock.elapsedRealtime() - mStartRTC;
|
||||
if (tRTC >= mAnimationDurationMillis) {
|
||||
mFinished = true;
|
||||
mCurrentZoom = mEndZoom;
|
||||
return false;
|
||||
}
|
||||
|
||||
float t = tRTC * 1f / mAnimationDurationMillis;
|
||||
mCurrentZoom = mEndZoom * mInterpolator.getInterpolation(t);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current zoom level.
|
||||
*
|
||||
* @see android.widget.Scroller#getCurrX()
|
||||
*/
|
||||
public float getCurrZoom() {
|
||||
return mCurrentZoom;
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.BubbleValue;
|
||||
|
||||
public interface BubbleChartOnValueSelectListener extends OnValueDeselectListener {
|
||||
|
||||
public void onValueSelected(int bubbleIndex, BubbleValue value);
|
||||
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
|
||||
public interface ColumnChartOnValueSelectListener extends OnValueDeselectListener {
|
||||
|
||||
public void onValueSelected(int columnIndex, int subcolumnIndex, SubcolumnValue value);
|
||||
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
|
||||
public interface ComboLineColumnChartOnValueSelectListener extends OnValueDeselectListener {
|
||||
|
||||
public void onColumnValueSelected(int columnIndex, int subcolumnIndex, SubcolumnValue value);
|
||||
|
||||
public void onPointValueSelected(int lineIndex, int pointIndex, PointValue value);
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.BubbleValue;
|
||||
|
||||
public class DummyBubbleChartOnValueSelectListener implements BubbleChartOnValueSelectListener {
|
||||
|
||||
@Override
|
||||
public void onValueSelected(int bubbleIndex, BubbleValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueDeselected() {
|
||||
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
|
||||
public class DummyColumnChartOnValueSelectListener implements ColumnChartOnValueSelectListener {
|
||||
|
||||
@Override
|
||||
public void onValueSelected(int columnIndex, int subcolumnIndex, SubcolumnValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueDeselected() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
|
||||
public class DummyCompoLineColumnChartOnValueSelectListener implements ComboLineColumnChartOnValueSelectListener {
|
||||
|
||||
@Override
|
||||
public void onColumnValueSelected(int columnIndex, int subcolumnIndex, SubcolumnValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPointValueSelected(int lineIndex, int pointIndex, PointValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueDeselected() {
|
||||
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
|
||||
public class DummyLineChartOnValueSelectListener implements LineChartOnValueSelectListener {
|
||||
|
||||
@Override
|
||||
public void onValueSelected(int lineIndex, int pointIndex, PointValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueDeselected() {
|
||||
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.SliceValue;
|
||||
|
||||
public class DummyPieChartOnValueSelectListener implements PieChartOnValueSelectListener {
|
||||
|
||||
@Override
|
||||
public void onValueSelected(int arcIndex, SliceValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueDeselected() {
|
||||
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
|
||||
public class DummyVieportChangeListener implements ViewportChangeListener {
|
||||
|
||||
@Override
|
||||
public void onViewportChanged(Viewport viewport) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
|
||||
public interface LineChartOnValueSelectListener extends OnValueDeselectListener {
|
||||
|
||||
public void onValueSelected(int lineIndex, int pointIndex, PointValue value);
|
||||
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
public interface OnValueDeselectListener {
|
||||
|
||||
/**
|
||||
* Called only in chart selection mode when user touch empty space causing value deselection.
|
||||
* Note: this method is not called when selection mode is disabled.
|
||||
*/
|
||||
public void onValueDeselected();
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
|
||||
import lecho.lib.hellocharts.model.SliceValue;
|
||||
|
||||
public interface PieChartOnValueSelectListener extends OnValueDeselectListener {
|
||||
|
||||
public void onValueSelected(int arcIndex, SliceValue value);
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package lecho.lib.hellocharts.listener;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
|
||||
/**
|
||||
* Use implementations of this listener to be notified when chart viewport changed. For now it works only for preview
|
||||
* charts. To make it works for other chart types you just need to uncomment last line in
|
||||
* {@link lecho.lib.hellocharts.computator.ChartComputator#constrainViewport(float, float, float, float)} method.
|
||||
*/
|
||||
public interface ViewportChangeListener {
|
||||
|
||||
/**
|
||||
* Called when current viewport of chart changed. You should not modify that viewport.
|
||||
*/
|
||||
public void onViewportChanged(Viewport viewport);
|
||||
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
|
||||
/**
|
||||
* Base class for most chart data models.
|
||||
*/
|
||||
public abstract class AbstractChartData implements ChartData {
|
||||
public static final int DEFAULT_TEXT_SIZE_SP = 12;
|
||||
protected Axis axisXBottom;
|
||||
protected Axis axisYLeft;
|
||||
protected Axis axisXTop;
|
||||
protected Axis axisYRight;
|
||||
protected int valueLabelTextColor = Color.WHITE;
|
||||
protected int valueLabelTextSize = DEFAULT_TEXT_SIZE_SP;
|
||||
protected Typeface valueLabelTypeface;
|
||||
|
||||
/**
|
||||
* If true each value label will have background rectangle
|
||||
*/
|
||||
protected boolean isValueLabelBackgroundEnabled = true;
|
||||
|
||||
/**
|
||||
* If true and {@link #isValueLabelBackgroundEnabled} is true each label will have background rectangle and that
|
||||
* rectangle will be filled with color specified for given value.
|
||||
*/
|
||||
protected boolean isValueLabelBackgrountAuto = true;
|
||||
|
||||
/**
|
||||
* If {@link #isValueLabelBackgroundEnabled} is true and {@link #isValueLabelBackgrountAuto} is false each label
|
||||
* will have background rectangle and that rectangle will be filled with this color. Helpful if you want all labels
|
||||
* to have the same background color.
|
||||
*/
|
||||
protected int valueLabelBackgroundColor = ChartUtils.darkenColor(ChartUtils.DEFAULT_DARKEN_COLOR);
|
||||
|
||||
public AbstractChartData() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor for deep copy.
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
public AbstractChartData(AbstractChartData data) {
|
||||
if (null != data.axisXBottom) {
|
||||
this.axisXBottom = new Axis(data.axisXBottom);
|
||||
}
|
||||
if (null != data.axisXTop) {
|
||||
this.axisXTop = new Axis(data.axisXTop);
|
||||
}
|
||||
if (null != data.axisYLeft) {
|
||||
this.axisYLeft = new Axis(data.axisYLeft);
|
||||
}
|
||||
if (null != data.axisYRight) {
|
||||
this.axisYRight = new Axis(data.axisYRight);
|
||||
}
|
||||
this.valueLabelTextColor = data.valueLabelTextColor;
|
||||
this.valueLabelTextSize = data.valueLabelTextSize;
|
||||
this.valueLabelTypeface = data.valueLabelTypeface;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Axis getAxisXBottom() {
|
||||
return axisXBottom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAxisXBottom(Axis axisX) {
|
||||
this.axisXBottom = axisX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Axis getAxisYLeft() {
|
||||
return axisYLeft;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAxisYLeft(Axis axisY) {
|
||||
this.axisYLeft = axisY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Axis getAxisXTop() {
|
||||
return axisXTop;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAxisXTop(Axis axisX) {
|
||||
this.axisXTop = axisX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Axis getAxisYRight() {
|
||||
return axisYRight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAxisYRight(Axis axisY) {
|
||||
this.axisYRight = axisY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getValueLabelTextColor() {
|
||||
return valueLabelTextColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueLabelsTextColor(int valueLabelTextColor) {
|
||||
this.valueLabelTextColor = valueLabelTextColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getValueLabelTextSize() {
|
||||
return valueLabelTextSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueLabelTextSize(int valueLabelTextSize) {
|
||||
this.valueLabelTextSize = valueLabelTextSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Typeface getValueLabelTypeface() {
|
||||
return valueLabelTypeface;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueLabelTypeface(Typeface typeface) {
|
||||
this.valueLabelTypeface = typeface;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValueLabelBackgroundEnabled() {
|
||||
return isValueLabelBackgroundEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueLabelBackgroundEnabled(boolean isValueLabelBackgroundEnabled) {
|
||||
this.isValueLabelBackgroundEnabled = isValueLabelBackgroundEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValueLabelBackgroundAuto() {
|
||||
return isValueLabelBackgrountAuto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueLabelBackgroundAuto(boolean isValueLabelBackgrountAuto) {
|
||||
this.isValueLabelBackgrountAuto = isValueLabelBackgrountAuto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getValueLabelBackgroundColor() {
|
||||
return valueLabelBackgroundColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueLabelBackgroundColor(int valueLabelBackgroundColor) {
|
||||
this.valueLabelBackgroundColor = valueLabelBackgroundColor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lecho.lib.hellocharts.formatter.AxisValueFormatter;
|
||||
import lecho.lib.hellocharts.formatter.SimpleAxisValueFormatter;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
|
||||
/**
|
||||
* Single axis model. By default axis is auto-generated. Use {@link #setAutoGenerated(boolean)} to disable axis values
|
||||
* generation and set values manually using {@link #setValues(List)}. If Axis is auto-generated {@link #setValues(List)}
|
||||
* will be ignored but if you set some values Axis will switch to manual mode. Change how axis labels are displayed by
|
||||
* changing formatter {@link #setFormatter(lecho.lib.hellocharts.formatter.AxisValueFormatter)}. Axis can have a name
|
||||
* that should be displayed next to
|
||||
* labels(that depends on renderer implementation), you can change name using {@link #setName(String)}, by default axis
|
||||
* name is empty and therefore not displayed.
|
||||
*/
|
||||
public class Axis {
|
||||
public static final int DEFAULT_TEXT_SIZE_SP = 12;
|
||||
public static final int DEFAULT_MAX_AXIS_LABEL_CHARS = 3;
|
||||
/**
|
||||
* Text size for axis labels and name.
|
||||
*/
|
||||
private int textSize = DEFAULT_TEXT_SIZE_SP;
|
||||
/**
|
||||
* Maximum number of characters used for this axis. Used to determine axis dimensions.
|
||||
*/
|
||||
private int maxLabelChars = DEFAULT_MAX_AXIS_LABEL_CHARS;
|
||||
/**
|
||||
* Axis values, each value will be used to calculate its label position.
|
||||
*/
|
||||
private List<AxisValue> values = new ArrayList<AxisValue>();
|
||||
/**
|
||||
* Name for this axis.
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* If true axis will be generated to automatically fit chart ranges. *
|
||||
*/
|
||||
private boolean isAutoGenerated = true;
|
||||
/**
|
||||
* If true renderer will draw lines(grid) for this axis.
|
||||
*/
|
||||
private boolean hasLines = false;
|
||||
/**
|
||||
* If true axis labels will be drown inside chart area.
|
||||
*/
|
||||
private boolean isInside = false;
|
||||
/**
|
||||
* Axis labels and name text color.
|
||||
*/
|
||||
private int textColor = Color.LTGRAY;
|
||||
/**
|
||||
* Axis grid lines color.
|
||||
*/
|
||||
private int lineColor = ChartUtils.DEFAULT_DARKEN_COLOR;
|
||||
/**
|
||||
* Typeface for labels and name text.
|
||||
*/
|
||||
private Typeface typeface;
|
||||
|
||||
/**
|
||||
* Formatter used to format labels.
|
||||
*/
|
||||
private AxisValueFormatter formatter = new SimpleAxisValueFormatter();
|
||||
|
||||
/**
|
||||
* If true draws a line between the labels and the graph *
|
||||
*/
|
||||
private boolean hasSeparationLine = true;
|
||||
|
||||
private boolean hasTiltedLabels = false;
|
||||
|
||||
/**
|
||||
* Creates auto-generated axis without name and with default formatter.
|
||||
*/
|
||||
public Axis() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates axis with given values(not auto-generated) without name and with default formatter.
|
||||
*/
|
||||
public Axis(List<AxisValue> values) {
|
||||
setValues(values);
|
||||
}
|
||||
|
||||
public Axis(Axis axis) {
|
||||
this.name = axis.name;
|
||||
this.isAutoGenerated = axis.isAutoGenerated;
|
||||
this.hasLines = axis.hasLines;
|
||||
this.isInside = axis.isInside;
|
||||
this.textColor = axis.textColor;
|
||||
this.lineColor = axis.lineColor;
|
||||
this.textSize = axis.textSize;
|
||||
this.maxLabelChars = axis.maxLabelChars;
|
||||
this.typeface = axis.typeface;
|
||||
this.formatter = axis.formatter;
|
||||
this.hasSeparationLine = axis.hasSeparationLine;
|
||||
|
||||
for (AxisValue axisValue : axis.values) {
|
||||
this.values.add(new AxisValue(axisValue));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Axis with values from start to stop inclusive.
|
||||
*/
|
||||
public static Axis generateAxisFromRange(float start, float stop, float step) {
|
||||
|
||||
List<AxisValue> values = new ArrayList<AxisValue>();
|
||||
for (float value = start; value <= stop; value += step) {
|
||||
AxisValue axisValue = new AxisValue(value);
|
||||
values.add(axisValue);
|
||||
}
|
||||
|
||||
Axis axis = new Axis(values);
|
||||
return axis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Axis with values from given list.
|
||||
*/
|
||||
public static Axis generateAxisFromCollection(List<Float> axisValues) {
|
||||
List<AxisValue> values = new ArrayList<AxisValue>();
|
||||
int index = 0;
|
||||
for (float value : axisValues) {
|
||||
AxisValue axisValue = new AxisValue(value);
|
||||
values.add(axisValue);
|
||||
++index;
|
||||
}
|
||||
|
||||
Axis axis = new Axis(values);
|
||||
return axis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Axis with values and labels from given lists, both lists must have the same size.
|
||||
*/
|
||||
public static Axis generateAxisFromCollection(List<Float> axisValues, List<String> axisValuesLabels) {
|
||||
if (axisValues.size() != axisValuesLabels.size()) {
|
||||
throw new IllegalArgumentException("Values and labels lists must have the same size!");
|
||||
}
|
||||
|
||||
List<AxisValue> values = new ArrayList<AxisValue>();
|
||||
int index = 0;
|
||||
for (float value : axisValues) {
|
||||
AxisValue axisValue = new AxisValue(value).setLabel(axisValuesLabels.get(index));
|
||||
values.add(axisValue);
|
||||
++index;
|
||||
}
|
||||
|
||||
Axis axis = new Axis(values);
|
||||
return axis;
|
||||
}
|
||||
|
||||
public List<AxisValue> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public Axis setValues(List<AxisValue> values) {
|
||||
if (null == values) {
|
||||
this.values = new ArrayList<AxisValue>();
|
||||
} else {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
this.isAutoGenerated = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Axis setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isAutoGenerated() {
|
||||
return isAutoGenerated;
|
||||
}
|
||||
|
||||
public Axis setAutoGenerated(boolean isAutoGenerated) {
|
||||
this.isAutoGenerated = isAutoGenerated;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasLines() {
|
||||
return hasLines;
|
||||
}
|
||||
|
||||
public Axis setHasLines(boolean hasLines) {
|
||||
this.hasLines = hasLines;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getTextColor() {
|
||||
return textColor;
|
||||
}
|
||||
|
||||
public Axis setTextColor(int color) {
|
||||
this.textColor = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setInside(boolean)
|
||||
*/
|
||||
public boolean isInside() {
|
||||
return isInside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if you want axis values to be drawn inside chart area(axis name still will be drawn outside), by
|
||||
* default this is set to false and axis is drawn outside chart area.
|
||||
*/
|
||||
public Axis setInside(boolean isInside) {
|
||||
this.isInside = isInside;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getLineColor() {
|
||||
return lineColor;
|
||||
}
|
||||
|
||||
public Axis setLineColor(int lineColor) {
|
||||
this.lineColor = lineColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getTextSize() {
|
||||
return textSize;
|
||||
}
|
||||
|
||||
public Axis setTextSize(int textSize) {
|
||||
this.textSize = textSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getMaxLabelChars() {
|
||||
return maxLabelChars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum number of characters for axis labels, min 0, max 32.
|
||||
*/
|
||||
public Axis setMaxLabelChars(int maxLabelChars) {
|
||||
if (maxLabelChars < 0) {
|
||||
maxLabelChars = 0;
|
||||
} else if (maxLabelChars > 32) {
|
||||
maxLabelChars = 32;
|
||||
}
|
||||
this.maxLabelChars = maxLabelChars;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Typeface getTypeface() {
|
||||
return typeface;
|
||||
}
|
||||
|
||||
public Axis setTypeface(Typeface typeface) {
|
||||
this.typeface = typeface;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AxisValueFormatter getFormatter() {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
public Axis setFormatter(AxisValueFormatter formatter) {
|
||||
if (null == formatter) {
|
||||
this.formatter = new SimpleAxisValueFormatter();
|
||||
} else {
|
||||
this.formatter = formatter;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set true if you want to draw separation line for this axis, set false to hide separation line, by default true.
|
||||
*/
|
||||
public Axis setHasSeparationLine(boolean hasSeparationLine) {
|
||||
this.hasSeparationLine = hasSeparationLine;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasSeparationLine() {
|
||||
return hasSeparationLine;
|
||||
}
|
||||
|
||||
public boolean hasTiltedLabels() {
|
||||
return hasTiltedLabels;
|
||||
}
|
||||
|
||||
public Axis setHasTiltedLabels(boolean hasTiltedLabels) {
|
||||
this.hasTiltedLabels = hasTiltedLabels;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Single axis value, use it to manually set axis labels position. You can use label attribute to display text instead
|
||||
* of number but value formatter implementation have to handle it.
|
||||
*/
|
||||
public class AxisValue {
|
||||
private float value;
|
||||
private char[] label;
|
||||
|
||||
public AxisValue(float value) {
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public AxisValue(float value, char[] label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public AxisValue(AxisValue axisValue) {
|
||||
this.value = axisValue.value;
|
||||
this.label = axisValue.label;
|
||||
}
|
||||
|
||||
public float getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public AxisValue setValue(float value) {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public char[] getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom label for this axis value.
|
||||
*
|
||||
* @param label
|
||||
*/
|
||||
public AxisValue setLabel(String label) {
|
||||
this.label = label.toCharArray();
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getLabelAsChars() {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom label for this axis value.
|
||||
*
|
||||
* @param label
|
||||
*/
|
||||
@Deprecated
|
||||
public AxisValue setLabel(char[] label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
AxisValue axisValue = (AxisValue) o;
|
||||
|
||||
if (Float.compare(axisValue.value, value) != 0) return false;
|
||||
if (!Arrays.equals(label, axisValue.label)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (value != +0.0f ? Float.floatToIntBits(value) : 0);
|
||||
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lecho.lib.hellocharts.formatter.BubbleChartValueFormatter;
|
||||
import lecho.lib.hellocharts.formatter.SimpleBubbleChartValueFormatter;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Data for BubbleChart.
|
||||
*/
|
||||
public class BubbleChartData extends AbstractChartData {
|
||||
public static final int DEFAULT_MIN_BUBBLE_RADIUS_DP = 6;
|
||||
public static final float DEFAULT_BUBBLE_SCALE = 1f;
|
||||
private BubbleChartValueFormatter formatter = new SimpleBubbleChartValueFormatter();
|
||||
private boolean hasLabels = false;
|
||||
private boolean hasLabelsOnlyForSelected = false;
|
||||
private int minBubbleRadius = DEFAULT_MIN_BUBBLE_RADIUS_DP;
|
||||
private float bubbleScale = DEFAULT_BUBBLE_SCALE;
|
||||
// TODO: consider Collections.emptyList()
|
||||
private List<BubbleValue> values = new ArrayList<BubbleValue>();
|
||||
|
||||
public BubbleChartData() {
|
||||
}
|
||||
|
||||
public BubbleChartData(List<BubbleValue> values) {
|
||||
setValues(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor for deep copy.
|
||||
*/
|
||||
public BubbleChartData(BubbleChartData data) {
|
||||
super(data);
|
||||
this.formatter = data.formatter;
|
||||
this.hasLabels = data.hasLabels;
|
||||
this.hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected;
|
||||
this.minBubbleRadius = data.minBubbleRadius;
|
||||
this.bubbleScale = data.bubbleScale;
|
||||
|
||||
for (BubbleValue bubbleValue : data.getValues()) {
|
||||
this.values.add(new BubbleValue(bubbleValue));
|
||||
}
|
||||
}
|
||||
|
||||
public static BubbleChartData generateDummyData() {
|
||||
final int numValues = 4;
|
||||
BubbleChartData data = new BubbleChartData();
|
||||
List<BubbleValue> values = new ArrayList<BubbleValue>(numValues);
|
||||
values.add(new BubbleValue(0, 20, 15000));
|
||||
values.add(new BubbleValue(3, 22, 20000));
|
||||
values.add(new BubbleValue(5, 25, 5000));
|
||||
values.add(new BubbleValue(7, 30, 30000));
|
||||
values.add(new BubbleValue(11, 22, 10));
|
||||
data.setValues(values);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float scale) {
|
||||
for (BubbleValue value : values) {
|
||||
value.update(scale);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
for (BubbleValue value : values) {
|
||||
value.finish();
|
||||
}
|
||||
}
|
||||
|
||||
public List<BubbleValue> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public BubbleChartData setValues(List<BubbleValue> values) {
|
||||
if (null == values) {
|
||||
this.values = new ArrayList<BubbleValue>();
|
||||
} else {
|
||||
this.values = values;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasLabels() {
|
||||
return hasLabels;
|
||||
}
|
||||
|
||||
public BubbleChartData setHasLabels(boolean hasLabels) {
|
||||
this.hasLabels = hasLabels;
|
||||
if (hasLabels) {
|
||||
hasLabelsOnlyForSelected = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setHasLabelsOnlyForSelected(boolean)
|
||||
*/
|
||||
public boolean hasLabelsOnlyForSelected() {
|
||||
return hasLabelsOnlyForSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set true if you want to show value labels only for selected value, works best when chart has
|
||||
* isValueSelectionEnabled set to true {@link Chart#setValueSelectionEnabled(boolean)}.
|
||||
*/
|
||||
public BubbleChartData setHasLabelsOnlyForSelected(boolean hasLabelsOnlyForSelected) {
|
||||
this.hasLabelsOnlyForSelected = hasLabelsOnlyForSelected;
|
||||
if (hasLabelsOnlyForSelected) {
|
||||
this.hasLabels = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns minimal bubble radius in dp.
|
||||
*
|
||||
* @see #setMinBubbleRadius(int)
|
||||
*/
|
||||
public int getMinBubbleRadius() {
|
||||
return minBubbleRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimal bubble radius in dp, helpful when you want small bubbles(bubbles with very small z values compared to
|
||||
* other bubbles) to be visible on chart, default 6dp
|
||||
*/
|
||||
public void setMinBubbleRadius(int minBubbleRadius) {
|
||||
this.minBubbleRadius = minBubbleRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bubble scale which is used to adjust bubble size.
|
||||
*
|
||||
* @see #setBubbleScale(float)
|
||||
*/
|
||||
public float getBubbleScale() {
|
||||
return bubbleScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bubble scale which is used to adjust bubble size. If you want smaller bubbles set scale {@code <0, 1>},
|
||||
* if you want bigger bubbles set scale greater than 1, default is 1.0f.
|
||||
*/
|
||||
public void setBubbleScale(float bubbleScale) {
|
||||
this.bubbleScale = bubbleScale;
|
||||
}
|
||||
|
||||
public BubbleChartValueFormatter getFormatter() {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
public BubbleChartData setFormatter(BubbleChartValueFormatter formatter) {
|
||||
if (null != formatter) {
|
||||
this.formatter = formatter;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Single value drawn as bubble on BubbleChart.
|
||||
*/
|
||||
public class BubbleValue {
|
||||
|
||||
/**
|
||||
* Current X value.
|
||||
*/
|
||||
private float x;
|
||||
/**
|
||||
* Current Y value.
|
||||
*/
|
||||
private float y;
|
||||
/**
|
||||
* Current Z value , third bubble value interpreted as bubble area.
|
||||
*/
|
||||
private float z;
|
||||
|
||||
/**
|
||||
* Origin X value, used during value animation.
|
||||
*/
|
||||
private float originX;
|
||||
/**
|
||||
* Origin Y value, used during value animation.
|
||||
*/
|
||||
private float originY;
|
||||
/**
|
||||
* Origin Z value, used during value animation.
|
||||
*/
|
||||
private float originZ;
|
||||
|
||||
/**
|
||||
* Difference between originX value and target X value.
|
||||
*/
|
||||
private float diffX;
|
||||
|
||||
/**
|
||||
* Difference between originX value and target X value.
|
||||
*/
|
||||
private float diffY;
|
||||
|
||||
/**
|
||||
* Difference between originX value and target X value.
|
||||
*/
|
||||
private float diffZ;
|
||||
private int color = ChartUtils.DEFAULT_COLOR;
|
||||
private int darkenColor = ChartUtils.DEFAULT_DARKEN_COLOR;
|
||||
private ValueShape shape = ValueShape.CIRCLE;
|
||||
private char[] label;
|
||||
|
||||
public BubbleValue() {
|
||||
set(0, 0, 0);
|
||||
}
|
||||
|
||||
public BubbleValue(float x, float y, float z) {
|
||||
set(x, y, z);
|
||||
}
|
||||
|
||||
public BubbleValue(float x, float y, float z, int color) {
|
||||
set(x, y, z);
|
||||
setColor(color);
|
||||
}
|
||||
|
||||
public BubbleValue(BubbleValue bubbleValue) {
|
||||
set(bubbleValue.x, bubbleValue.y, bubbleValue.z);
|
||||
setColor(bubbleValue.color);
|
||||
this.label = bubbleValue.label;
|
||||
}
|
||||
|
||||
public void update(float scale) {
|
||||
x = originX + diffX * scale;
|
||||
y = originY + diffY * scale;
|
||||
z = originZ + diffZ * scale;
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
set(originX + diffX, originY + diffY, originZ + diffZ);
|
||||
}
|
||||
|
||||
public BubbleValue set(float x, float y, float z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.originX = x;
|
||||
this.originY = y;
|
||||
this.originZ = z;
|
||||
this.diffX = 0;
|
||||
this.diffY = 0;
|
||||
this.diffZ = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set target values that should be reached when data animation finish then call {@link Chart#startDataAnimation()}
|
||||
*/
|
||||
public BubbleValue setTarget(float targetX, float targetY, float targetZ) {
|
||||
set(x, y, z);
|
||||
this.diffX = targetX - originX;
|
||||
this.diffY = targetY - originY;
|
||||
this.diffZ = targetZ - originZ;
|
||||
return this;
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
public float getZ() {
|
||||
return this.z;
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public BubbleValue setColor(int color) {
|
||||
this.color = color;
|
||||
this.darkenColor = ChartUtils.darkenColor(color);
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getDarkenColor() {
|
||||
return darkenColor;
|
||||
}
|
||||
|
||||
public ValueShape getShape() {
|
||||
return shape;
|
||||
}
|
||||
|
||||
public BubbleValue setShape(ValueShape shape) {
|
||||
this.shape = shape;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public char[] getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public BubbleValue setLabel(String label) {
|
||||
this.label = label.toCharArray();
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getLabelAsChars() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public BubbleValue setLabel(char[] label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BubbleValue [x=" + x + ", y=" + y + ", z=" + z + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
BubbleValue that = (BubbleValue) o;
|
||||
|
||||
if (color != that.color) return false;
|
||||
if (darkenColor != that.darkenColor) return false;
|
||||
if (Float.compare(that.diffX, diffX) != 0) return false;
|
||||
if (Float.compare(that.diffY, diffY) != 0) return false;
|
||||
if (Float.compare(that.diffZ, diffZ) != 0) return false;
|
||||
if (Float.compare(that.originX, originX) != 0) return false;
|
||||
if (Float.compare(that.originY, originY) != 0) return false;
|
||||
if (Float.compare(that.originZ, originZ) != 0) return false;
|
||||
if (Float.compare(that.x, x) != 0) return false;
|
||||
if (Float.compare(that.y, y) != 0) return false;
|
||||
if (Float.compare(that.z, z) != 0) return false;
|
||||
if (!Arrays.equals(label, that.label)) return false;
|
||||
if (shape != that.shape) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (x != +0.0f ? Float.floatToIntBits(x) : 0);
|
||||
result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0);
|
||||
result = 31 * result + (z != +0.0f ? Float.floatToIntBits(z) : 0);
|
||||
result = 31 * result + (originX != +0.0f ? Float.floatToIntBits(originX) : 0);
|
||||
result = 31 * result + (originY != +0.0f ? Float.floatToIntBits(originY) : 0);
|
||||
result = 31 * result + (originZ != +0.0f ? Float.floatToIntBits(originZ) : 0);
|
||||
result = 31 * result + (diffX != +0.0f ? Float.floatToIntBits(diffX) : 0);
|
||||
result = 31 * result + (diffY != +0.0f ? Float.floatToIntBits(diffY) : 0);
|
||||
result = 31 * result + (diffZ != +0.0f ? Float.floatToIntBits(diffZ) : 0);
|
||||
result = 31 * result + color;
|
||||
result = 31 * result + darkenColor;
|
||||
result = 31 * result + (shape != null ? shape.hashCode() : 0);
|
||||
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
|
||||
/**
|
||||
* Base interface for all chart data models.
|
||||
*/
|
||||
public interface ChartData {
|
||||
|
||||
/**
|
||||
* Updates data by scale during animation.
|
||||
*
|
||||
* @param scale value from 0 to 1.0
|
||||
*/
|
||||
public void update(float scale);
|
||||
|
||||
/**
|
||||
* Inform data that animation finished(data should be update with scale 1.0f).
|
||||
*/
|
||||
public void finish();
|
||||
|
||||
/**
|
||||
* @see #setAxisXBottom(Axis)
|
||||
*/
|
||||
public Axis getAxisXBottom();
|
||||
|
||||
/**
|
||||
* Set horizontal axis at the bottom of the chart. Pass null to remove that axis.
|
||||
*
|
||||
* @param axisX
|
||||
*/
|
||||
public void setAxisXBottom(Axis axisX);
|
||||
|
||||
/**
|
||||
* @see #setAxisYLeft(Axis)
|
||||
*/
|
||||
public Axis getAxisYLeft();
|
||||
|
||||
/**
|
||||
* Set vertical axis on the left of the chart. Pass null to remove that axis.
|
||||
*
|
||||
* @param axisY
|
||||
*/
|
||||
public void setAxisYLeft(Axis axisY);
|
||||
|
||||
/**
|
||||
* @see #setAxisXTop(Axis)
|
||||
*/
|
||||
public Axis getAxisXTop();
|
||||
|
||||
/**
|
||||
* Set horizontal axis at the top of the chart. Pass null to remove that axis.
|
||||
*
|
||||
* @param axisX
|
||||
*/
|
||||
public void setAxisXTop(Axis axisX);
|
||||
|
||||
/**
|
||||
* @see #setAxisYRight(Axis)
|
||||
*/
|
||||
public Axis getAxisYRight();
|
||||
|
||||
/**
|
||||
* Set vertical axis on the right of the chart. Pass null to remove that axis.
|
||||
*
|
||||
* @param axisY
|
||||
*/
|
||||
public void setAxisYRight(Axis axisY);
|
||||
|
||||
/**
|
||||
* Returns color used to draw value label text.
|
||||
*/
|
||||
public int getValueLabelTextColor();
|
||||
|
||||
/**
|
||||
* Set value label text color, by default Color.WHITE.
|
||||
*/
|
||||
public void setValueLabelsTextColor(int labelsTextColor);
|
||||
|
||||
/**
|
||||
* Returns text size for value label in SP units.
|
||||
*/
|
||||
public int getValueLabelTextSize();
|
||||
|
||||
/**
|
||||
* Set text size for value label in SP units.
|
||||
*/
|
||||
public void setValueLabelTextSize(int labelsTextSize);
|
||||
|
||||
/**
|
||||
* Returns Typeface for value labels.
|
||||
*
|
||||
* @return Typeface or null if Typeface is not set.
|
||||
*/
|
||||
public Typeface getValueLabelTypeface();
|
||||
|
||||
/**
|
||||
* Set Typeface for all values labels.
|
||||
*
|
||||
* @param typeface
|
||||
*/
|
||||
public void setValueLabelTypeface(Typeface typeface);
|
||||
|
||||
/**
|
||||
* @see #setValueLabelBackgroundEnabled(boolean)
|
||||
*/
|
||||
public boolean isValueLabelBackgroundEnabled();
|
||||
|
||||
/**
|
||||
* Set whether labels should have rectangle background. Default is true.
|
||||
*/
|
||||
public void setValueLabelBackgroundEnabled(boolean isValueLabelBackgroundEnabled);
|
||||
|
||||
/**
|
||||
* @see #setValueLabelBackgroundAuto(boolean)
|
||||
*/
|
||||
public boolean isValueLabelBackgroundAuto();
|
||||
|
||||
/**
|
||||
* Set false if you want to set custom color for all value labels. Default is true.
|
||||
*/
|
||||
public void setValueLabelBackgroundAuto(boolean isValueLabelBackgrountAuto);
|
||||
|
||||
/**
|
||||
* @see #setValueLabelBackgroundColor(int)
|
||||
*/
|
||||
public int getValueLabelBackgroundColor();
|
||||
|
||||
/**
|
||||
* Set value labels background. This value is used only if isValueLabelBackgroundAuto returns false. Default is
|
||||
* green.
|
||||
*/
|
||||
public void setValueLabelBackgroundColor(int valueLabelBackgroundColor);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lecho.lib.hellocharts.formatter.ColumnChartValueFormatter;
|
||||
import lecho.lib.hellocharts.formatter.SimpleColumnChartValueFormatter;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Single column for ColumnChart. One column can be divided into multiple sub-columns(ColumnValues) especially for
|
||||
* stacked ColumnChart.
|
||||
* Note: you can set X value for columns or sub-columns, columns are by default indexed from 0 to numOfColumns-1 and
|
||||
* column index is used as column X value, so first column has X value 0, second clumn has X value 1 etc.
|
||||
* If you want to display AxisValue for given column you should initialize AxisValue with X value of that column.
|
||||
*/
|
||||
public class Column {
|
||||
private boolean hasLabels = false;
|
||||
private boolean hasLabelsOnlyForSelected = false;
|
||||
private ColumnChartValueFormatter formatter = new SimpleColumnChartValueFormatter();
|
||||
// TODO: consider Collections.emptyList()
|
||||
private List<SubcolumnValue> values = new ArrayList<SubcolumnValue>();
|
||||
|
||||
public Column() {
|
||||
|
||||
}
|
||||
|
||||
public Column(List<SubcolumnValue> values) {
|
||||
setValues(values);
|
||||
}
|
||||
|
||||
public Column(Column column) {
|
||||
this.hasLabels = column.hasLabels;
|
||||
this.hasLabelsOnlyForSelected = column.hasLabelsOnlyForSelected;
|
||||
this.formatter = column.formatter;
|
||||
|
||||
for (SubcolumnValue columnValue : column.values) {
|
||||
this.values.add(new SubcolumnValue(columnValue));
|
||||
}
|
||||
}
|
||||
|
||||
public void update(float scale) {
|
||||
for (SubcolumnValue value : values) {
|
||||
value.update(scale);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
for (SubcolumnValue value : values) {
|
||||
value.finish();
|
||||
}
|
||||
}
|
||||
|
||||
public List<SubcolumnValue> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public Column setValues(List<SubcolumnValue> values) {
|
||||
if (null == values) {
|
||||
this.values = new ArrayList<SubcolumnValue>();
|
||||
} else {
|
||||
this.values = values;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasLabels() {
|
||||
return hasLabels;
|
||||
}
|
||||
|
||||
public Column setHasLabels(boolean hasLabels) {
|
||||
this.hasLabels = hasLabels;
|
||||
if (hasLabels) {
|
||||
this.hasLabelsOnlyForSelected = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setHasLabelsOnlyForSelected(boolean)
|
||||
*/
|
||||
public boolean hasLabelsOnlyForSelected() {
|
||||
return hasLabelsOnlyForSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set true if you want to show value labels only for selected value, works best when chart has
|
||||
* isValueSelectionEnabled set to true {@link Chart#setValueSelectionEnabled(boolean)}.
|
||||
*/
|
||||
public Column setHasLabelsOnlyForSelected(boolean hasLabelsOnlyForSelected) {
|
||||
this.hasLabelsOnlyForSelected = hasLabelsOnlyForSelected;
|
||||
if (hasLabelsOnlyForSelected) {
|
||||
this.hasLabels = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ColumnChartValueFormatter getFormatter() {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
public Column setFormatter(ColumnChartValueFormatter formatter) {
|
||||
if (null != formatter) {
|
||||
this.formatter = formatter;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data model for column chart. Note: you can set X value for columns or sub-columns, columns are by default indexed
|
||||
* from 0 to numOfColumns-1 and
|
||||
* column index is used as column X value, so first column has X value 0, second clumn has X value 1 etc.
|
||||
* If you want to display AxisValue for given column you should initialize AxisValue with X value of that column.
|
||||
*/
|
||||
public class ColumnChartData extends AbstractChartData {
|
||||
public static final float DEFAULT_FILL_RATIO = 0.75f;
|
||||
public static final float DEFAULT_BASE_VALUE = 0.0f;
|
||||
private float fillRatio = DEFAULT_FILL_RATIO;
|
||||
private float baseValue = DEFAULT_BASE_VALUE;
|
||||
private List<Column> columns = new ArrayList<Column>();
|
||||
private boolean isStacked = false;
|
||||
|
||||
public ColumnChartData() {
|
||||
}
|
||||
|
||||
public ColumnChartData(List<Column> columns) {
|
||||
setColumns(columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor for deep copy.
|
||||
*/
|
||||
public ColumnChartData(ColumnChartData data) {
|
||||
super(data);
|
||||
this.isStacked = data.isStacked;
|
||||
this.fillRatio = data.fillRatio;
|
||||
|
||||
for (Column column : data.columns) {
|
||||
this.columns.add(new Column(column));
|
||||
}
|
||||
}
|
||||
|
||||
public static ColumnChartData generateDummyData() {
|
||||
final int numColumns = 4;
|
||||
ColumnChartData data = new ColumnChartData();
|
||||
List<Column> columns = new ArrayList<Column>(numColumns);
|
||||
List<SubcolumnValue> values;
|
||||
Column column;
|
||||
for (int i = 1; i <= numColumns; ++i) {
|
||||
values = new ArrayList<SubcolumnValue>(numColumns);
|
||||
values.add(new SubcolumnValue(i));
|
||||
column = new Column(values);
|
||||
columns.add(column);
|
||||
}
|
||||
|
||||
data.setColumns(columns);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float scale) {
|
||||
for (Column column : columns) {
|
||||
column.update(scale);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
for (Column column : columns) {
|
||||
column.finish();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Column> getColumns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
public ColumnChartData setColumns(List<Column> columns) {
|
||||
if (null == columns) {
|
||||
this.columns = new ArrayList<Column>();
|
||||
} else {
|
||||
this.columns = columns;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isStacked() {
|
||||
return isStacked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set true if you want stacked column chart.
|
||||
*
|
||||
* @param isStacked
|
||||
* @return
|
||||
*/
|
||||
public ColumnChartData setStacked(boolean isStacked) {
|
||||
this.isStacked = isStacked;
|
||||
return this;
|
||||
}
|
||||
|
||||
public float getFillRatio() {
|
||||
return fillRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fill ration for columns, value from 0 to 1, 1 means that there will be almost no free space between columns,
|
||||
* 0 means that columns will have minimum width(2px).
|
||||
*
|
||||
* @param fillRatio
|
||||
* @return
|
||||
*/
|
||||
public ColumnChartData setFillRatio(float fillRatio) {
|
||||
if (fillRatio < 0) {
|
||||
fillRatio = 0;
|
||||
}
|
||||
if (fillRatio > 1) {
|
||||
fillRatio = 1;
|
||||
}
|
||||
this.fillRatio = fillRatio;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setBaseValue(float)
|
||||
*/
|
||||
public float getBaseValue() {
|
||||
return baseValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value below which values will be drawn as negative, by default 0.
|
||||
*/
|
||||
public ColumnChartData setBaseValue(float baseValue) {
|
||||
this.baseValue = baseValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
/**
|
||||
* Data model for combo line-column chart. It uses ColumnChartData and LineChartData internally.
|
||||
*/
|
||||
public class ComboLineColumnChartData extends AbstractChartData {
|
||||
|
||||
private ColumnChartData columnChartData;
|
||||
private LineChartData lineChartData;
|
||||
|
||||
public ComboLineColumnChartData() {
|
||||
this.columnChartData = new ColumnChartData();
|
||||
this.lineChartData = new LineChartData();
|
||||
}
|
||||
|
||||
public ComboLineColumnChartData(ColumnChartData columnChartData, LineChartData lineChartData) {
|
||||
setColumnChartData(columnChartData);
|
||||
setLineChartData(lineChartData);
|
||||
}
|
||||
|
||||
public ComboLineColumnChartData(ComboLineColumnChartData data) {
|
||||
super(data);
|
||||
|
||||
setColumnChartData(new ColumnChartData(data.getColumnChartData()));
|
||||
setLineChartData(new LineChartData(data.getLineChartData()));
|
||||
}
|
||||
|
||||
public static ComboLineColumnChartData generateDummyData() {
|
||||
ComboLineColumnChartData data = new ComboLineColumnChartData();
|
||||
data.setColumnChartData(ColumnChartData.generateDummyData());
|
||||
data.setLineChartData(LineChartData.generateDummyData());
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float scale) {
|
||||
columnChartData.update(scale);
|
||||
lineChartData.update(scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
columnChartData.finish();
|
||||
lineChartData.finish();
|
||||
}
|
||||
|
||||
public ColumnChartData getColumnChartData() {
|
||||
return columnChartData;
|
||||
}
|
||||
|
||||
public void setColumnChartData(ColumnChartData columnChartData) {
|
||||
if (null == columnChartData) {
|
||||
this.columnChartData = new ColumnChartData();
|
||||
} else {
|
||||
this.columnChartData = columnChartData;
|
||||
}
|
||||
}
|
||||
|
||||
public LineChartData getLineChartData() {
|
||||
return lineChartData;
|
||||
}
|
||||
|
||||
public void setLineChartData(LineChartData lineChartData) {
|
||||
if (null == lineChartData) {
|
||||
this.lineChartData = new LineChartData();
|
||||
} else {
|
||||
this.lineChartData = lineChartData;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import android.graphics.PathEffect;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lecho.lib.hellocharts.formatter.LineChartValueFormatter;
|
||||
import lecho.lib.hellocharts.formatter.SimpleLineChartValueFormatter;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Single line for line chart.
|
||||
*/
|
||||
public class Line {
|
||||
private static final int DEFAULT_LINE_STROKE_WIDTH_DP = 3;
|
||||
private static final int DEFAULT_POINT_RADIUS_DP = 6;
|
||||
private static final int DEFAULT_AREA_TRANSPARENCY = 64;
|
||||
public static final int UNINITIALIZED = 0;
|
||||
private int color = ChartUtils.DEFAULT_COLOR;
|
||||
private int pointColor = UNINITIALIZED;
|
||||
private int darkenColor = ChartUtils.DEFAULT_DARKEN_COLOR;
|
||||
/**
|
||||
* Transparency of area when line is filled. *
|
||||
*/
|
||||
private int areaTransparency = DEFAULT_AREA_TRANSPARENCY;
|
||||
private int strokeWidth = DEFAULT_LINE_STROKE_WIDTH_DP;
|
||||
private int pointRadius = DEFAULT_POINT_RADIUS_DP;
|
||||
private boolean hasGradientToTransparent = false;
|
||||
private boolean hasPoints = true;
|
||||
private boolean hasLines = true;
|
||||
private boolean hasLabels = false;
|
||||
private boolean hasLabelsOnlyForSelected = false;
|
||||
private boolean isCubic = false;
|
||||
private boolean isSquare = false;
|
||||
private boolean isFilled = false;
|
||||
private ValueShape shape = ValueShape.CIRCLE;
|
||||
private PathEffect pathEffect;
|
||||
private LineChartValueFormatter formatter = new SimpleLineChartValueFormatter();
|
||||
private List<PointValue> values = new ArrayList<PointValue>();
|
||||
|
||||
public Line() {
|
||||
|
||||
}
|
||||
|
||||
public Line(List<PointValue> values) {
|
||||
setValues(values);
|
||||
}
|
||||
|
||||
public Line(Line line) {
|
||||
this.color = line.color;
|
||||
this.pointColor = line.pointColor;
|
||||
this.darkenColor = line.darkenColor;
|
||||
this.areaTransparency = line.areaTransparency;
|
||||
this.strokeWidth = line.strokeWidth;
|
||||
this.pointRadius = line.pointRadius;
|
||||
this.hasGradientToTransparent = line.hasGradientToTransparent;
|
||||
this.hasPoints = line.hasPoints;
|
||||
this.hasLines = line.hasLines;
|
||||
this.hasLabels = line.hasLabels;
|
||||
this.hasLabelsOnlyForSelected = line.hasLabelsOnlyForSelected;
|
||||
this.isSquare = line.isSquare;
|
||||
this.isCubic = line.isCubic;
|
||||
this.isFilled = line.isFilled;
|
||||
this.shape = line.shape;
|
||||
this.pathEffect = line.pathEffect;
|
||||
this.formatter = line.formatter;
|
||||
|
||||
for (PointValue pointValue : line.values) {
|
||||
this.values.add(new PointValue(pointValue));
|
||||
}
|
||||
}
|
||||
|
||||
public void update(float scale) {
|
||||
for (PointValue value : values) {
|
||||
value.update(scale);
|
||||
}
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
for (PointValue value : values) {
|
||||
value.finish();
|
||||
}
|
||||
}
|
||||
|
||||
public List<PointValue> getValues() {
|
||||
return this.values;
|
||||
}
|
||||
|
||||
public void setValues(List<PointValue> values) {
|
||||
if (null == values) {
|
||||
this.values = new ArrayList<PointValue>();
|
||||
} else {
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public Line setColor(int color) {
|
||||
this.color = color;
|
||||
if (pointColor == UNINITIALIZED) {
|
||||
this.darkenColor = ChartUtils.darkenColor(color);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getPointColor() {
|
||||
if (pointColor == UNINITIALIZED) {
|
||||
return color;
|
||||
} else {
|
||||
return pointColor;
|
||||
}
|
||||
}
|
||||
|
||||
public Line setPointColor(int pointColor) {
|
||||
this.pointColor = pointColor;
|
||||
if (pointColor == UNINITIALIZED) {
|
||||
this.darkenColor = ChartUtils.darkenColor(color);
|
||||
} else {
|
||||
this.darkenColor = ChartUtils.darkenColor(pointColor);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getDarkenColor() {
|
||||
return darkenColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setAreaTransparency(int)
|
||||
*/
|
||||
public int getAreaTransparency() {
|
||||
return areaTransparency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set area transparency(255 is full opacity) for filled lines
|
||||
*
|
||||
* @param areaTransparency
|
||||
* @return
|
||||
*/
|
||||
public Line setAreaTransparency(int areaTransparency) {
|
||||
this.areaTransparency = areaTransparency;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getStrokeWidth() {
|
||||
return strokeWidth;
|
||||
}
|
||||
|
||||
public Line setStrokeWidth(int strokeWidth) {
|
||||
this.strokeWidth = strokeWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasPoints() {
|
||||
return hasPoints;
|
||||
}
|
||||
|
||||
public Line setHasPoints(boolean hasPoints) {
|
||||
this.hasPoints = hasPoints;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasLines() {
|
||||
return hasLines;
|
||||
}
|
||||
|
||||
public Line setHasLines(boolean hasLines) {
|
||||
this.hasLines = hasLines;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasLabels() {
|
||||
return hasLabels;
|
||||
}
|
||||
|
||||
public Line setHasLabels(boolean hasLabels) {
|
||||
this.hasLabels = hasLabels;
|
||||
if (hasLabels) {
|
||||
this.hasLabelsOnlyForSelected = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setHasLabelsOnlyForSelected(boolean)
|
||||
*/
|
||||
public boolean hasLabelsOnlyForSelected() {
|
||||
return hasLabelsOnlyForSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set true if you want to show value labels only for selected value, works best when chart has
|
||||
* isValueSelectionEnabled set to true {@link Chart#setValueSelectionEnabled(boolean)}.
|
||||
*/
|
||||
public Line setHasLabelsOnlyForSelected(boolean hasLabelsOnlyForSelected) {
|
||||
this.hasLabelsOnlyForSelected = hasLabelsOnlyForSelected;
|
||||
if (hasLabelsOnlyForSelected) {
|
||||
this.hasLabels = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getPointRadius() {
|
||||
return pointRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set radius for points for this line.
|
||||
*
|
||||
* @param pointRadius
|
||||
* @return
|
||||
*/
|
||||
public Line setPointRadius(int pointRadius) {
|
||||
this.pointRadius = pointRadius;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean getGradientToTransparent() {
|
||||
return hasGradientToTransparent;
|
||||
}
|
||||
|
||||
public Line setHasGradientToTransparent(boolean hasGradientToTransparent) {
|
||||
this.hasGradientToTransparent = hasGradientToTransparent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isCubic() {
|
||||
return isCubic;
|
||||
}
|
||||
|
||||
public Line setCubic(boolean isCubic) {
|
||||
this.isCubic = isCubic;
|
||||
if (isSquare)
|
||||
setSquare(false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isSquare() {
|
||||
return isSquare;
|
||||
}
|
||||
|
||||
public Line setSquare(boolean isSquare) {
|
||||
this.isSquare = isSquare;
|
||||
if (isCubic)
|
||||
setCubic(false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isFilled() {
|
||||
return isFilled;
|
||||
}
|
||||
|
||||
public Line setFilled(boolean isFilled) {
|
||||
this.isFilled = isFilled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setShape(ValueShape)
|
||||
*/
|
||||
public ValueShape getShape() {
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set shape for points, possible values: SQUARE, CIRCLE
|
||||
*
|
||||
* @param shape
|
||||
* @return
|
||||
*/
|
||||
public Line setShape(ValueShape shape) {
|
||||
this.shape = shape;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathEffect getPathEffect() {
|
||||
return pathEffect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set path effect for this line, note: it will slow down drawing, try to not use complicated effects,
|
||||
* DashPathEffect should be safe choice.
|
||||
*
|
||||
* @param pathEffect
|
||||
*/
|
||||
public void setPathEffect(PathEffect pathEffect) {
|
||||
this.pathEffect = pathEffect;
|
||||
}
|
||||
|
||||
public LineChartValueFormatter getFormatter() {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
public Line setFormatter(LineChartValueFormatter formatter) {
|
||||
if (null != formatter) {
|
||||
this.formatter = formatter;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data model for LineChartView.
|
||||
*/
|
||||
public class LineChartData extends AbstractChartData {
|
||||
public static final float DEFAULT_BASE_VALUE = 0.0f;
|
||||
|
||||
private List<Line> lines = new ArrayList<Line>();
|
||||
private float baseValue = DEFAULT_BASE_VALUE;
|
||||
|
||||
public LineChartData() {
|
||||
|
||||
}
|
||||
|
||||
public LineChartData(List<Line> lines) {
|
||||
setLines(lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor to perform deep copy of chart data.
|
||||
*/
|
||||
public LineChartData(LineChartData data) {
|
||||
super(data);
|
||||
this.baseValue = data.baseValue;
|
||||
|
||||
for (Line line : data.lines) {
|
||||
this.lines.add(new Line(line));
|
||||
}
|
||||
}
|
||||
|
||||
public static LineChartData generateDummyData() {
|
||||
final int numValues = 4;
|
||||
LineChartData data = new LineChartData();
|
||||
List<PointValue> values = new ArrayList<PointValue>(numValues);
|
||||
values.add(new PointValue(0, 2));
|
||||
values.add(new PointValue(1, 4));
|
||||
values.add(new PointValue(2, 3));
|
||||
values.add(new PointValue(3, 4));
|
||||
Line line = new Line(values);
|
||||
List<Line> lines = new ArrayList<Line>(1);
|
||||
lines.add(line);
|
||||
data.setLines(lines);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float scale) {
|
||||
for (Line line : lines) {
|
||||
line.update(scale);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
for (Line line : lines) {
|
||||
line.finish();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Line> getLines() {
|
||||
return lines;
|
||||
}
|
||||
|
||||
public LineChartData setLines(List<Line> lines) {
|
||||
if (null == lines) {
|
||||
this.lines = new ArrayList<Line>();
|
||||
} else {
|
||||
this.lines = lines;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setBaseValue(float)
|
||||
*/
|
||||
public float getBaseValue() {
|
||||
return baseValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value below which values will be drawn as negative, important attribute for drawing filled area charts, by
|
||||
* default 0.
|
||||
*/
|
||||
public LineChartData setBaseValue(float baseValue) {
|
||||
this.baseValue = baseValue;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lecho.lib.hellocharts.formatter.PieChartValueFormatter;
|
||||
import lecho.lib.hellocharts.formatter.SimplePieChartValueFormatter;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
import lecho.lib.hellocharts.view.PieChartView;
|
||||
|
||||
/**
|
||||
* Data for PieChart, by default it doesn't have axes.
|
||||
*/
|
||||
public class PieChartData extends AbstractChartData {
|
||||
public static final int DEFAULT_CENTER_TEXT1_SIZE_SP = 42;
|
||||
public static final int DEFAULT_CENTER_TEXT2_SIZE_SP = 16;
|
||||
public static final float DEFAULT_CENTER_CIRCLE_SCALE = 0.6f;
|
||||
private static final int DEFAULT_SLICE_SPACING_DP = 2;
|
||||
private int centerText1FontSize = DEFAULT_CENTER_TEXT1_SIZE_SP;
|
||||
private int centerText2FontSize = DEFAULT_CENTER_TEXT2_SIZE_SP;
|
||||
private float centerCircleScale = DEFAULT_CENTER_CIRCLE_SCALE;
|
||||
private int slicesSpacing = DEFAULT_SLICE_SPACING_DP;
|
||||
private PieChartValueFormatter formatter = new SimplePieChartValueFormatter();
|
||||
private boolean hasLabels = false;
|
||||
private boolean hasLabelsOnlyForSelected = false;
|
||||
private boolean hasLabelsOutside = false;
|
||||
private boolean hasCenterCircle = false;
|
||||
private int centerCircleColor = Color.TRANSPARENT;
|
||||
private int centerText1Color = Color.BLACK;
|
||||
private Typeface centerText1Typeface;
|
||||
private String centerText1;
|
||||
private int centerText2Color = Color.BLACK;
|
||||
private Typeface centerText2Typeface;
|
||||
private String centerText2;
|
||||
|
||||
private List<SliceValue> values = new ArrayList<SliceValue>();
|
||||
|
||||
public PieChartData() {
|
||||
setAxisXBottom(null);
|
||||
setAxisYLeft(null);
|
||||
}
|
||||
|
||||
public PieChartData(List<SliceValue> values) {
|
||||
setValues(values);
|
||||
// Empty axes. Pie chart don't need axes.
|
||||
setAxisXBottom(null);
|
||||
setAxisYLeft(null);
|
||||
}
|
||||
|
||||
public PieChartData(PieChartData data) {
|
||||
super(data);
|
||||
this.formatter = data.formatter;
|
||||
this.hasLabels = data.hasLabels;
|
||||
this.hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected;
|
||||
this.hasLabelsOutside = data.hasLabelsOutside;
|
||||
|
||||
this.hasCenterCircle = data.hasCenterCircle;
|
||||
this.centerCircleColor = data.centerCircleColor;
|
||||
this.centerCircleScale = data.centerCircleScale;
|
||||
|
||||
this.centerText1Color = data.centerText1Color;
|
||||
this.centerText1FontSize = data.centerText1FontSize;
|
||||
this.centerText1Typeface = data.centerText1Typeface;
|
||||
this.centerText1 = data.centerText1;
|
||||
|
||||
this.centerText2Color = data.centerText2Color;
|
||||
this.centerText2FontSize = data.centerText2FontSize;
|
||||
this.centerText2Typeface = data.centerText2Typeface;
|
||||
this.centerText2 = data.centerText2;
|
||||
|
||||
for (SliceValue sliceValue : data.values) {
|
||||
this.values.add(new SliceValue(sliceValue));
|
||||
}
|
||||
}
|
||||
|
||||
public static PieChartData generateDummyData() {
|
||||
final int numValues = 4;
|
||||
PieChartData data = new PieChartData();
|
||||
List<SliceValue> values = new ArrayList<SliceValue>(numValues);
|
||||
values.add(new SliceValue(40f));
|
||||
values.add(new SliceValue(20f));
|
||||
values.add(new SliceValue(30f));
|
||||
values.add(new SliceValue(50f));
|
||||
data.setValues(values);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float scale) {
|
||||
for (SliceValue value : values) {
|
||||
value.update(scale);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
for (SliceValue value : values) {
|
||||
value.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PieChart does not support axes so method call will be ignored
|
||||
*/
|
||||
@Override
|
||||
public void setAxisXBottom(Axis axisX) {
|
||||
super.setAxisXBottom(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* PieChart does not support axes so method call will be ignored
|
||||
*/
|
||||
@Override
|
||||
public void setAxisYLeft(Axis axisY) {
|
||||
super.setAxisYLeft(null);
|
||||
}
|
||||
|
||||
public List<SliceValue> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public PieChartData setValues(List<SliceValue> values) {
|
||||
if (null == values) {
|
||||
this.values = new ArrayList<SliceValue>();
|
||||
} else {
|
||||
this.values = values;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasLabels() {
|
||||
return hasLabels;
|
||||
}
|
||||
|
||||
public PieChartData setHasLabels(boolean hasLabels) {
|
||||
this.hasLabels = hasLabels;
|
||||
if (hasLabels) {
|
||||
hasLabelsOnlyForSelected = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setHasLabelsOnlyForSelected(boolean)
|
||||
*/
|
||||
public boolean hasLabelsOnlyForSelected() {
|
||||
return hasLabelsOnlyForSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set true if you want to show value labels only for selected value, works best when chart has
|
||||
* isValueSelectionEnabled set to true {@link Chart#setValueSelectionEnabled(boolean)}.
|
||||
*/
|
||||
public PieChartData setHasLabelsOnlyForSelected(boolean hasLabelsOnlyForSelected) {
|
||||
this.hasLabelsOnlyForSelected = hasLabelsOnlyForSelected;
|
||||
if (hasLabelsOnlyForSelected) {
|
||||
this.hasLabels = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasLabelsOutside() {
|
||||
return hasLabelsOutside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if labels should be drawn inside circle(false) or outside(true). By default false. If you set it to true you
|
||||
* should also change chart fill ration using {@link PieChartView#setCircleFillRatio(float)}. This flag is used only
|
||||
* if you also set hasLabels or hasLabelsOnlyForSelected flags.
|
||||
*/
|
||||
public PieChartData setHasLabelsOutside(boolean hasLabelsOutside) {
|
||||
this.hasLabelsOutside = hasLabelsOutside;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasCenterCircle() {
|
||||
return hasCenterCircle;
|
||||
}
|
||||
|
||||
public PieChartData setHasCenterCircle(boolean hasCenterCircle) {
|
||||
this.hasCenterCircle = hasCenterCircle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCenterCircleColor() {
|
||||
return centerCircleColor;
|
||||
}
|
||||
|
||||
public PieChartData setCenterCircleColor(int centerCircleColor) {
|
||||
this.centerCircleColor = centerCircleColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public float getCenterCircleScale() {
|
||||
return centerCircleScale;
|
||||
}
|
||||
|
||||
public PieChartData setCenterCircleScale(float centerCircleScale) {
|
||||
this.centerCircleScale = centerCircleScale;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCenterText1Color() {
|
||||
return centerText1Color;
|
||||
}
|
||||
|
||||
public PieChartData setCenterText1Color(int centerText1Color) {
|
||||
this.centerText1Color = centerText1Color;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCenterText1FontSize() {
|
||||
return centerText1FontSize;
|
||||
}
|
||||
|
||||
public PieChartData setCenterText1FontSize(int centerText1FontSize) {
|
||||
this.centerText1FontSize = centerText1FontSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Typeface getCenterText1Typeface() {
|
||||
return centerText1Typeface;
|
||||
}
|
||||
|
||||
public PieChartData setCenterText1Typeface(Typeface text1Typeface) {
|
||||
this.centerText1Typeface = text1Typeface;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCenterText1() {
|
||||
return centerText1;
|
||||
}
|
||||
|
||||
public PieChartData setCenterText1(String centerText1) {
|
||||
this.centerText1 = centerText1;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCenterText2() {
|
||||
return centerText2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that centerText2 will be drawn only if centerText1 is not empty/null.
|
||||
*/
|
||||
public PieChartData setCenterText2(String centerText2) {
|
||||
this.centerText2 = centerText2;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCenterText2Color() {
|
||||
return centerText2Color;
|
||||
}
|
||||
|
||||
public PieChartData setCenterText2Color(int centerText2Color) {
|
||||
this.centerText2Color = centerText2Color;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCenterText2FontSize() {
|
||||
return centerText2FontSize;
|
||||
}
|
||||
|
||||
public PieChartData setCenterText2FontSize(int centerText2FontSize) {
|
||||
this.centerText2FontSize = centerText2FontSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Typeface getCenterText2Typeface() {
|
||||
return centerText2Typeface;
|
||||
}
|
||||
|
||||
public PieChartData setCenterText2Typeface(Typeface text2Typeface) {
|
||||
this.centerText2Typeface = text2Typeface;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getSlicesSpacing() {
|
||||
return slicesSpacing;
|
||||
}
|
||||
|
||||
public PieChartData setSlicesSpacing(int sliceSpacing) {
|
||||
this.slicesSpacing = sliceSpacing;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PieChartValueFormatter getFormatter() {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
public PieChartData setFormatter(PieChartValueFormatter formatter) {
|
||||
if (null != formatter) {
|
||||
this.formatter = formatter;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Single point coordinates, used for LineChartData.
|
||||
*/
|
||||
public class PointValue {
|
||||
|
||||
private float x;
|
||||
private float y;
|
||||
private float originX;
|
||||
private float originY;
|
||||
private float diffX;
|
||||
private float diffY;
|
||||
private char[] label;
|
||||
|
||||
public PointValue() {
|
||||
set(0, 0);
|
||||
}
|
||||
|
||||
public PointValue(float x, float y) {
|
||||
set(x, y);
|
||||
}
|
||||
|
||||
public PointValue(PointValue pointValue) {
|
||||
set(pointValue.x, pointValue.y);
|
||||
this.label = pointValue.label;
|
||||
}
|
||||
|
||||
public void update(float scale) {
|
||||
x = originX + diffX * scale;
|
||||
y = originY + diffY * scale;
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
set(originX + diffX, originY + diffY);
|
||||
}
|
||||
|
||||
public PointValue set(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.originX = x;
|
||||
this.originY = y;
|
||||
this.diffX = 0;
|
||||
this.diffY = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set target values that should be reached when data animation finish then call {@link Chart#startDataAnimation()}
|
||||
*/
|
||||
public PointValue setTarget(float targetX, float targetY) {
|
||||
set(x, y);
|
||||
this.diffX = targetX - originX;
|
||||
this.diffY = targetY - originY;
|
||||
return this;
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public char[] getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public PointValue setLabel(String label) {
|
||||
this.label = label.toCharArray();
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getLabelAsChars() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public PointValue setLabel(char[] label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PointValue [x=" + x + ", y=" + y + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PointValue that = (PointValue) o;
|
||||
|
||||
if (Float.compare(that.diffX, diffX) != 0) return false;
|
||||
if (Float.compare(that.diffY, diffY) != 0) return false;
|
||||
if (Float.compare(that.originX, originX) != 0) return false;
|
||||
if (Float.compare(that.originY, originY) != 0) return false;
|
||||
if (Float.compare(that.x, x) != 0) return false;
|
||||
if (Float.compare(that.y, y) != 0) return false;
|
||||
if (!Arrays.equals(label, that.label)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (x != +0.0f ? Float.floatToIntBits(x) : 0);
|
||||
result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0);
|
||||
result = 31 * result + (originX != +0.0f ? Float.floatToIntBits(originX) : 0);
|
||||
result = 31 * result + (originY != +0.0f ? Float.floatToIntBits(originY) : 0);
|
||||
result = 31 * result + (diffX != +0.0f ? Float.floatToIntBits(diffX) : 0);
|
||||
result = 31 * result + (diffY != +0.0f ? Float.floatToIntBits(diffY) : 0);
|
||||
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
/**
|
||||
* Holds selected values indexes, i.e. for LineChartModel it will be firstIndex=lineIndex; secondIndex=valueIndex.
|
||||
*/
|
||||
public class SelectedValue {
|
||||
|
||||
/**
|
||||
* First index i.e for LineChart that will be line index.
|
||||
*/
|
||||
private int firstIndex;
|
||||
|
||||
/**
|
||||
* Second index i.e for LineChart that will be PointValue index.
|
||||
*/
|
||||
private int secondIndex;
|
||||
|
||||
/**
|
||||
* Used only for combo charts, in other cases should have value NONE.
|
||||
*/
|
||||
private SelectedValueType type = SelectedValueType.NONE;
|
||||
|
||||
public SelectedValue() {
|
||||
clear();
|
||||
}
|
||||
|
||||
public SelectedValue(int firstIndex, int secondIndex, SelectedValueType type) {
|
||||
set(firstIndex, secondIndex, type);
|
||||
}
|
||||
|
||||
public void set(int firstIndex, int secondIndex, SelectedValueType type) {
|
||||
this.firstIndex = firstIndex;
|
||||
this.secondIndex = secondIndex;
|
||||
if (null != type) {
|
||||
this.type = type;
|
||||
} else {
|
||||
this.type = SelectedValueType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public void set(SelectedValue selectedValue) {
|
||||
this.firstIndex = selectedValue.firstIndex;
|
||||
this.secondIndex = selectedValue.secondIndex;
|
||||
this.type = selectedValue.type;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
set(Integer.MIN_VALUE, Integer.MIN_VALUE, SelectedValueType.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if selected value have meaningful value.
|
||||
*/
|
||||
public boolean isSet() {
|
||||
if (firstIndex >= 0 && secondIndex >= 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First index i.e for LineChart that will be line index.
|
||||
*/
|
||||
public int getFirstIndex() {
|
||||
return firstIndex;
|
||||
}
|
||||
|
||||
public void setFirstIndex(int firstIndex) {
|
||||
this.firstIndex = firstIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Second index i.e for LineChart that will be PointValue index.
|
||||
*/
|
||||
public int getSecondIndex() {
|
||||
return secondIndex;
|
||||
}
|
||||
|
||||
public void setSecondIndex(int secondIndex) {
|
||||
this.secondIndex = secondIndex;
|
||||
}
|
||||
|
||||
public SelectedValueType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(SelectedValueType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + firstIndex;
|
||||
result = prime * result + secondIndex;
|
||||
result = prime * result + ((type == null) ? 0 : type.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
SelectedValue other = (SelectedValue) obj;
|
||||
if (firstIndex != other.firstIndex)
|
||||
return false;
|
||||
if (secondIndex != other.secondIndex)
|
||||
return false;
|
||||
if (type != other.type)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SelectedValue [firstIndex=" + firstIndex + ", secondIndex=" + secondIndex + ", type=" + type + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in combo chart to determine if selected value is used for line or column selection.
|
||||
*/
|
||||
public enum SelectedValueType {
|
||||
NONE, LINE, COLUMN
|
||||
}
|
||||
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Model representing single slice on PieChart.
|
||||
*/
|
||||
public class SliceValue {
|
||||
private static final int DEFAULT_SLICE_SPACING_DP = 2;
|
||||
@Deprecated
|
||||
/** Spacing between this slice and its neighbors. */
|
||||
private int sliceSpacing = DEFAULT_SLICE_SPACING_DP;
|
||||
/**
|
||||
* Current value of this slice.
|
||||
*/
|
||||
private float value;
|
||||
/**
|
||||
* Origin value of this slice, used during value animation.
|
||||
*/
|
||||
private float originValue;
|
||||
/**
|
||||
* Difference between originValue and targetValue.
|
||||
*/
|
||||
private float diff;
|
||||
/**
|
||||
* Color of this slice.
|
||||
*/
|
||||
private int color = ChartUtils.DEFAULT_COLOR;
|
||||
/**
|
||||
* Darken color used to draw label background and give touch feedback.
|
||||
*/
|
||||
private int darkenColor = ChartUtils.DEFAULT_DARKEN_COLOR;
|
||||
/**
|
||||
* Custom label for this slice, if not set number formatting will be used.
|
||||
*/
|
||||
private char[] label;
|
||||
|
||||
public SliceValue() {
|
||||
setValue(0);
|
||||
}
|
||||
|
||||
public SliceValue(float value) {
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
public SliceValue(float value, int color) {
|
||||
setValue(value);
|
||||
setColor(color);
|
||||
}
|
||||
|
||||
public SliceValue(float value, int color, int sliceSpacing) {
|
||||
setValue(value);
|
||||
setColor(color);
|
||||
this.sliceSpacing = sliceSpacing;
|
||||
}
|
||||
|
||||
public SliceValue(SliceValue sliceValue) {
|
||||
setValue(sliceValue.value);
|
||||
setColor(sliceValue.color);
|
||||
this.sliceSpacing = sliceValue.sliceSpacing;
|
||||
this.label = sliceValue.label;
|
||||
}
|
||||
|
||||
public void update(float scale) {
|
||||
value = originValue + diff * scale;
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
setValue(originValue + diff);
|
||||
}
|
||||
|
||||
public float getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public SliceValue setValue(float value) {
|
||||
this.value = value;
|
||||
this.originValue = value;
|
||||
this.diff = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set target value that should be reached when data animation finish then call {@link Chart#startDataAnimation()}
|
||||
*
|
||||
* @param target
|
||||
* @return
|
||||
*/
|
||||
public SliceValue setTarget(float target) {
|
||||
setValue(value);
|
||||
this.diff = target - originValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public SliceValue setColor(int color) {
|
||||
this.color = color;
|
||||
this.darkenColor = ChartUtils.darkenColor(color);
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getDarkenColor() {
|
||||
return darkenColor;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getSliceSpacing() {
|
||||
return sliceSpacing;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public SliceValue setSliceSpacing(int sliceSpacing) {
|
||||
this.sliceSpacing = sliceSpacing;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public char[] getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public SliceValue setLabel(char[] label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SliceValue setLabel(String label) {
|
||||
this.label = label.toCharArray();
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getLabelAsChars() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SliceValue [value=" + value + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
SliceValue that = (SliceValue) o;
|
||||
|
||||
if (color != that.color) return false;
|
||||
if (darkenColor != that.darkenColor) return false;
|
||||
if (Float.compare(that.diff, diff) != 0) return false;
|
||||
if (Float.compare(that.originValue, originValue) != 0) return false;
|
||||
if (sliceSpacing != that.sliceSpacing) return false;
|
||||
if (Float.compare(that.value, value) != 0) return false;
|
||||
if (!Arrays.equals(label, that.label)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (value != +0.0f ? Float.floatToIntBits(value) : 0);
|
||||
result = 31 * result + (originValue != +0.0f ? Float.floatToIntBits(originValue) : 0);
|
||||
result = 31 * result + (diff != +0.0f ? Float.floatToIntBits(diff) : 0);
|
||||
result = 31 * result + color;
|
||||
result = 31 * result + darkenColor;
|
||||
result = 31 * result + sliceSpacing;
|
||||
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Single sub-column value for ColumnChart.
|
||||
*/
|
||||
public class SubcolumnValue {
|
||||
|
||||
private float value;
|
||||
private float originValue;
|
||||
private float diff;
|
||||
private int color = ChartUtils.DEFAULT_COLOR;
|
||||
private int darkenColor = ChartUtils.DEFAULT_DARKEN_COLOR;
|
||||
private char[] label;
|
||||
|
||||
public SubcolumnValue() {
|
||||
setValue(0);
|
||||
}
|
||||
|
||||
public SubcolumnValue(float value) {
|
||||
// point and targetPoint have to be different objects
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
public SubcolumnValue(float value, int color) {
|
||||
// point and targetPoint have to be different objects
|
||||
setValue(value);
|
||||
setColor(color);
|
||||
}
|
||||
|
||||
public SubcolumnValue(SubcolumnValue columnValue) {
|
||||
setValue(columnValue.value);
|
||||
setColor(columnValue.color);
|
||||
this.label = columnValue.label;
|
||||
}
|
||||
|
||||
public void update(float scale) {
|
||||
value = originValue + diff * scale;
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
setValue(originValue + diff);
|
||||
}
|
||||
|
||||
public float getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public SubcolumnValue setValue(float value) {
|
||||
this.value = value;
|
||||
this.originValue = value;
|
||||
this.diff = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set target value that should be reached when data animation finish then call {@link Chart#startDataAnimation()}
|
||||
*
|
||||
* @param target
|
||||
* @return
|
||||
*/
|
||||
public SubcolumnValue setTarget(float target) {
|
||||
setValue(value);
|
||||
this.diff = target - originValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public SubcolumnValue setColor(int color) {
|
||||
this.color = color;
|
||||
this.darkenColor = ChartUtils.darkenColor(color);
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getDarkenColor() {
|
||||
return darkenColor;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public char[] getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public SubcolumnValue setLabel(String label) {
|
||||
this.label = label.toCharArray();
|
||||
return this;
|
||||
}
|
||||
|
||||
public char[] getLabelAsChars() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public SubcolumnValue setLabel(char[] label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ColumnValue [value=" + value + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
SubcolumnValue that = (SubcolumnValue) o;
|
||||
|
||||
if (color != that.color) return false;
|
||||
if (darkenColor != that.darkenColor) return false;
|
||||
if (Float.compare(that.diff, diff) != 0) return false;
|
||||
if (Float.compare(that.originValue, originValue) != 0) return false;
|
||||
if (Float.compare(that.value, value) != 0) return false;
|
||||
if (!Arrays.equals(label, that.label)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (value != +0.0f ? Float.floatToIntBits(value) : 0);
|
||||
result = 31 * result + (originValue != +0.0f ? Float.floatToIntBits(originValue) : 0);
|
||||
result = 31 * result + (diff != +0.0f ? Float.floatToIntBits(diff) : 0);
|
||||
result = 31 * result + color;
|
||||
result = 31 * result + darkenColor;
|
||||
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
public enum ValueShape {
|
||||
CIRCLE, SQUARE, DIAMOND
|
||||
}
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
package lecho.lib.hellocharts.model;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Partial copy of android.graphics.Rect but here the top should be greater then the bottom. Viewport holds 4 float
|
||||
* coordinates for a chart extremes. The viewport is represented by the coordinates of its 4 edges (left, top, right
|
||||
* bottom). These fields can be accessed directly. Use width() and height() to retrieve the viewport's width and height.
|
||||
* Note: most methods do not check to see that the coordinates are sorted correctly (i.e. left is less than right and
|
||||
* bottom is less than top). Viewport implements Parcerable.
|
||||
*/
|
||||
public class Viewport implements Parcelable {
|
||||
|
||||
public float left;
|
||||
public float top;
|
||||
public float right;
|
||||
public float bottom;
|
||||
public static final Parcelable.Creator<Viewport> CREATOR = new Parcelable.Creator<Viewport>() {
|
||||
/**
|
||||
* Return a new viewport from the data in the specified parcel.
|
||||
*/
|
||||
public Viewport createFromParcel(Parcel in) {
|
||||
Viewport v = new Viewport();
|
||||
v.readFromParcel(in);
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of viewports of the specified size.
|
||||
*/
|
||||
public Viewport[] newArray(int size) {
|
||||
return new Viewport[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new empty Viewport. All coordinates are initialized to 0.
|
||||
*/
|
||||
public Viewport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new viewport with the specified coordinates. Note: no range checking is performed, so the caller must
|
||||
* ensure that left is less than right and bottom is less than top.
|
||||
*
|
||||
* @param left The X coordinate of the left side of the viewport
|
||||
* @param top The Y coordinate of the top of the viewport
|
||||
* @param right The X coordinate of the right side of the viewport
|
||||
* @param bottom The Y coordinate of the bottom of the viewport
|
||||
*/
|
||||
public Viewport(float left, float top, float right, float bottom) {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.right = right;
|
||||
this.bottom = bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new viewport, initialized with the values in the specified viewport (which is left unmodified).
|
||||
*
|
||||
* @param v The viewport whose coordinates are copied into the new viewport.
|
||||
*/
|
||||
public Viewport(Viewport v) {
|
||||
if (v == null) {
|
||||
left = top = right = bottom = 0.0f;
|
||||
} else {
|
||||
left = v.left;
|
||||
top = v.top;
|
||||
right = v.right;
|
||||
bottom = v.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Viewport other = (Viewport) obj;
|
||||
if (Float.floatToIntBits(bottom) != Float.floatToIntBits(other.bottom))
|
||||
return false;
|
||||
if (Float.floatToIntBits(left) != Float.floatToIntBits(other.left))
|
||||
return false;
|
||||
if (Float.floatToIntBits(right) != Float.floatToIntBits(other.right))
|
||||
return false;
|
||||
if (Float.floatToIntBits(top) != Float.floatToIntBits(other.top))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the viewport is empty {@code left >= right or bottom >= top}
|
||||
*/
|
||||
public final boolean isEmpty() {
|
||||
return left >= right || bottom >= top;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the viewport to (0,0,0,0)
|
||||
*/
|
||||
public void setEmpty() {
|
||||
left = right = top = bottom = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the viewport's width. This does not check for a valid viewport (i.e. {@code left <= right}) so the
|
||||
* result may be negative.
|
||||
*/
|
||||
public final float width() {
|
||||
return right - left;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the viewport's height. This does not check for a valid viewport (i.e. {@code top <= bottom}) so the
|
||||
* result may be negative.
|
||||
*/
|
||||
public final float height() {
|
||||
return top - bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the horizontal center of the viewport. This does not check for a valid viewport (i.e. {@code left <=
|
||||
* right})
|
||||
*/
|
||||
public final float centerX() {
|
||||
return (left + right) * 0.5f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the vertical center of the viewport. This does not check for a valid viewport (i.e. {@code bottom <=
|
||||
* top})
|
||||
*/
|
||||
public final float centerY() {
|
||||
return (top + bottom) * 0.5f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the viewport's coordinates to the specified values. Note: no range checking is performed, so it is up to the
|
||||
* caller to ensure that {@code left <= right and bottom <= top}.
|
||||
*
|
||||
* @param left The X coordinate of the left side of the viewport
|
||||
* @param top The Y coordinate of the top of the viewport
|
||||
* @param right The X coordinate of the right side of the viewport
|
||||
* @param bottom The Y coordinate of the bottom of the viewport
|
||||
*/
|
||||
public void set(float left, float top, float right, float bottom) {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.right = right;
|
||||
this.bottom = bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the coordinates from src into this viewport.
|
||||
*
|
||||
* @param src The viewport whose coordinates are copied into this viewport.
|
||||
*/
|
||||
public void set(Viewport src) {
|
||||
this.left = src.left;
|
||||
this.top = src.top;
|
||||
this.right = src.right;
|
||||
this.bottom = src.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset the viewport by adding dx to its left and right coordinates, and adding dy to its top and bottom
|
||||
* coordinates.
|
||||
*
|
||||
* @param dx The amount to add to the viewport's left and right coordinates
|
||||
* @param dy The amount to add to the viewport's top and bottom coordinates
|
||||
*/
|
||||
public void offset(float dx, float dy) {
|
||||
left += dx;
|
||||
top += dy;
|
||||
right += dx;
|
||||
bottom += dy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset the viewport to a specific (left, top) position, keeping its width and height the same.
|
||||
*
|
||||
* @param newLeft The new "left" coordinate for the viewport
|
||||
* @param newTop The new "top" coordinate for the viewport
|
||||
*/
|
||||
public void offsetTo(float newLeft, float newTop) {
|
||||
right += newLeft - left;
|
||||
bottom += newTop - top;
|
||||
left = newLeft;
|
||||
top = newTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inset the viewport by (dx,dy). If dx is positive, then the sides are moved inwards, making the viewport narrower.
|
||||
* If dx is negative, then the sides are moved outwards, making the viewport wider. The same holds true for dy and
|
||||
* the top and bottom.
|
||||
*
|
||||
* @param dx The amount to add(subtract) from the viewport's left(right)
|
||||
* @param dy The amount to add(subtract) from the viewport's top(bottom)
|
||||
*/
|
||||
public void inset(float dx, float dy) {
|
||||
left += dx;
|
||||
top -= dy;
|
||||
right -= dx;
|
||||
bottom += dy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if (x,y) is inside the viewport. The left and top are considered to be inside, while the right and
|
||||
* bottom are not. This means that for a x,y to be contained: {@code left <= x < right and bottom <= y < top}. An
|
||||
* empty viewport never contains any point.
|
||||
*
|
||||
* @param x The X coordinate of the point being tested for containment
|
||||
* @param y The Y coordinate of the point being tested for containment
|
||||
* @return true iff (x,y) are contained by the viewport, where containment means {@code left <= x < right and top <=
|
||||
* y < bottom}
|
||||
*/
|
||||
public boolean contains(float x, float y) {
|
||||
return left < right && bottom < top // check for empty first
|
||||
&& x >= left && x < right && y >= bottom && y < top;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the 4 specified sides of a viewport are inside or equal to this viewport. i.e. is this viewport
|
||||
* a superset of the specified viewport. An empty viewport never contains another viewport.
|
||||
*
|
||||
* @param left The left side of the viewport being tested for containment
|
||||
* @param top The top of the viewport being tested for containment
|
||||
* @param right The right side of the viewport being tested for containment
|
||||
* @param bottom The bottom of the viewport being tested for containment
|
||||
* @return true iff the the 4 specified sides of a viewport are inside or equal to this viewport
|
||||
*/
|
||||
public boolean contains(float left, float top, float right, float bottom) {
|
||||
// check for empty first
|
||||
return this.left < this.right && this.bottom < this.top
|
||||
// now check for containment
|
||||
&& this.left <= left && this.top >= top && this.right >= right && this.bottom <= bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the specified viewport r is inside or equal to this viewport. An empty viewport never contains
|
||||
* another viewport.
|
||||
*
|
||||
* @param v The viewport being tested for containment.
|
||||
* @return true iff the specified viewport r is inside or equal to this viewport
|
||||
*/
|
||||
public boolean contains(Viewport v) {
|
||||
// check for empty first
|
||||
return this.left < this.right && this.bottom < this.top
|
||||
// now check for containment
|
||||
&& left <= v.left && top >= v.top && right >= v.right && bottom <= v.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this Viewport to enclose itself and the specified viewport. If the specified viewport is empty, nothing is
|
||||
* done. If this viewport is empty it is set to the specified viewport.
|
||||
*
|
||||
* @param left The left edge being unioned with this viewport
|
||||
* @param top The top edge being unioned with this viewport
|
||||
* @param right The right edge being unioned with this viewport
|
||||
* @param bottom The bottom edge being unioned with this viewport
|
||||
*/
|
||||
public void union(float left, float top, float right, float bottom) {
|
||||
if ((left < right) && (bottom < top)) {
|
||||
if ((this.left < this.right) && (this.bottom < this.top)) {
|
||||
if (this.left > left)
|
||||
this.left = left;
|
||||
if (this.top < top)
|
||||
this.top = top;
|
||||
if (this.right < right)
|
||||
this.right = right;
|
||||
if (this.bottom > bottom)
|
||||
this.bottom = bottom;
|
||||
} else {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.right = right;
|
||||
this.bottom = bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this Viewport to enclose itself and the specified viewport. If the specified viewport is empty, nothing is
|
||||
* done. If this viewport is empty it is set to the specified viewport.
|
||||
*
|
||||
* @param v The viewport being unioned with this viewport
|
||||
*/
|
||||
public void union(Viewport v) {
|
||||
union(v.left, v.top, v.right, v.bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the viewport specified by left,top,right,bottom intersects this viewport, return true and set this viewport to
|
||||
* that intersection, otherwise return false and do not change this viewport. No check is performed to see if either
|
||||
* viewport is empty. Note: To just test for intersection, use intersects()
|
||||
*
|
||||
* @param left The left side of the viewport being intersected with this viewport
|
||||
* @param top The top of the viewport being intersected with this viewport
|
||||
* @param right The right side of the viewport being intersected with this viewport.
|
||||
* @param bottom The bottom of the viewport being intersected with this viewport.
|
||||
* @return true if the specified viewport and this viewport intersect (and this viewport is then set to that
|
||||
* intersection) else return false and do not change this viewport.
|
||||
*/
|
||||
public boolean intersect(float left, float top, float right, float bottom) {
|
||||
if (this.left < right && left < this.right && this.bottom < top && bottom < this.top) {
|
||||
if (this.left < left) {
|
||||
this.left = left;
|
||||
}
|
||||
if (this.top > top) {
|
||||
this.top = top;
|
||||
}
|
||||
if (this.right > right) {
|
||||
this.right = right;
|
||||
}
|
||||
if (this.bottom < bottom) {
|
||||
this.bottom = bottom;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the specified viewport intersects this viewport, return true and set this viewport to that intersection,
|
||||
* otherwise return false and do not change this viewport. No check is performed to see if either viewport is empty.
|
||||
* To just test for intersection, use intersects()
|
||||
*
|
||||
* @param v The viewport being intersected with this viewport.
|
||||
* @return true if the specified viewport and this viewport intersect (and this viewport is then set to that
|
||||
* intersection) else return false and do not change this viewport.
|
||||
*/
|
||||
public boolean intersect(Viewport v) {
|
||||
return intersect(v.left, v.top, v.right, v.bottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Viewport [left=" + left + ", top=" + top + ", right=" + right + ", bottom=" + bottom + "]";
|
||||
}
|
||||
|
||||
// ** PARCERABLE **
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + Float.floatToIntBits(bottom);
|
||||
result = prime * result + Float.floatToIntBits(left);
|
||||
result = prime * result + Float.floatToIntBits(right);
|
||||
result = prime * result + Float.floatToIntBits(top);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable interface methods
|
||||
*/
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this viewport to the specified parcel. To restore a viewport from a parcel, use readFromParcel()
|
||||
*
|
||||
* @param out The parcel to write the viewport's coordinates into
|
||||
*/
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeFloat(left);
|
||||
out.writeFloat(top);
|
||||
out.writeFloat(right);
|
||||
out.writeFloat(bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the viewport's coordinates from the data stored in the specified parcel. To write a viewport to a parcel,
|
||||
* call writeToParcel().
|
||||
*
|
||||
* @param in The parcel to read the viewport's coordinates from
|
||||
*/
|
||||
public void readFromParcel(Parcel in) {
|
||||
left = in.readFloat();
|
||||
top = in.readFloat();
|
||||
right = in.readFloat();
|
||||
bottom = in.readFloat();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package lecho.lib.hellocharts.provider;
|
||||
|
||||
import lecho.lib.hellocharts.model.BubbleChartData;
|
||||
|
||||
public interface BubbleChartDataProvider {
|
||||
|
||||
public BubbleChartData getBubbleChartData();
|
||||
|
||||
public void setBubbleChartData(BubbleChartData data);
|
||||
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package lecho.lib.hellocharts.provider;
|
||||
|
||||
import lecho.lib.hellocharts.model.ColumnChartData;
|
||||
|
||||
public interface ColumnChartDataProvider {
|
||||
|
||||
public ColumnChartData getColumnChartData();
|
||||
|
||||
public void setColumnChartData(ColumnChartData data);
|
||||
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package lecho.lib.hellocharts.provider;
|
||||
|
||||
import lecho.lib.hellocharts.model.ComboLineColumnChartData;
|
||||
|
||||
public interface ComboLineColumnChartDataProvider {
|
||||
|
||||
public ComboLineColumnChartData getComboLineColumnChartData();
|
||||
|
||||
public void setComboLineColumnChartData(ComboLineColumnChartData data);
|
||||
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package lecho.lib.hellocharts.provider;
|
||||
|
||||
import lecho.lib.hellocharts.model.LineChartData;
|
||||
|
||||
public interface LineChartDataProvider {
|
||||
|
||||
public LineChartData getLineChartData();
|
||||
|
||||
public void setLineChartData(LineChartData data);
|
||||
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package lecho.lib.hellocharts.provider;
|
||||
|
||||
import lecho.lib.hellocharts.model.PieChartData;
|
||||
|
||||
public interface PieChartDataProvider {
|
||||
|
||||
public PieChartData getPieChartData();
|
||||
|
||||
public void setPieChartData(PieChartData data);
|
||||
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Paint.FontMetricsInt;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import lecho.lib.hellocharts.computator.ChartComputator;
|
||||
import lecho.lib.hellocharts.model.ChartData;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Abstract renderer implementation, every chart renderer extends this class(although it is not required it helps).
|
||||
*/
|
||||
public abstract class AbstractChartRenderer implements ChartRenderer {
|
||||
public int DEFAULT_LABEL_MARGIN_DP = 4;
|
||||
protected Chart chart;
|
||||
protected ChartComputator computator;
|
||||
/**
|
||||
* Paint for value labels.
|
||||
*/
|
||||
protected Paint labelPaint = new Paint();
|
||||
/**
|
||||
* Paint for labels background.
|
||||
*/
|
||||
protected Paint labelBackgroundPaint = new Paint();
|
||||
/**
|
||||
* Holds coordinates for label background rect.
|
||||
*/
|
||||
protected RectF labelBackgroundRect = new RectF();
|
||||
/**
|
||||
* Font metrics for label paint, used to determine text height.
|
||||
*/
|
||||
protected FontMetricsInt fontMetrics = new FontMetricsInt();
|
||||
/**
|
||||
* If true maximum and current viewport will be calculated when chart data change or during data animations.
|
||||
*/
|
||||
protected boolean isViewportCalculationEnabled = true;
|
||||
protected float density;
|
||||
protected float scaledDensity;
|
||||
protected SelectedValue selectedValue = new SelectedValue();
|
||||
protected char[] labelBuffer = new char[64];
|
||||
protected int labelOffset;
|
||||
protected int labelMargin;
|
||||
protected boolean isValueLabelBackgroundEnabled;
|
||||
protected boolean isValueLabelBackgroundAuto;
|
||||
|
||||
public AbstractChartRenderer(Context context, Chart chart) {
|
||||
this.density = context.getResources().getDisplayMetrics().density;
|
||||
this.scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
|
||||
this.chart = chart;
|
||||
this.computator = chart.getChartComputator();
|
||||
|
||||
labelMargin = ChartUtils.dp2px(density, DEFAULT_LABEL_MARGIN_DP);
|
||||
labelOffset = labelMargin;
|
||||
|
||||
labelPaint.setAntiAlias(true);
|
||||
labelPaint.setStyle(Paint.Style.FILL);
|
||||
labelPaint.setTextAlign(Align.LEFT);
|
||||
labelPaint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
|
||||
labelPaint.setColor(Color.WHITE);
|
||||
|
||||
labelBackgroundPaint.setAntiAlias(true);
|
||||
labelBackgroundPaint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetRenderer() {
|
||||
this.computator = chart.getChartComputator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartDataChanged() {
|
||||
final ChartData data = chart.getChartData();
|
||||
|
||||
Typeface typeface = chart.getChartData().getValueLabelTypeface();
|
||||
if (null != typeface) {
|
||||
labelPaint.setTypeface(typeface);
|
||||
}
|
||||
|
||||
labelPaint.setColor(data.getValueLabelTextColor());
|
||||
labelPaint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getValueLabelTextSize()));
|
||||
labelPaint.getFontMetricsInt(fontMetrics);
|
||||
|
||||
this.isValueLabelBackgroundEnabled = data.isValueLabelBackgroundEnabled();
|
||||
this.isValueLabelBackgroundAuto = data.isValueLabelBackgroundAuto();
|
||||
this.labelBackgroundPaint.setColor(data.getValueLabelBackgroundColor());
|
||||
|
||||
// Important - clear selection when data changed.
|
||||
selectedValue.clear();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws label text and label background if isValueLabelBackgroundEnabled is true.
|
||||
*/
|
||||
protected void drawLabelTextAndBackground(Canvas canvas, char[] labelBuffer, int startIndex, int numChars,
|
||||
int autoBackgroundColor) {
|
||||
final float textX;
|
||||
final float textY;
|
||||
|
||||
if (isValueLabelBackgroundEnabled) {
|
||||
|
||||
if (isValueLabelBackgroundAuto) {
|
||||
labelBackgroundPaint.setColor(autoBackgroundColor);
|
||||
}
|
||||
|
||||
canvas.drawRect(labelBackgroundRect, labelBackgroundPaint);
|
||||
|
||||
textX = labelBackgroundRect.left + labelMargin;
|
||||
textY = labelBackgroundRect.bottom - labelMargin;
|
||||
} else {
|
||||
textX = labelBackgroundRect.left;
|
||||
textY = labelBackgroundRect.bottom;
|
||||
}
|
||||
|
||||
canvas.drawText(labelBuffer, startIndex, numChars, textX, textY, labelPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTouched() {
|
||||
return selectedValue.isSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTouch() {
|
||||
selectedValue.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Viewport getMaximumViewport() {
|
||||
return computator.getMaximumViewport();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaximumViewport(Viewport maxViewport) {
|
||||
if (null != maxViewport) {
|
||||
computator.setMaxViewport(maxViewport);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Viewport getCurrentViewport() {
|
||||
return computator.getCurrentViewport();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentViewport(Viewport viewport) {
|
||||
if (null != viewport) {
|
||||
computator.setCurrentViewport(viewport);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewportCalculationEnabled() {
|
||||
return isViewportCalculationEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setViewportCalculationEnabled(boolean isEnabled) {
|
||||
this.isViewportCalculationEnabled = isEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectValue(SelectedValue selectedValue) {
|
||||
this.selectedValue.set(selectedValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SelectedValue getSelectedValue() {
|
||||
return selectedValue;
|
||||
}
|
||||
}
|
||||
+639
@@ -0,0 +1,639 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Paint.FontMetricsInt;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import lecho.lib.hellocharts.computator.ChartComputator;
|
||||
import lecho.lib.hellocharts.model.Axis;
|
||||
import lecho.lib.hellocharts.model.AxisValue;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.util.AxisAutoValues;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.util.FloatUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Default axes renderer. Can draw maximum four axes - two horizontal(top/bottom) and two vertical(left/right).
|
||||
*/
|
||||
public class AxesRenderer {
|
||||
private static final int DEFAULT_AXIS_MARGIN_DP = 2;
|
||||
|
||||
/**
|
||||
* Axis positions indexes, used for indexing tabs that holds axes parameters, see below.
|
||||
*/
|
||||
private static final int TOP = 0;
|
||||
private static final int LEFT = 1;
|
||||
private static final int RIGHT = 2;
|
||||
private static final int BOTTOM = 3;
|
||||
|
||||
/**
|
||||
* Used to measure label width. If label has mas 5 characters only 5 first characters of this array are used to
|
||||
* measure text width.
|
||||
*/
|
||||
private static final char[] labelWidthChars = new char[]{
|
||||
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
|
||||
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
|
||||
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
|
||||
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'};
|
||||
|
||||
private Chart chart;
|
||||
private ChartComputator computator;
|
||||
private int axisMargin;
|
||||
private float density;
|
||||
private float scaledDensity;
|
||||
private Paint[] labelPaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
|
||||
private Paint[] namePaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
|
||||
private Paint[] linePaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
|
||||
private float[] nameBaselineTab = new float[4];
|
||||
private float[] labelBaselineTab = new float[4];
|
||||
private float[] separationLineTab = new float[4];
|
||||
private int[] labelWidthTab = new int[4];
|
||||
private int[] labelTextAscentTab = new int[4];
|
||||
private int[] labelTextDescentTab = new int[4];
|
||||
private int[] labelDimensionForMarginsTab = new int[4];
|
||||
private int[] labelDimensionForStepsTab = new int[4];
|
||||
private int[] tiltedLabelXTranslation = new int[4];
|
||||
private int[] tiltedLabelYTranslation = new int[4];
|
||||
private FontMetricsInt[] fontMetricsTab = new FontMetricsInt[]{new FontMetricsInt(), new FontMetricsInt(),
|
||||
new FontMetricsInt(), new FontMetricsInt()};
|
||||
/**
|
||||
* Holds formatted axis value label.
|
||||
*/
|
||||
private char[] labelBuffer = new char[64];
|
||||
|
||||
/**
|
||||
* Holds number of values that should be drown for each axis.
|
||||
*/
|
||||
private int[] valuesToDrawNumTab = new int[4];
|
||||
|
||||
/**
|
||||
* Holds raw values to draw for each axis.
|
||||
*/
|
||||
private float[][] rawValuesTab = new float[4][0];
|
||||
|
||||
/**
|
||||
* Holds auto-generated values that should be drawn, i.e if axis is inside not all auto-generated values should be
|
||||
* drawn to avoid overdrawing. Used only for auto axes.
|
||||
*/
|
||||
private float[][] autoValuesToDrawTab = new float[4][0];
|
||||
|
||||
/**
|
||||
* Holds custom values that should be drawn, used only for custom axes.
|
||||
*/
|
||||
private AxisValue[][] valuesToDrawTab = new AxisValue[4][0];
|
||||
|
||||
/**
|
||||
* Buffers for axes lines coordinates(to draw grid in the background).
|
||||
*/
|
||||
private float[][] linesDrawBufferTab = new float[4][0];
|
||||
|
||||
/**
|
||||
* Buffers for auto-generated values for each axis, used only if there are auto axes.
|
||||
*/
|
||||
private AxisAutoValues[] autoValuesBufferTab = new AxisAutoValues[]{new AxisAutoValues(),
|
||||
new AxisAutoValues(), new AxisAutoValues(), new AxisAutoValues()};
|
||||
|
||||
public AxesRenderer(Context context, Chart chart) {
|
||||
this.chart = chart;
|
||||
computator = chart.getChartComputator();
|
||||
density = context.getResources().getDisplayMetrics().density;
|
||||
scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
|
||||
axisMargin = ChartUtils.dp2px(density, DEFAULT_AXIS_MARGIN_DP);
|
||||
for (int position = 0; position < 4; ++position) {
|
||||
labelPaintTab[position].setStyle(Paint.Style.FILL);
|
||||
labelPaintTab[position].setAntiAlias(true);
|
||||
namePaintTab[position].setStyle(Paint.Style.FILL);
|
||||
namePaintTab[position].setAntiAlias(true);
|
||||
linePaintTab[position].setStyle(Paint.Style.STROKE);
|
||||
linePaintTab[position].setAntiAlias(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void onChartSizeChanged() {
|
||||
onChartDataOrSizeChanged();
|
||||
}
|
||||
|
||||
public void onChartDataChanged() {
|
||||
onChartDataOrSizeChanged();
|
||||
}
|
||||
|
||||
private void onChartDataOrSizeChanged() {
|
||||
initAxis(chart.getChartData().getAxisXTop(), TOP);
|
||||
initAxis(chart.getChartData().getAxisXBottom(), BOTTOM);
|
||||
initAxis(chart.getChartData().getAxisYLeft(), LEFT);
|
||||
initAxis(chart.getChartData().getAxisYRight(), RIGHT);
|
||||
}
|
||||
|
||||
public void resetRenderer() {
|
||||
this.computator = chart.getChartComputator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize attributes and measurement for axes(left, right, top, bottom);
|
||||
*/
|
||||
private void initAxis(Axis axis, int position) {
|
||||
if (null == axis) {
|
||||
return;
|
||||
}
|
||||
initAxisAttributes(axis, position);
|
||||
initAxisMargin(axis, position);
|
||||
initAxisMeasurements(axis, position);
|
||||
}
|
||||
|
||||
private void initAxisAttributes(Axis axis, int position) {
|
||||
initAxisPaints(axis, position);
|
||||
initAxisTextAlignment(axis, position);
|
||||
if (axis.hasTiltedLabels()) {
|
||||
initAxisDimensionForTiltedLabels(position);
|
||||
intiTiltedLabelsTranslation(axis, position);
|
||||
} else {
|
||||
initAxisDimension(position);
|
||||
}
|
||||
}
|
||||
|
||||
private void initAxisPaints(Axis axis, int position) {
|
||||
Typeface typeface = axis.getTypeface();
|
||||
if (null != typeface) {
|
||||
labelPaintTab[position].setTypeface(typeface);
|
||||
namePaintTab[position].setTypeface(typeface);
|
||||
}
|
||||
labelPaintTab[position].setColor(axis.getTextColor());
|
||||
labelPaintTab[position].setTextSize(ChartUtils.sp2px(scaledDensity, axis.getTextSize()));
|
||||
labelPaintTab[position].getFontMetricsInt(fontMetricsTab[position]);
|
||||
namePaintTab[position].setColor(axis.getTextColor());
|
||||
namePaintTab[position].setTextSize(ChartUtils.sp2px(scaledDensity, axis.getTextSize()));
|
||||
linePaintTab[position].setColor(axis.getLineColor());
|
||||
|
||||
labelTextAscentTab[position] = Math.abs(fontMetricsTab[position].ascent);
|
||||
labelTextDescentTab[position] = Math.abs(fontMetricsTab[position].descent);
|
||||
labelWidthTab[position] = (int) labelPaintTab[position].measureText(labelWidthChars, 0,
|
||||
axis.getMaxLabelChars());
|
||||
}
|
||||
|
||||
private void initAxisTextAlignment(Axis axis, int position) {
|
||||
namePaintTab[position].setTextAlign(Align.CENTER);
|
||||
if (TOP == position || BOTTOM == position) {
|
||||
labelPaintTab[position].setTextAlign(Align.CENTER);
|
||||
} else if (LEFT == position) {
|
||||
if (axis.isInside()) {
|
||||
labelPaintTab[position].setTextAlign(Align.LEFT);
|
||||
} else {
|
||||
labelPaintTab[position].setTextAlign(Align.RIGHT);
|
||||
}
|
||||
} else if (RIGHT == position) {
|
||||
if (axis.isInside()) {
|
||||
labelPaintTab[position].setTextAlign(Align.RIGHT);
|
||||
} else {
|
||||
labelPaintTab[position].setTextAlign(Align.LEFT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initAxisDimensionForTiltedLabels(int position) {
|
||||
int pythagoreanFromLabelWidth = (int) Math.sqrt(Math.pow(labelWidthTab[position], 2) / 2);
|
||||
int pythagoreanFromAscent = (int) Math.sqrt(Math.pow(labelTextAscentTab[position], 2) / 2);
|
||||
labelDimensionForMarginsTab[position] = pythagoreanFromAscent + pythagoreanFromLabelWidth;
|
||||
labelDimensionForStepsTab[position] = Math.round(labelDimensionForMarginsTab[position] * 0.75f);
|
||||
}
|
||||
|
||||
private void initAxisDimension(int position) {
|
||||
if (LEFT == position || RIGHT == position) {
|
||||
labelDimensionForMarginsTab[position] = labelWidthTab[position];
|
||||
labelDimensionForStepsTab[position] = labelTextAscentTab[position];
|
||||
} else if (TOP == position || BOTTOM == position) {
|
||||
labelDimensionForMarginsTab[position] = labelTextAscentTab[position] +
|
||||
labelTextDescentTab[position];
|
||||
labelDimensionForStepsTab[position] = labelWidthTab[position];
|
||||
}
|
||||
}
|
||||
|
||||
private void intiTiltedLabelsTranslation(Axis axis, int position) {
|
||||
int pythagoreanFromLabelWidth = (int) Math.sqrt(Math.pow(labelWidthTab[position], 2) / 2);
|
||||
int pythagoreanFromAscent = (int) Math.sqrt(Math.pow(labelTextAscentTab[position], 2) / 2);
|
||||
int dx = 0;
|
||||
int dy = 0;
|
||||
if (axis.isInside()) {
|
||||
if (LEFT == position) {
|
||||
dx = pythagoreanFromAscent;
|
||||
} else if (RIGHT == position) {
|
||||
dy = -pythagoreanFromLabelWidth / 2;
|
||||
} else if (TOP == position) {
|
||||
dy = (pythagoreanFromAscent + pythagoreanFromLabelWidth / 2) - labelTextAscentTab[position];
|
||||
} else if (BOTTOM == position) {
|
||||
dy = -pythagoreanFromLabelWidth / 2;
|
||||
}
|
||||
} else {
|
||||
if (LEFT == position) {
|
||||
dy = -pythagoreanFromLabelWidth / 2;
|
||||
} else if (RIGHT == position) {
|
||||
dx = pythagoreanFromAscent;
|
||||
} else if (TOP == position) {
|
||||
dy = -pythagoreanFromLabelWidth / 2;
|
||||
} else if (BOTTOM == position) {
|
||||
dy = (pythagoreanFromAscent + pythagoreanFromLabelWidth / 2) - labelTextAscentTab[position];
|
||||
}
|
||||
}
|
||||
tiltedLabelXTranslation[position] = dx;
|
||||
tiltedLabelYTranslation[position] = dy;
|
||||
}
|
||||
|
||||
private void initAxisMargin(Axis axis, int position) {
|
||||
int margin = 0;
|
||||
if (!axis.isInside() && (axis.isAutoGenerated() || !axis.getValues().isEmpty())) {
|
||||
margin += axisMargin + labelDimensionForMarginsTab[position];
|
||||
}
|
||||
margin += getAxisNameMargin(axis, position);
|
||||
insetContentRectWithAxesMargins(margin, position);
|
||||
}
|
||||
|
||||
private int getAxisNameMargin(Axis axis, int position) {
|
||||
int margin = 0;
|
||||
if (!TextUtils.isEmpty(axis.getName())) {
|
||||
margin += labelTextAscentTab[position];
|
||||
margin += labelTextDescentTab[position];
|
||||
margin += axisMargin;
|
||||
}
|
||||
return margin;
|
||||
}
|
||||
|
||||
private void insetContentRectWithAxesMargins(int axisMargin, int position) {
|
||||
if (LEFT == position) {
|
||||
chart.getChartComputator().insetContentRect(axisMargin, 0, 0, 0);
|
||||
} else if (RIGHT == position) {
|
||||
chart.getChartComputator().insetContentRect(0, 0, axisMargin, 0);
|
||||
} else if (TOP == position) {
|
||||
chart.getChartComputator().insetContentRect(0, axisMargin, 0, 0);
|
||||
} else if (BOTTOM == position) {
|
||||
chart.getChartComputator().insetContentRect(0, 0, 0, axisMargin);
|
||||
}
|
||||
}
|
||||
|
||||
private void initAxisMeasurements(Axis axis, int position) {
|
||||
if (LEFT == position) {
|
||||
if (axis.isInside()) {
|
||||
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().left + axisMargin;
|
||||
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().left - axisMargin
|
||||
- labelTextDescentTab[position];
|
||||
} else {
|
||||
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().left - axisMargin;
|
||||
nameBaselineTab[position] = labelBaselineTab[position] - axisMargin
|
||||
- labelTextDescentTab[position] - labelDimensionForMarginsTab[position];
|
||||
}
|
||||
separationLineTab[position] = computator.getContentRectMinusAllMargins().left;
|
||||
} else if (RIGHT == position) {
|
||||
if (axis.isInside()) {
|
||||
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().right - axisMargin;
|
||||
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().right + axisMargin
|
||||
+ labelTextAscentTab[position];
|
||||
} else {
|
||||
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().right + axisMargin;
|
||||
nameBaselineTab[position] = labelBaselineTab[position] + axisMargin
|
||||
+ labelTextAscentTab[position] + labelDimensionForMarginsTab[position];
|
||||
}
|
||||
separationLineTab[position] = computator.getContentRectMinusAllMargins().right;
|
||||
} else if (BOTTOM == position) {
|
||||
if (axis.isInside()) {
|
||||
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().bottom - axisMargin
|
||||
- labelTextDescentTab[position];
|
||||
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().bottom + axisMargin
|
||||
+ labelTextAscentTab[position];
|
||||
} else {
|
||||
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().bottom + axisMargin
|
||||
+ labelTextAscentTab[position];
|
||||
nameBaselineTab[position] = labelBaselineTab[position] + axisMargin +
|
||||
labelDimensionForMarginsTab[position];
|
||||
}
|
||||
separationLineTab[position] = computator.getContentRectMinusAllMargins().bottom;
|
||||
} else if (TOP == position) {
|
||||
if (axis.isInside()) {
|
||||
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().top + axisMargin
|
||||
+ labelTextAscentTab[position];
|
||||
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().top - axisMargin
|
||||
- labelTextDescentTab[position];
|
||||
} else {
|
||||
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().top - axisMargin
|
||||
- labelTextDescentTab[position];
|
||||
nameBaselineTab[position] = labelBaselineTab[position] - axisMargin -
|
||||
labelDimensionForMarginsTab[position];
|
||||
}
|
||||
separationLineTab[position] = computator.getContentRectMinusAllMargins().top;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid axis position: " + position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare axes coordinates and draw axes lines(if enabled) in the background.
|
||||
*
|
||||
* @param canvas
|
||||
*/
|
||||
public void drawInBackground(Canvas canvas) {
|
||||
Axis axis = chart.getChartData().getAxisYLeft();
|
||||
if (null != axis) {
|
||||
prepareAxisToDraw(axis, LEFT);
|
||||
drawAxisLines(canvas, axis, LEFT);
|
||||
}
|
||||
|
||||
axis = chart.getChartData().getAxisYRight();
|
||||
if (null != axis) {
|
||||
prepareAxisToDraw(axis, RIGHT);
|
||||
drawAxisLines(canvas, axis, RIGHT);
|
||||
}
|
||||
|
||||
axis = chart.getChartData().getAxisXBottom();
|
||||
if (null != axis) {
|
||||
prepareAxisToDraw(axis, BOTTOM);
|
||||
drawAxisLines(canvas, axis, BOTTOM);
|
||||
}
|
||||
|
||||
axis = chart.getChartData().getAxisXTop();
|
||||
if (null != axis) {
|
||||
prepareAxisToDraw(axis, TOP);
|
||||
drawAxisLines(canvas, axis, TOP);
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareAxisToDraw(Axis axis, int position) {
|
||||
if (axis.isAutoGenerated()) {
|
||||
prepareAutoGeneratedAxis(axis, position);
|
||||
} else {
|
||||
prepareCustomAxis(axis, position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw axes labels and names in the foreground.
|
||||
*
|
||||
* @param canvas
|
||||
*/
|
||||
public void drawInForeground(Canvas canvas) {
|
||||
Axis axis = chart.getChartData().getAxisYLeft();
|
||||
if (null != axis) {
|
||||
drawAxisLabelsAndName(canvas, axis, LEFT);
|
||||
}
|
||||
|
||||
axis = chart.getChartData().getAxisYRight();
|
||||
if (null != axis) {
|
||||
drawAxisLabelsAndName(canvas, axis, RIGHT);
|
||||
}
|
||||
|
||||
axis = chart.getChartData().getAxisXBottom();
|
||||
if (null != axis) {
|
||||
drawAxisLabelsAndName(canvas, axis, BOTTOM);
|
||||
}
|
||||
|
||||
axis = chart.getChartData().getAxisXTop();
|
||||
if (null != axis) {
|
||||
drawAxisLabelsAndName(canvas, axis, TOP);
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareCustomAxis(Axis axis, int position) {
|
||||
final Viewport maxViewport = computator.getMaximumViewport();
|
||||
final Viewport visibleViewport = computator.getVisibleViewport();
|
||||
final Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
boolean isAxisVertical = isAxisVertical(position);
|
||||
float viewportMin, viewportMax;
|
||||
float scale = 1;
|
||||
if (isAxisVertical) {
|
||||
if (maxViewport.height() > 0 && visibleViewport.height() > 0) {
|
||||
scale = contentRect.height() * (maxViewport.height() / visibleViewport.height());
|
||||
}
|
||||
viewportMin = visibleViewport.bottom;
|
||||
viewportMax = visibleViewport.top;
|
||||
} else {
|
||||
if (maxViewport.width() > 0 && visibleViewport.width() > 0) {
|
||||
scale = contentRect.width() * (maxViewport.width() / visibleViewport.width());
|
||||
}
|
||||
viewportMin = visibleViewport.left;
|
||||
viewportMax = visibleViewport.right;
|
||||
}
|
||||
if (scale == 0) {
|
||||
scale = 1;
|
||||
}
|
||||
int module = (int) Math.max(1,
|
||||
Math.ceil((axis.getValues().size() * labelDimensionForStepsTab[position] * 1.5) / scale));
|
||||
//Reinitialize tab to hold lines coordinates.
|
||||
if (axis.hasLines() && (linesDrawBufferTab[position].length < axis.getValues().size() * 4)) {
|
||||
linesDrawBufferTab[position] = new float[axis.getValues().size() * 4];
|
||||
}
|
||||
//Reinitialize tabs to hold all raw values to draw.
|
||||
if (rawValuesTab[position].length < axis.getValues().size()) {
|
||||
rawValuesTab[position] = new float[axis.getValues().size()];
|
||||
}
|
||||
//Reinitialize tabs to hold all raw values to draw.
|
||||
if (valuesToDrawTab[position].length < axis.getValues().size()) {
|
||||
valuesToDrawTab[position] = new AxisValue[axis.getValues().size()];
|
||||
}
|
||||
|
||||
float rawValue;
|
||||
int valueIndex = 0;
|
||||
int valueToDrawIndex = 0;
|
||||
for (AxisValue axisValue : axis.getValues()) {
|
||||
// Draw axis values that are within visible viewport.
|
||||
final float value = axisValue.getValue();
|
||||
if (value >= viewportMin && value <= viewportMax) {
|
||||
// Draw axis values that have 0 module value, this will hide some labels if there is no place for them.
|
||||
if (0 == valueIndex % module) {
|
||||
if (isAxisVertical) {
|
||||
rawValue = computator.computeRawY(value);
|
||||
} else {
|
||||
rawValue = computator.computeRawX(value);
|
||||
}
|
||||
if (checkRawValue(contentRect, rawValue, axis.isInside(), position, isAxisVertical)) {
|
||||
rawValuesTab[position][valueToDrawIndex] = rawValue;
|
||||
valuesToDrawTab[position][valueToDrawIndex] = axisValue;
|
||||
++valueToDrawIndex;
|
||||
}
|
||||
}
|
||||
// If within viewport - increment valueIndex;
|
||||
++valueIndex;
|
||||
}
|
||||
}
|
||||
valuesToDrawNumTab[position] = valueToDrawIndex;
|
||||
}
|
||||
|
||||
private void prepareAutoGeneratedAxis(Axis axis, int position) {
|
||||
final Viewport visibleViewport = computator.getVisibleViewport();
|
||||
final Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
boolean isAxisVertical = isAxisVertical(position);
|
||||
float start, stop;
|
||||
int contentRectDimension;
|
||||
if (isAxisVertical) {
|
||||
start = visibleViewport.bottom;
|
||||
stop = visibleViewport.top;
|
||||
contentRectDimension = contentRect.height();
|
||||
} else {
|
||||
start = visibleViewport.left;
|
||||
stop = visibleViewport.right;
|
||||
contentRectDimension = contentRect.width();
|
||||
}
|
||||
FloatUtils.computeAutoGeneratedAxisValues(start, stop, Math.abs(contentRectDimension) /
|
||||
labelDimensionForStepsTab[position] / 2, autoValuesBufferTab[position]);
|
||||
//Reinitialize tab to hold lines coordinates.
|
||||
if (axis.hasLines()
|
||||
&& (linesDrawBufferTab[position].length < autoValuesBufferTab[position].valuesNumber * 4)) {
|
||||
linesDrawBufferTab[position] = new float[autoValuesBufferTab[position].valuesNumber * 4];
|
||||
}
|
||||
//Reinitialize tabs to hold all raw and auto values.
|
||||
if (rawValuesTab[position].length < autoValuesBufferTab[position].valuesNumber) {
|
||||
rawValuesTab[position] = new float[autoValuesBufferTab[position].valuesNumber];
|
||||
}
|
||||
if (autoValuesToDrawTab[position].length < autoValuesBufferTab[position].valuesNumber) {
|
||||
autoValuesToDrawTab[position] = new float[autoValuesBufferTab[position].valuesNumber];
|
||||
}
|
||||
|
||||
float rawValue;
|
||||
int valueToDrawIndex = 0;
|
||||
for (int i = 0; i < autoValuesBufferTab[position].valuesNumber; ++i) {
|
||||
if (isAxisVertical) {
|
||||
rawValue = computator.computeRawY(autoValuesBufferTab[position].values[i]);
|
||||
} else {
|
||||
rawValue = computator.computeRawX(autoValuesBufferTab[position].values[i]);
|
||||
}
|
||||
if (checkRawValue(contentRect, rawValue, axis.isInside(), position, isAxisVertical)) {
|
||||
rawValuesTab[position][valueToDrawIndex] = rawValue;
|
||||
autoValuesToDrawTab[position][valueToDrawIndex] = autoValuesBufferTab[position].values[i];
|
||||
++valueToDrawIndex;
|
||||
}
|
||||
}
|
||||
valuesToDrawNumTab[position] = valueToDrawIndex;
|
||||
}
|
||||
|
||||
private boolean checkRawValue(Rect rect, float rawValue, boolean axisInside, int position, boolean isVertical) {
|
||||
if (axisInside) {
|
||||
if (isVertical) {
|
||||
float marginBottom = labelTextAscentTab[BOTTOM] + axisMargin;
|
||||
float marginTop = labelTextAscentTab[TOP] + axisMargin;
|
||||
if (rawValue <= rect.bottom - marginBottom && rawValue >= rect.top + marginTop) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
float margin = labelWidthTab[position] / 2;
|
||||
if (rawValue >= rect.left + margin && rawValue <= rect.right - margin) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void drawAxisLines(Canvas canvas, Axis axis, int position) {
|
||||
final Rect contentRectMargins = computator.getContentRectMinusAxesMargins();
|
||||
float separationX1, separationY1, separationX2, separationY2;
|
||||
separationX1 = separationY1 = separationX2 = separationY2 = 0;
|
||||
float lineX1, lineY1, lineX2, lineY2;
|
||||
lineX1 = lineY1 = lineX2 = lineY2 = 0;
|
||||
boolean isAxisVertical = isAxisVertical(position);
|
||||
if (LEFT == position || RIGHT == position) {
|
||||
separationX1 = separationX2 = separationLineTab[position];
|
||||
separationY1 = contentRectMargins.bottom;
|
||||
separationY2 = contentRectMargins.top;
|
||||
lineX1 = contentRectMargins.left;
|
||||
lineX2 = contentRectMargins.right;
|
||||
} else if (TOP == position || BOTTOM == position) {
|
||||
separationX1 = contentRectMargins.left;
|
||||
separationX2 = contentRectMargins.right;
|
||||
separationY1 = separationY2 = separationLineTab[position];
|
||||
lineY1 = contentRectMargins.top;
|
||||
lineY2 = contentRectMargins.bottom;
|
||||
}
|
||||
// Draw separation line with the same color as axis labels and name.
|
||||
if (axis.hasSeparationLine()) {
|
||||
canvas.drawLine(separationX1, separationY1, separationX2, separationY2, labelPaintTab[position]);
|
||||
}
|
||||
|
||||
if (axis.hasLines()) {
|
||||
int valueToDrawIndex = 0;
|
||||
for (; valueToDrawIndex < valuesToDrawNumTab[position]; ++valueToDrawIndex) {
|
||||
if (isAxisVertical) {
|
||||
lineY1 = lineY2 = rawValuesTab[position][valueToDrawIndex];
|
||||
} else {
|
||||
lineX1 = lineX2 = rawValuesTab[position][valueToDrawIndex];
|
||||
}
|
||||
linesDrawBufferTab[position][valueToDrawIndex * 4 + 0] = lineX1;
|
||||
linesDrawBufferTab[position][valueToDrawIndex * 4 + 1] = lineY1;
|
||||
linesDrawBufferTab[position][valueToDrawIndex * 4 + 2] = lineX2;
|
||||
linesDrawBufferTab[position][valueToDrawIndex * 4 + 3] = lineY2;
|
||||
}
|
||||
canvas.drawLines(linesDrawBufferTab[position], 0, valueToDrawIndex * 4, linePaintTab[position]);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawAxisLabelsAndName(Canvas canvas, Axis axis, int position) {
|
||||
float labelX, labelY;
|
||||
labelX = labelY = 0;
|
||||
boolean isAxisVertical = isAxisVertical(position);
|
||||
if (LEFT == position || RIGHT == position) {
|
||||
labelX = labelBaselineTab[position];
|
||||
} else if (TOP == position || BOTTOM == position) {
|
||||
labelY = labelBaselineTab[position];
|
||||
}
|
||||
|
||||
for (int valueToDrawIndex = 0; valueToDrawIndex < valuesToDrawNumTab[position]; ++valueToDrawIndex) {
|
||||
int charsNumber = 0;
|
||||
if (axis.isAutoGenerated()) {
|
||||
final float value = autoValuesToDrawTab[position][valueToDrawIndex];
|
||||
charsNumber = axis.getFormatter().formatValueForAutoGeneratedAxis(labelBuffer, value,
|
||||
autoValuesBufferTab[position].decimals);
|
||||
} else {
|
||||
AxisValue axisValue = valuesToDrawTab[position][valueToDrawIndex];
|
||||
charsNumber = axis.getFormatter().formatValueForManualAxis(labelBuffer, axisValue);
|
||||
}
|
||||
|
||||
if (isAxisVertical) {
|
||||
labelY = rawValuesTab[position][valueToDrawIndex];
|
||||
} else {
|
||||
labelX = rawValuesTab[position][valueToDrawIndex];
|
||||
}
|
||||
|
||||
if (axis.hasTiltedLabels()) {
|
||||
canvas.save();
|
||||
canvas.translate(tiltedLabelXTranslation[position], tiltedLabelYTranslation[position]);
|
||||
canvas.rotate(-45, labelX, labelY);
|
||||
canvas.drawText(labelBuffer, labelBuffer.length - charsNumber, charsNumber, labelX, labelY,
|
||||
labelPaintTab[position]);
|
||||
canvas.restore();
|
||||
} else {
|
||||
canvas.drawText(labelBuffer, labelBuffer.length - charsNumber, charsNumber, labelX, labelY,
|
||||
labelPaintTab[position]);
|
||||
}
|
||||
}
|
||||
|
||||
// Drawing axis name
|
||||
final Rect contentRectMargins = computator.getContentRectMinusAxesMargins();
|
||||
if (!TextUtils.isEmpty(axis.getName())) {
|
||||
if (isAxisVertical) {
|
||||
canvas.save();
|
||||
canvas.rotate(-90, contentRectMargins.centerY(), contentRectMargins.centerY());
|
||||
canvas.drawText(axis.getName(), contentRectMargins.centerY(), nameBaselineTab[position],
|
||||
namePaintTab[position]);
|
||||
canvas.restore();
|
||||
} else {
|
||||
canvas.drawText(axis.getName(), contentRectMargins.centerX(), nameBaselineTab[position],
|
||||
namePaintTab[position]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAxisVertical(int position) {
|
||||
if (LEFT == position || RIGHT == position) {
|
||||
return true;
|
||||
} else if (TOP == position || BOTTOM == position) {
|
||||
return false;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid axis position " + position);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import lecho.lib.hellocharts.computator.ChartComputator;
|
||||
import lecho.lib.hellocharts.formatter.BubbleChartValueFormatter;
|
||||
import lecho.lib.hellocharts.model.BubbleChartData;
|
||||
import lecho.lib.hellocharts.model.BubbleValue;
|
||||
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
|
||||
import lecho.lib.hellocharts.model.ValueShape;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.provider.BubbleChartDataProvider;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
public class BubbleChartRenderer extends AbstractChartRenderer {
|
||||
private static final int DEFAULT_TOUCH_ADDITIONAL_DP = 4;
|
||||
private static final int MODE_DRAW = 0;
|
||||
private static final int MODE_HIGHLIGHT = 1;
|
||||
|
||||
private BubbleChartDataProvider dataProvider;
|
||||
|
||||
/**
|
||||
* Additional value added to bubble radius when drawing highlighted bubble, used to give tauch feedback.
|
||||
*/
|
||||
private int touchAdditional;
|
||||
|
||||
/**
|
||||
* Scales for bubble radius value, only one is used depending on screen orientation;
|
||||
*/
|
||||
private float bubbleScaleX;
|
||||
private float bubbleScaleY;
|
||||
|
||||
/**
|
||||
* True if bubbleScale = bubbleScaleX so the renderer should used {@link ChartComputator#computeRawDistanceX(float)}
|
||||
* , if false bubbleScale = bubbleScaleY and renderer should use
|
||||
* {@link ChartComputator#computeRawDistanceY(float)}.
|
||||
*/
|
||||
private boolean isBubbleScaledByX = true;
|
||||
|
||||
/**
|
||||
* Maximum bubble radius.
|
||||
*/
|
||||
private float maxRadius;
|
||||
|
||||
/**
|
||||
* Minimal bubble radius in pixels.
|
||||
*/
|
||||
private float minRawRadius;
|
||||
private PointF bubbleCenter = new PointF();
|
||||
private Paint bubblePaint = new Paint();
|
||||
|
||||
/**
|
||||
* Rect used for drawing bubbles with SHAPE_SQUARE.
|
||||
*/
|
||||
private RectF bubbleRect = new RectF();
|
||||
|
||||
private boolean hasLabels;
|
||||
private boolean hasLabelsOnlyForSelected;
|
||||
private BubbleChartValueFormatter valueFormatter;
|
||||
private Viewport tempMaximumViewport = new Viewport();
|
||||
|
||||
public BubbleChartRenderer(Context context, Chart chart, BubbleChartDataProvider dataProvider) {
|
||||
super(context, chart);
|
||||
this.dataProvider = dataProvider;
|
||||
|
||||
touchAdditional = ChartUtils.dp2px(density, DEFAULT_TOUCH_ADDITIONAL_DP);
|
||||
|
||||
bubblePaint.setAntiAlias(true);
|
||||
bubblePaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartSizeChanged() {
|
||||
final ChartComputator computator = chart.getChartComputator();
|
||||
Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
if (contentRect.width() < contentRect.height()) {
|
||||
isBubbleScaledByX = true;
|
||||
} else {
|
||||
isBubbleScaledByX = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartDataChanged() {
|
||||
super.onChartDataChanged();
|
||||
BubbleChartData data = dataProvider.getBubbleChartData();
|
||||
this.hasLabels = data.hasLabels();
|
||||
this.hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected();
|
||||
this.valueFormatter = data.getFormatter();
|
||||
|
||||
onChartViewportChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartViewportChanged() {
|
||||
if (isViewportCalculationEnabled) {
|
||||
calculateMaxViewport();
|
||||
computator.setMaxViewport(tempMaximumViewport);
|
||||
computator.setCurrentViewport(computator.getMaximumViewport());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
drawBubbles(canvas);
|
||||
if (isTouched()) {
|
||||
highlightBubbles(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawUnclipped(Canvas canvas) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkTouch(float touchX, float touchY) {
|
||||
selectedValue.clear();
|
||||
final BubbleChartData data = dataProvider.getBubbleChartData();
|
||||
int valueIndex = 0;
|
||||
for (BubbleValue bubbleValue : data.getValues()) {
|
||||
float rawRadius = processBubble(bubbleValue, bubbleCenter);
|
||||
|
||||
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
|
||||
if (bubbleRect.contains(touchX, touchY)) {
|
||||
selectedValue.set(valueIndex, valueIndex, SelectedValueType.NONE);
|
||||
}
|
||||
} else if (ValueShape.CIRCLE.equals(bubbleValue.getShape())) {
|
||||
final float diffX = touchX - bubbleCenter.x;
|
||||
final float diffY = touchY - bubbleCenter.y;
|
||||
final float touchDistance = (float) Math.sqrt((diffX * diffX) + (diffY * diffY));
|
||||
|
||||
if (touchDistance <= rawRadius) {
|
||||
selectedValue.set(valueIndex, valueIndex, SelectedValueType.NONE);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid bubble shape: " + bubbleValue.getShape());
|
||||
}
|
||||
|
||||
++valueIndex;
|
||||
}
|
||||
|
||||
return isTouched();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes empty spaces on sides of chart(left-right for landscape, top-bottom for portrait). *This method should be
|
||||
* called after layout had been drawn*. Because most often chart is drawn as rectangle with proportions other than
|
||||
* 1:1 and bubbles have to be drawn as circles not ellipses I am unable to calculate correct margins based on chart
|
||||
* data only. I need to know chart dimension to remove extra empty spaces, that bad because viewport depends a
|
||||
* little on contentRectMinusAllMargins.
|
||||
*/
|
||||
public void removeMargins() {
|
||||
final Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
if (contentRect.height() == 0 || contentRect.width() == 0) {
|
||||
// View probably not yet measured, skip removing margins.
|
||||
return;
|
||||
}
|
||||
final float pxX = computator.computeRawDistanceX(maxRadius * bubbleScaleX);
|
||||
final float pxY = computator.computeRawDistanceY(maxRadius * bubbleScaleY);
|
||||
final float scaleX = computator.getMaximumViewport().width() / contentRect.width();
|
||||
final float scaleY = computator.getMaximumViewport().height() / contentRect.height();
|
||||
float dx = 0;
|
||||
float dy = 0;
|
||||
if (isBubbleScaledByX) {
|
||||
dy = (pxY - pxX) * scaleY * 0.75f;
|
||||
} else {
|
||||
dx = (pxX - pxY) * scaleX * 0.75f;
|
||||
}
|
||||
|
||||
Viewport maxViewport = computator.getMaximumViewport();
|
||||
maxViewport.inset(dx, dy);
|
||||
Viewport currentViewport = computator.getCurrentViewport();
|
||||
currentViewport.inset(dx, dy);
|
||||
computator.setMaxViewport(maxViewport);
|
||||
computator.setCurrentViewport(currentViewport);
|
||||
}
|
||||
|
||||
private void drawBubbles(Canvas canvas) {
|
||||
final BubbleChartData data = dataProvider.getBubbleChartData();
|
||||
for (BubbleValue bubbleValue : data.getValues()) {
|
||||
drawBubble(canvas, bubbleValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBubble(Canvas canvas, BubbleValue bubbleValue) {
|
||||
float rawRadius = processBubble(bubbleValue, bubbleCenter);
|
||||
// Not touched bubbles are a little smaller than touched to give user touch feedback.
|
||||
rawRadius -= touchAdditional;
|
||||
bubbleRect.inset(touchAdditional, touchAdditional);
|
||||
bubblePaint.setColor(bubbleValue.getColor());
|
||||
drawBubbleShapeAndLabel(canvas, bubbleValue, rawRadius, MODE_DRAW);
|
||||
|
||||
}
|
||||
|
||||
private void drawBubbleShapeAndLabel(Canvas canvas, BubbleValue bubbleValue, float rawRadius, int mode) {
|
||||
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
|
||||
canvas.drawRect(bubbleRect, bubblePaint);
|
||||
} else if (ValueShape.CIRCLE.equals(bubbleValue.getShape())) {
|
||||
canvas.drawCircle(bubbleCenter.x, bubbleCenter.y, rawRadius, bubblePaint);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid bubble shape: " + bubbleValue.getShape());
|
||||
}
|
||||
|
||||
if (MODE_HIGHLIGHT == mode) {
|
||||
if (hasLabels || hasLabelsOnlyForSelected) {
|
||||
drawLabel(canvas, bubbleValue, bubbleCenter.x, bubbleCenter.y);
|
||||
}
|
||||
} else if (MODE_DRAW == mode) {
|
||||
if (hasLabels) {
|
||||
drawLabel(canvas, bubbleValue, bubbleCenter.x, bubbleCenter.y);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot process bubble in mode: " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
private void highlightBubbles(Canvas canvas) {
|
||||
final BubbleChartData data = dataProvider.getBubbleChartData();
|
||||
BubbleValue bubbleValue = data.getValues().get(selectedValue.getFirstIndex());
|
||||
highlightBubble(canvas, bubbleValue);
|
||||
}
|
||||
|
||||
private void highlightBubble(Canvas canvas, BubbleValue bubbleValue) {
|
||||
float rawRadius = processBubble(bubbleValue, bubbleCenter);
|
||||
bubblePaint.setColor(bubbleValue.getDarkenColor());
|
||||
drawBubbleShapeAndLabel(canvas, bubbleValue, rawRadius, MODE_HIGHLIGHT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bubble radius and center x and y coordinates. Center x and x will be stored in point parameter, radius
|
||||
* will be returned as float value.
|
||||
*/
|
||||
private float processBubble(BubbleValue bubbleValue, PointF point) {
|
||||
final float rawX = computator.computeRawX(bubbleValue.getX());
|
||||
final float rawY = computator.computeRawY(bubbleValue.getY());
|
||||
float radius = (float) Math.sqrt(Math.abs(bubbleValue.getZ()) / Math.PI);
|
||||
float rawRadius;
|
||||
if (isBubbleScaledByX) {
|
||||
radius *= bubbleScaleX;
|
||||
rawRadius = computator.computeRawDistanceX(radius);
|
||||
} else {
|
||||
radius *= bubbleScaleY;
|
||||
rawRadius = computator.computeRawDistanceY(radius);
|
||||
}
|
||||
|
||||
if (rawRadius < minRawRadius + touchAdditional) {
|
||||
rawRadius = minRawRadius + touchAdditional;
|
||||
}
|
||||
|
||||
bubbleCenter.set(rawX, rawY);
|
||||
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
|
||||
bubbleRect.set(rawX - rawRadius, rawY - rawRadius, rawX + rawRadius, rawY + rawRadius);
|
||||
}
|
||||
return rawRadius;
|
||||
}
|
||||
|
||||
private void drawLabel(Canvas canvas, BubbleValue bubbleValue, float rawX, float rawY) {
|
||||
final Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
final int numChars = valueFormatter.formatChartValue(labelBuffer, bubbleValue);
|
||||
|
||||
if (numChars == 0) {
|
||||
// No need to draw empty label
|
||||
return;
|
||||
}
|
||||
|
||||
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
|
||||
final int labelHeight = Math.abs(fontMetrics.ascent);
|
||||
float left = rawX - labelWidth / 2 - labelMargin;
|
||||
float right = rawX + labelWidth / 2 + labelMargin;
|
||||
float top = rawY - labelHeight / 2 - labelMargin;
|
||||
float bottom = rawY + labelHeight / 2 + labelMargin;
|
||||
|
||||
if (top < contentRect.top) {
|
||||
top = rawY;
|
||||
bottom = rawY + labelHeight + labelMargin * 2;
|
||||
}
|
||||
if (bottom > contentRect.bottom) {
|
||||
top = rawY - labelHeight - labelMargin * 2;
|
||||
bottom = rawY;
|
||||
}
|
||||
if (left < contentRect.left) {
|
||||
left = rawX;
|
||||
right = rawX + labelWidth + labelMargin * 2;
|
||||
}
|
||||
if (right > contentRect.right) {
|
||||
left = rawX - labelWidth - labelMargin * 2;
|
||||
right = rawX;
|
||||
}
|
||||
|
||||
labelBackgroundRect.set(left, top, right, bottom);
|
||||
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
|
||||
bubbleValue.getDarkenColor());
|
||||
|
||||
}
|
||||
|
||||
private void calculateMaxViewport() {
|
||||
float maxZ = Float.MIN_VALUE;
|
||||
tempMaximumViewport.set(Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MAX_VALUE);
|
||||
BubbleChartData data = dataProvider.getBubbleChartData();
|
||||
// TODO: Optimize.
|
||||
for (BubbleValue bubbleValue : data.getValues()) {
|
||||
if (Math.abs(bubbleValue.getZ()) > maxZ) {
|
||||
maxZ = Math.abs(bubbleValue.getZ());
|
||||
}
|
||||
if (bubbleValue.getX() < tempMaximumViewport.left) {
|
||||
tempMaximumViewport.left = bubbleValue.getX();
|
||||
}
|
||||
if (bubbleValue.getX() > tempMaximumViewport.right) {
|
||||
tempMaximumViewport.right = bubbleValue.getX();
|
||||
}
|
||||
if (bubbleValue.getY() < tempMaximumViewport.bottom) {
|
||||
tempMaximumViewport.bottom = bubbleValue.getY();
|
||||
}
|
||||
if (bubbleValue.getY() > tempMaximumViewport.top) {
|
||||
tempMaximumViewport.top = bubbleValue.getY();
|
||||
}
|
||||
}
|
||||
|
||||
maxRadius = (float) Math.sqrt(maxZ / Math.PI);
|
||||
|
||||
// Number 4 is determined by trials and errors method, no magic behind it:).
|
||||
bubbleScaleX = tempMaximumViewport.width() / (maxRadius * 4);
|
||||
if (bubbleScaleX == 0) {
|
||||
// case for 0 viewport width.
|
||||
bubbleScaleX = 1;
|
||||
}
|
||||
|
||||
bubbleScaleY = tempMaximumViewport.height() / (maxRadius * 4);
|
||||
if (bubbleScaleY == 0) {
|
||||
// case for 0 viewport height.
|
||||
bubbleScaleY = 1;
|
||||
}
|
||||
|
||||
// For cases when user sets different than 1 bubble scale in BubbleChartData.
|
||||
bubbleScaleX *= data.getBubbleScale();
|
||||
bubbleScaleY *= data.getBubbleScale();
|
||||
|
||||
// Prevent cutting of bubbles on the edges of chart area.
|
||||
tempMaximumViewport.inset(-maxRadius * bubbleScaleX, -maxRadius * bubbleScaleY);
|
||||
|
||||
minRawRadius = ChartUtils.dp2px(density, dataProvider.getBubbleChartData().getMinBubbleRadius());
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
|
||||
/**
|
||||
* Interface for all chart renderer.
|
||||
*/
|
||||
public interface ChartRenderer {
|
||||
|
||||
public void onChartSizeChanged();
|
||||
|
||||
public void onChartDataChanged();
|
||||
|
||||
public void onChartViewportChanged();
|
||||
|
||||
public void resetRenderer();
|
||||
|
||||
/**
|
||||
* Draw chart data.
|
||||
*/
|
||||
public void draw(Canvas canvas);
|
||||
|
||||
/**
|
||||
* Draw chart data that should not be clipped to contentRect area.
|
||||
*/
|
||||
public void drawUnclipped(Canvas canvas);
|
||||
|
||||
/**
|
||||
* Checks if given pixel coordinates corresponds to any chart value. If yes return true and set selectedValue, if
|
||||
* not selectedValue should be *cleared* and method should return false.
|
||||
*/
|
||||
public boolean checkTouch(float touchX, float touchY);
|
||||
|
||||
/**
|
||||
* Returns true if there is value selected.
|
||||
*/
|
||||
public boolean isTouched();
|
||||
|
||||
/**
|
||||
* Clear value selection.
|
||||
*/
|
||||
public void clearTouch();
|
||||
|
||||
public Viewport getMaximumViewport();
|
||||
|
||||
public void setMaximumViewport(Viewport maxViewport);
|
||||
|
||||
public Viewport getCurrentViewport();
|
||||
|
||||
public void setCurrentViewport(Viewport viewport);
|
||||
|
||||
public boolean isViewportCalculationEnabled();
|
||||
|
||||
public void setViewportCalculationEnabled(boolean isEnabled);
|
||||
|
||||
public void selectValue(SelectedValue selectedValue);
|
||||
|
||||
public SelectedValue getSelectedValue();
|
||||
|
||||
}
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Cap;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import lecho.lib.hellocharts.model.Column;
|
||||
import lecho.lib.hellocharts.model.ColumnChartData;
|
||||
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Magic renderer for ColumnChart.
|
||||
*/
|
||||
public class ColumnChartRenderer extends AbstractChartRenderer {
|
||||
public static final int DEFAULT_SUBCOLUMN_SPACING_DP = 1;
|
||||
public static final int DEFAULT_COLUMN_TOUCH_ADDITIONAL_WIDTH_DP = 4;
|
||||
|
||||
private static final int MODE_DRAW = 0;
|
||||
private static final int MODE_CHECK_TOUCH = 1;
|
||||
private static final int MODE_HIGHLIGHT = 2;
|
||||
|
||||
private ColumnChartDataProvider dataProvider;
|
||||
|
||||
/**
|
||||
* Additional width for hightlighted column, used to give tauch feedback.
|
||||
*/
|
||||
private int touchAdditionalWidth;
|
||||
|
||||
/**
|
||||
* Spacing between sub-columns.
|
||||
*/
|
||||
private int subcolumnSpacing;
|
||||
|
||||
/**
|
||||
* Paint used to draw every column.
|
||||
*/
|
||||
private Paint columnPaint = new Paint();
|
||||
|
||||
/**
|
||||
* Holds coordinates for currently processed column/sub-column.
|
||||
*/
|
||||
private RectF drawRect = new RectF();
|
||||
|
||||
/**
|
||||
* Coordinated of user tauch.
|
||||
*/
|
||||
private PointF touchedPoint = new PointF();
|
||||
|
||||
private float fillRatio;
|
||||
|
||||
private float baseValue;
|
||||
|
||||
private Viewport tempMaximumViewport = new Viewport();
|
||||
|
||||
public ColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider dataProvider) {
|
||||
super(context, chart);
|
||||
this.dataProvider = dataProvider;
|
||||
subcolumnSpacing = ChartUtils.dp2px(density, DEFAULT_SUBCOLUMN_SPACING_DP);
|
||||
touchAdditionalWidth = ChartUtils.dp2px(density, DEFAULT_COLUMN_TOUCH_ADDITIONAL_WIDTH_DP);
|
||||
|
||||
columnPaint.setAntiAlias(true);
|
||||
columnPaint.setStyle(Paint.Style.FILL);
|
||||
columnPaint.setStrokeCap(Cap.SQUARE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartSizeChanged() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartDataChanged() {
|
||||
super.onChartDataChanged();
|
||||
ColumnChartData data = dataProvider.getColumnChartData();
|
||||
fillRatio = data.getFillRatio();
|
||||
baseValue = data.getBaseValue();
|
||||
|
||||
onChartViewportChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartViewportChanged() {
|
||||
if (isViewportCalculationEnabled) {
|
||||
calculateMaxViewport();
|
||||
computator.setMaxViewport(tempMaximumViewport);
|
||||
computator.setCurrentViewport(computator.getMaximumViewport());
|
||||
}
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
if (data.isStacked()) {
|
||||
drawColumnForStacked(canvas);
|
||||
if (isTouched()) {
|
||||
highlightColumnForStacked(canvas);
|
||||
}
|
||||
} else {
|
||||
drawColumnsForSubcolumns(canvas);
|
||||
if (isTouched()) {
|
||||
highlightColumnsForSubcolumns(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawUnclipped(Canvas canvas) {
|
||||
// Do nothing, for this kind of chart there is nothing to draw beyond clipped area
|
||||
}
|
||||
|
||||
public boolean checkTouch(float touchX, float touchY) {
|
||||
selectedValue.clear();
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
if (data.isStacked()) {
|
||||
checkTouchForStacked(touchX, touchY);
|
||||
} else {
|
||||
checkTouchForSubcolumns(touchX, touchY);
|
||||
}
|
||||
return isTouched();
|
||||
}
|
||||
|
||||
private void calculateMaxViewport() {
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
// Column chart always has X values from 0 to numColumns-1, to add some margin on the left and right I added
|
||||
// extra 0.5 to the each side, that margins will be negative scaled according to number of columns, so for more
|
||||
// columns there will be less margin.
|
||||
tempMaximumViewport.set(-0.5f, baseValue, data.getColumns().size() - 0.5f, baseValue);
|
||||
if (data.isStacked()) {
|
||||
calculateMaxViewportForStacked(data);
|
||||
} else {
|
||||
calculateMaxViewportForSubcolumns(data);
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateMaxViewportForSubcolumns(ColumnChartData data) {
|
||||
for (Column column : data.getColumns()) {
|
||||
for (SubcolumnValue columnValue : column.getValues()) {
|
||||
if (columnValue.getValue() >= baseValue && columnValue.getValue() > tempMaximumViewport.top) {
|
||||
tempMaximumViewport.top = columnValue.getValue();
|
||||
}
|
||||
if (columnValue.getValue() < baseValue && columnValue.getValue() < tempMaximumViewport.bottom) {
|
||||
tempMaximumViewport.bottom = columnValue.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateMaxViewportForStacked(ColumnChartData data) {
|
||||
for (Column column : data.getColumns()) {
|
||||
float sumPositive = baseValue;
|
||||
float sumNegative = baseValue;
|
||||
for (SubcolumnValue columnValue : column.getValues()) {
|
||||
if (columnValue.getValue() >= baseValue) {
|
||||
sumPositive += columnValue.getValue();
|
||||
} else {
|
||||
sumNegative += columnValue.getValue();
|
||||
}
|
||||
}
|
||||
if (sumPositive > tempMaximumViewport.top) {
|
||||
tempMaximumViewport.top = sumPositive;
|
||||
}
|
||||
if (sumNegative < tempMaximumViewport.bottom) {
|
||||
tempMaximumViewport.bottom = sumNegative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawColumnsForSubcolumns(Canvas canvas) {
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
final float columnWidth = calculateColumnWidth();
|
||||
int columnIndex = 0;
|
||||
for (Column column : data.getColumns()) {
|
||||
processColumnForSubcolumns(canvas, column, columnWidth, columnIndex, MODE_DRAW);
|
||||
++columnIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void highlightColumnsForSubcolumns(Canvas canvas) {
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
final float columnWidth = calculateColumnWidth();
|
||||
Column column = data.getColumns().get(selectedValue.getFirstIndex());
|
||||
processColumnForSubcolumns(canvas, column, columnWidth, selectedValue.getFirstIndex(), MODE_HIGHLIGHT);
|
||||
}
|
||||
|
||||
private void checkTouchForSubcolumns(float touchX, float touchY) {
|
||||
// Using member variable to hold touch point to avoid too much parameters in methods.
|
||||
touchedPoint.x = touchX;
|
||||
touchedPoint.y = touchY;
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
final float columnWidth = calculateColumnWidth();
|
||||
int columnIndex = 0;
|
||||
for (Column column : data.getColumns()) {
|
||||
// canvas is not needed for checking touch
|
||||
processColumnForSubcolumns(null, column, columnWidth, columnIndex, MODE_CHECK_TOUCH);
|
||||
++columnIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void processColumnForSubcolumns(Canvas canvas, Column column, float columnWidth, int columnIndex,
|
||||
int mode) {
|
||||
// For n subcolumns there will be n-1 spacing and there will be one
|
||||
// subcolumn for every columnValue
|
||||
float subcolumnWidth = (columnWidth - (subcolumnSpacing * (column.getValues().size() - 1)))
|
||||
/ column.getValues().size();
|
||||
if (subcolumnWidth < 1) {
|
||||
subcolumnWidth = 1;
|
||||
}
|
||||
// Columns are indexes from 0 to n, column index is also column X value
|
||||
final float rawX = computator.computeRawX(columnIndex);
|
||||
final float halfColumnWidth = columnWidth / 2;
|
||||
final float baseRawY = computator.computeRawY(baseValue);
|
||||
// First subcolumn will starts at the left edge of current column,
|
||||
// rawValueX is horizontal center of that column
|
||||
float subcolumnRawX = rawX - halfColumnWidth;
|
||||
int valueIndex = 0;
|
||||
for (SubcolumnValue columnValue : column.getValues()) {
|
||||
columnPaint.setColor(columnValue.getColor());
|
||||
if (subcolumnRawX > rawX + halfColumnWidth) {
|
||||
break;
|
||||
}
|
||||
final float rawY = computator.computeRawY(columnValue.getValue());
|
||||
calculateRectToDraw(columnValue, subcolumnRawX, subcolumnRawX + subcolumnWidth, baseRawY, rawY);
|
||||
switch (mode) {
|
||||
case MODE_DRAW:
|
||||
drawSubcolumn(canvas, column, columnValue, false);
|
||||
break;
|
||||
case MODE_HIGHLIGHT:
|
||||
highlightSubcolumn(canvas, column, columnValue, valueIndex, false);
|
||||
break;
|
||||
case MODE_CHECK_TOUCH:
|
||||
checkRectToDraw(columnIndex, valueIndex);
|
||||
break;
|
||||
default:
|
||||
// There no else, every case should be handled or exception will
|
||||
// be thrown
|
||||
throw new IllegalStateException("Cannot process column in mode: " + mode);
|
||||
}
|
||||
subcolumnRawX += subcolumnWidth + subcolumnSpacing;
|
||||
++valueIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawColumnForStacked(Canvas canvas) {
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
final float columnWidth = calculateColumnWidth();
|
||||
// Columns are indexes from 0 to n, column index is also column X value
|
||||
int columnIndex = 0;
|
||||
for (Column column : data.getColumns()) {
|
||||
processColumnForStacked(canvas, column, columnWidth, columnIndex, MODE_DRAW);
|
||||
++columnIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void highlightColumnForStacked(Canvas canvas) {
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
final float columnWidth = calculateColumnWidth();
|
||||
// Columns are indexes from 0 to n, column index is also column X value
|
||||
Column column = data.getColumns().get(selectedValue.getFirstIndex());
|
||||
processColumnForStacked(canvas, column, columnWidth, selectedValue.getFirstIndex(), MODE_HIGHLIGHT);
|
||||
}
|
||||
|
||||
private void checkTouchForStacked(float touchX, float touchY) {
|
||||
touchedPoint.x = touchX;
|
||||
touchedPoint.y = touchY;
|
||||
final ColumnChartData data = dataProvider.getColumnChartData();
|
||||
final float columnWidth = calculateColumnWidth();
|
||||
int columnIndex = 0;
|
||||
for (Column column : data.getColumns()) {
|
||||
// canvas is not needed for checking touch
|
||||
processColumnForStacked(null, column, columnWidth, columnIndex, MODE_CHECK_TOUCH);
|
||||
++columnIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void processColumnForStacked(Canvas canvas, Column column, float columnWidth, int columnIndex, int mode) {
|
||||
final float rawX = computator.computeRawX(columnIndex);
|
||||
final float halfColumnWidth = columnWidth / 2;
|
||||
float mostPositiveValue = baseValue;
|
||||
float mostNegativeValue = baseValue;
|
||||
float subcolumnBaseValue = baseValue;
|
||||
int valueIndex = 0;
|
||||
for (SubcolumnValue columnValue : column.getValues()) {
|
||||
columnPaint.setColor(columnValue.getColor());
|
||||
if (columnValue.getValue() >= baseValue) {
|
||||
// Using values instead of raw pixels make code easier to
|
||||
// understand(for me)
|
||||
subcolumnBaseValue = mostPositiveValue;
|
||||
mostPositiveValue += columnValue.getValue();
|
||||
} else {
|
||||
subcolumnBaseValue = mostNegativeValue;
|
||||
mostNegativeValue += columnValue.getValue();
|
||||
}
|
||||
final float rawBaseY = computator.computeRawY(subcolumnBaseValue);
|
||||
final float rawY = computator.computeRawY(subcolumnBaseValue + columnValue.getValue());
|
||||
calculateRectToDraw(columnValue, rawX - halfColumnWidth, rawX + halfColumnWidth, rawBaseY, rawY);
|
||||
switch (mode) {
|
||||
case MODE_DRAW:
|
||||
drawSubcolumn(canvas, column, columnValue, true);
|
||||
break;
|
||||
case MODE_HIGHLIGHT:
|
||||
highlightSubcolumn(canvas, column, columnValue, valueIndex, true);
|
||||
break;
|
||||
case MODE_CHECK_TOUCH:
|
||||
checkRectToDraw(columnIndex, valueIndex);
|
||||
break;
|
||||
default:
|
||||
// There no else, every case should be handled or exception will
|
||||
// be thrown
|
||||
throw new IllegalStateException("Cannot process column in mode: " + mode);
|
||||
}
|
||||
++valueIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSubcolumn(Canvas canvas, Column column, SubcolumnValue columnValue, boolean isStacked) {
|
||||
canvas.drawRect(drawRect, columnPaint);
|
||||
if (column.hasLabels()) {
|
||||
drawLabel(canvas, column, columnValue, isStacked, labelOffset);
|
||||
}
|
||||
}
|
||||
|
||||
private void highlightSubcolumn(Canvas canvas, Column column, SubcolumnValue columnValue, int valueIndex,
|
||||
boolean isStacked) {
|
||||
if (selectedValue.getSecondIndex() == valueIndex) {
|
||||
columnPaint.setColor(columnValue.getDarkenColor());
|
||||
canvas.drawRect(drawRect.left - touchAdditionalWidth, drawRect.top, drawRect.right + touchAdditionalWidth,
|
||||
drawRect.bottom, columnPaint);
|
||||
if (column.hasLabels() || column.hasLabelsOnlyForSelected()) {
|
||||
drawLabel(canvas, column, columnValue, isStacked, labelOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRectToDraw(int columnIndex, int valueIndex) {
|
||||
if (drawRect.contains(touchedPoint.x, touchedPoint.y)) {
|
||||
selectedValue.set(columnIndex, valueIndex, SelectedValueType.COLUMN);
|
||||
}
|
||||
}
|
||||
|
||||
private float calculateColumnWidth() {
|
||||
// columnWidht should be at least 2 px
|
||||
float columnWidth = fillRatio * computator.getContentRectMinusAllMargins().width() / computator
|
||||
.getVisibleViewport().width();
|
||||
if (columnWidth < 2) {
|
||||
columnWidth = 2;
|
||||
}
|
||||
return columnWidth;
|
||||
}
|
||||
|
||||
private void calculateRectToDraw(SubcolumnValue columnValue, float left, float right, float rawBaseY, float rawY) {
|
||||
// Calculate rect that will be drawn as column, subcolumn or label background.
|
||||
drawRect.left = left;
|
||||
drawRect.right = right;
|
||||
if (columnValue.getValue() >= baseValue) {
|
||||
drawRect.top = rawY;
|
||||
drawRect.bottom = rawBaseY - subcolumnSpacing;
|
||||
} else {
|
||||
drawRect.bottom = rawY;
|
||||
drawRect.top = rawBaseY + subcolumnSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLabel(Canvas canvas, Column column, SubcolumnValue columnValue, boolean isStacked, float offset) {
|
||||
final int numChars = column.getFormatter().formatChartValue(labelBuffer, columnValue);
|
||||
|
||||
if (numChars == 0) {
|
||||
// No need to draw empty label
|
||||
return;
|
||||
}
|
||||
|
||||
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
|
||||
final int labelHeight = Math.abs(fontMetrics.ascent);
|
||||
float left = drawRect.centerX() - labelWidth / 2 - labelMargin;
|
||||
float right = drawRect.centerX() + labelWidth / 2 + labelMargin;
|
||||
float top;
|
||||
float bottom;
|
||||
if (isStacked && labelHeight < drawRect.height() - (2 * labelMargin)) {
|
||||
// For stacked columns draw label only if label height is less than subcolumn height - (2 * labelMargin).
|
||||
if (columnValue.getValue() >= baseValue) {
|
||||
top = drawRect.top;
|
||||
bottom = drawRect.top + labelHeight + labelMargin * 2;
|
||||
} else {
|
||||
top = drawRect.bottom - labelHeight - labelMargin * 2;
|
||||
bottom = drawRect.bottom;
|
||||
}
|
||||
} else if (!isStacked) {
|
||||
// For not stacked draw label at the top for positive and at the bottom for negative values
|
||||
if (columnValue.getValue() >= baseValue) {
|
||||
top = drawRect.top - offset - labelHeight - labelMargin * 2;
|
||||
if (top < computator.getContentRectMinusAllMargins().top) {
|
||||
top = drawRect.top + offset;
|
||||
bottom = drawRect.top + offset + labelHeight + labelMargin * 2;
|
||||
} else {
|
||||
bottom = drawRect.top - offset;
|
||||
}
|
||||
} else {
|
||||
bottom = drawRect.bottom + offset + labelHeight + labelMargin * 2;
|
||||
if (bottom > computator.getContentRectMinusAllMargins().bottom) {
|
||||
top = drawRect.bottom - offset - labelHeight - labelMargin * 2;
|
||||
bottom = drawRect.bottom - offset;
|
||||
} else {
|
||||
top = drawRect.bottom + offset;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Draw nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
labelBackgroundRect.set(left, top, right, bottom);
|
||||
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
|
||||
columnValue.getDarkenColor());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
public class ComboChartRenderer extends AbstractChartRenderer {
|
||||
|
||||
protected List<ChartRenderer> renderers;
|
||||
protected Viewport unionViewport = new Viewport();
|
||||
|
||||
public ComboChartRenderer(Context context, Chart chart) {
|
||||
super(context, chart);
|
||||
this.renderers = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartSizeChanged() {
|
||||
for (ChartRenderer renderer : renderers) {
|
||||
renderer.onChartSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartDataChanged() {
|
||||
super.onChartDataChanged();
|
||||
for (ChartRenderer renderer : renderers) {
|
||||
renderer.onChartDataChanged();
|
||||
}
|
||||
onChartViewportChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartViewportChanged() {
|
||||
if (isViewportCalculationEnabled) {
|
||||
int rendererIndex = 0;
|
||||
for (ChartRenderer renderer : renderers) {
|
||||
renderer.onChartViewportChanged();
|
||||
if (rendererIndex == 0) {
|
||||
unionViewport.set(renderer.getMaximumViewport());
|
||||
} else {
|
||||
unionViewport.union(renderer.getMaximumViewport());
|
||||
}
|
||||
++rendererIndex;
|
||||
}
|
||||
computator.setMaxViewport(unionViewport);
|
||||
computator.setCurrentViewport(unionViewport);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
for (ChartRenderer renderer : renderers) {
|
||||
renderer.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawUnclipped(Canvas canvas) {
|
||||
for (ChartRenderer renderer : renderers) {
|
||||
renderer.drawUnclipped(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkTouch(float touchX, float touchY) {
|
||||
selectedValue.clear();
|
||||
int rendererIndex = renderers.size() - 1;
|
||||
for (; rendererIndex >= 0; rendererIndex--) {
|
||||
ChartRenderer renderer = renderers.get(rendererIndex);
|
||||
if (renderer.checkTouch(touchX, touchY)) {
|
||||
selectedValue.set(renderer.getSelectedValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//clear the rest of renderers if value was selected, if value was not selected this loop
|
||||
// will not be executed.
|
||||
for (rendererIndex--; rendererIndex >= 0; rendererIndex--) {
|
||||
ChartRenderer renderer = renderers.get(rendererIndex);
|
||||
renderer.clearTouch();
|
||||
}
|
||||
|
||||
return isTouched();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTouch() {
|
||||
for (ChartRenderer renderer : renderers) {
|
||||
renderer.clearTouch();
|
||||
}
|
||||
selectedValue.clear();
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
|
||||
import lecho.lib.hellocharts.provider.LineChartDataProvider;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
public class ComboLineColumnChartRenderer extends ComboChartRenderer {
|
||||
|
||||
private ColumnChartRenderer columnChartRenderer;
|
||||
private LineChartRenderer lineChartRenderer;
|
||||
|
||||
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider columnChartDataProvider,
|
||||
LineChartDataProvider lineChartDataProvider) {
|
||||
this(context, chart, new ColumnChartRenderer(context, chart, columnChartDataProvider),
|
||||
new LineChartRenderer(context, chart, lineChartDataProvider));
|
||||
}
|
||||
|
||||
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartRenderer columnChartRenderer,
|
||||
LineChartDataProvider lineChartDataProvider) {
|
||||
this(context, chart, columnChartRenderer, new LineChartRenderer(context, chart, lineChartDataProvider));
|
||||
}
|
||||
|
||||
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider columnChartDataProvider,
|
||||
LineChartRenderer lineChartRenderer) {
|
||||
this(context, chart, new ColumnChartRenderer(context, chart, columnChartDataProvider), lineChartRenderer);
|
||||
}
|
||||
|
||||
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartRenderer columnChartRenderer,
|
||||
LineChartRenderer lineChartRenderer) {
|
||||
super(context, chart);
|
||||
|
||||
this.columnChartRenderer = columnChartRenderer;
|
||||
this.lineChartRenderer = lineChartRenderer;
|
||||
|
||||
renderers.add(this.columnChartRenderer);
|
||||
renderers.add(this.lineChartRenderer);
|
||||
}
|
||||
}
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Cap;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PorterDuff.Mode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
|
||||
import lecho.lib.hellocharts.model.Line;
|
||||
import lecho.lib.hellocharts.model.LineChartData;
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
|
||||
import lecho.lib.hellocharts.model.ValueShape;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.provider.LineChartDataProvider;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Renderer for line chart. Can draw lines, cubic lines, filled area chart and scattered chart.
|
||||
*/
|
||||
public class LineChartRenderer extends AbstractChartRenderer {
|
||||
private static final float LINE_SMOOTHNESS = 0.16f;
|
||||
private static final int DEFAULT_LINE_STROKE_WIDTH_DP = 3;
|
||||
private static final int DEFAULT_TOUCH_TOLERANCE_MARGIN_DP = 4;
|
||||
|
||||
private static final int MODE_DRAW = 0;
|
||||
private static final int MODE_HIGHLIGHT = 1;
|
||||
|
||||
private LineChartDataProvider dataProvider;
|
||||
|
||||
private int checkPrecision;
|
||||
|
||||
private float baseValue;
|
||||
|
||||
private int touchToleranceMargin;
|
||||
private Path path = new Path();
|
||||
private Paint linePaint = new Paint();
|
||||
private Paint pointPaint = new Paint();
|
||||
|
||||
private Bitmap softwareBitmap;
|
||||
private Canvas softwareCanvas = new Canvas();
|
||||
private Viewport tempMaximumViewport = new Viewport();
|
||||
|
||||
public LineChartRenderer(Context context, Chart chart, LineChartDataProvider dataProvider) {
|
||||
super(context, chart);
|
||||
this.dataProvider = dataProvider;
|
||||
|
||||
touchToleranceMargin = ChartUtils.dp2px(density, DEFAULT_TOUCH_TOLERANCE_MARGIN_DP);
|
||||
|
||||
linePaint.setAntiAlias(true);
|
||||
linePaint.setStyle(Paint.Style.STROKE);
|
||||
linePaint.setStrokeCap(Cap.ROUND);
|
||||
linePaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_LINE_STROKE_WIDTH_DP));
|
||||
|
||||
pointPaint.setAntiAlias(true);
|
||||
pointPaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
checkPrecision = ChartUtils.dp2px(density, 2);
|
||||
|
||||
}
|
||||
|
||||
public void onChartSizeChanged() {
|
||||
final int internalMargin = calculateContentRectInternalMargin();
|
||||
computator.insetContentRectByInternalMargins(internalMargin, internalMargin,
|
||||
internalMargin, internalMargin);
|
||||
if (computator.getChartWidth() > 0 && computator.getChartHeight() > 0) {
|
||||
softwareBitmap = Bitmap.createBitmap(computator.getChartWidth(), computator.getChartHeight(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
softwareCanvas.setBitmap(softwareBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartDataChanged() {
|
||||
super.onChartDataChanged();
|
||||
final int internalMargin = calculateContentRectInternalMargin();
|
||||
computator.insetContentRectByInternalMargins(internalMargin, internalMargin,
|
||||
internalMargin, internalMargin);
|
||||
baseValue = dataProvider.getLineChartData().getBaseValue();
|
||||
|
||||
onChartViewportChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartViewportChanged() {
|
||||
if (isViewportCalculationEnabled) {
|
||||
calculateMaxViewport();
|
||||
computator.setMaxViewport(tempMaximumViewport);
|
||||
computator.setCurrentViewport(computator.getMaximumViewport());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
final LineChartData data = dataProvider.getLineChartData();
|
||||
|
||||
final Canvas drawCanvas;
|
||||
|
||||
// softwareBitmap can be null if chart is rendered in layout editor. In that case use default canvas and not
|
||||
// softwareCanvas.
|
||||
if (null != softwareBitmap) {
|
||||
drawCanvas = softwareCanvas;
|
||||
drawCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
|
||||
} else {
|
||||
drawCanvas = canvas;
|
||||
}
|
||||
|
||||
for (Line line : data.getLines()) {
|
||||
if (line.hasLines()) {
|
||||
if (line.isCubic()) {
|
||||
drawSmoothPath(drawCanvas, line);
|
||||
} else if (line.isSquare()) {
|
||||
drawSquarePath(drawCanvas, line);
|
||||
} else {
|
||||
drawPath(drawCanvas, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null != softwareBitmap) {
|
||||
canvas.drawBitmap(softwareBitmap, 0, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawUnclipped(Canvas canvas) {
|
||||
final LineChartData data = dataProvider.getLineChartData();
|
||||
int lineIndex = 0;
|
||||
for (Line line : data.getLines()) {
|
||||
if (checkIfShouldDrawPoints(line)) {
|
||||
drawPoints(canvas, line, lineIndex, MODE_DRAW);
|
||||
}
|
||||
++lineIndex;
|
||||
}
|
||||
if (isTouched()) {
|
||||
// Redraw touched point to bring it to the front
|
||||
highlightPoints(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkIfShouldDrawPoints(Line line) {
|
||||
return line.hasPoints() || line.getValues().size() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkTouch(float touchX, float touchY) {
|
||||
selectedValue.clear();
|
||||
final LineChartData data = dataProvider.getLineChartData();
|
||||
int lineIndex = 0;
|
||||
for (Line line : data.getLines()) {
|
||||
if (checkIfShouldDrawPoints(line)) {
|
||||
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
|
||||
int valueIndex = 0;
|
||||
for (PointValue pointValue : line.getValues()) {
|
||||
final float rawValueX = computator.computeRawX(pointValue.getX());
|
||||
final float rawValueY = computator.computeRawY(pointValue.getY());
|
||||
if (isInArea(rawValueX, rawValueY, touchX, touchY, pointRadius + touchToleranceMargin)) {
|
||||
selectedValue.set(lineIndex, valueIndex, SelectedValueType.LINE);
|
||||
}
|
||||
++valueIndex;
|
||||
}
|
||||
}
|
||||
++lineIndex;
|
||||
}
|
||||
return isTouched();
|
||||
}
|
||||
|
||||
private void calculateMaxViewport() {
|
||||
tempMaximumViewport.set(Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MAX_VALUE);
|
||||
LineChartData data = dataProvider.getLineChartData();
|
||||
|
||||
for (Line line : data.getLines()) {
|
||||
// Calculate max and min for viewport.
|
||||
for (PointValue pointValue : line.getValues()) {
|
||||
if (pointValue.getX() < tempMaximumViewport.left) {
|
||||
tempMaximumViewport.left = pointValue.getX();
|
||||
}
|
||||
if (pointValue.getX() > tempMaximumViewport.right) {
|
||||
tempMaximumViewport.right = pointValue.getX();
|
||||
}
|
||||
if (pointValue.getY() < tempMaximumViewport.bottom) {
|
||||
tempMaximumViewport.bottom = pointValue.getY();
|
||||
}
|
||||
if (pointValue.getY() > tempMaximumViewport.top) {
|
||||
tempMaximumViewport.top = pointValue.getY();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateContentRectInternalMargin() {
|
||||
int contentAreaMargin = 0;
|
||||
final LineChartData data = dataProvider.getLineChartData();
|
||||
for (Line line : data.getLines()) {
|
||||
if (checkIfShouldDrawPoints(line)) {
|
||||
int margin = line.getPointRadius() + DEFAULT_TOUCH_TOLERANCE_MARGIN_DP;
|
||||
if (margin > contentAreaMargin) {
|
||||
contentAreaMargin = margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ChartUtils.dp2px(density, contentAreaMargin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws lines, uses path for drawing filled area on software canvas. Line is drawn with canvas.drawLines() method.
|
||||
*/
|
||||
private void drawPath(Canvas canvas, final Line line) {
|
||||
prepareLinePaint(line);
|
||||
|
||||
int valueIndex = 0;
|
||||
for (PointValue pointValue : line.getValues()) {
|
||||
|
||||
final float rawX = computator.computeRawX(pointValue.getX());
|
||||
final float rawY = computator.computeRawY(pointValue.getY());
|
||||
|
||||
if (valueIndex == 0) {
|
||||
path.moveTo(rawX, rawY);
|
||||
} else {
|
||||
path.lineTo(rawX, rawY);
|
||||
}
|
||||
|
||||
++valueIndex;
|
||||
|
||||
}
|
||||
|
||||
canvas.drawPath(path, linePaint);
|
||||
|
||||
if (line.isFilled()) {
|
||||
drawArea(canvas, line);
|
||||
}
|
||||
|
||||
path.reset();
|
||||
}
|
||||
|
||||
private void drawSquarePath(Canvas canvas, final Line line) {
|
||||
prepareLinePaint(line);
|
||||
|
||||
int valueIndex = 0;
|
||||
float previousRawY = 0;
|
||||
for (PointValue pointValue : line.getValues()) {
|
||||
|
||||
final float rawX = computator.computeRawX(pointValue.getX());
|
||||
final float rawY = computator.computeRawY(pointValue.getY());
|
||||
|
||||
if (valueIndex == 0) {
|
||||
path.moveTo(rawX, rawY);
|
||||
} else {
|
||||
path.lineTo(rawX, previousRawY);
|
||||
path.lineTo(rawX, rawY);
|
||||
}
|
||||
|
||||
previousRawY = rawY;
|
||||
|
||||
++valueIndex;
|
||||
|
||||
}
|
||||
|
||||
canvas.drawPath(path, linePaint);
|
||||
|
||||
if (line.isFilled()) {
|
||||
drawArea(canvas, line);
|
||||
}
|
||||
|
||||
path.reset();
|
||||
}
|
||||
|
||||
private void drawSmoothPath(Canvas canvas, final Line line) {
|
||||
prepareLinePaint(line);
|
||||
|
||||
final int lineSize = line.getValues().size();
|
||||
float prePreviousPointX = Float.NaN;
|
||||
float prePreviousPointY = Float.NaN;
|
||||
float previousPointX = Float.NaN;
|
||||
float previousPointY = Float.NaN;
|
||||
float currentPointX = Float.NaN;
|
||||
float currentPointY = Float.NaN;
|
||||
float nextPointX = Float.NaN;
|
||||
float nextPointY = Float.NaN;
|
||||
|
||||
for (int valueIndex = 0; valueIndex < lineSize; ++valueIndex) {
|
||||
if (Float.isNaN(currentPointX)) {
|
||||
PointValue linePoint = line.getValues().get(valueIndex);
|
||||
currentPointX = computator.computeRawX(linePoint.getX());
|
||||
currentPointY = computator.computeRawY(linePoint.getY());
|
||||
}
|
||||
if (Float.isNaN(previousPointX)) {
|
||||
if (valueIndex > 0) {
|
||||
PointValue linePoint = line.getValues().get(valueIndex - 1);
|
||||
previousPointX = computator.computeRawX(linePoint.getX());
|
||||
previousPointY = computator.computeRawY(linePoint.getY());
|
||||
} else {
|
||||
previousPointX = currentPointX;
|
||||
previousPointY = currentPointY;
|
||||
}
|
||||
}
|
||||
|
||||
if (Float.isNaN(prePreviousPointX)) {
|
||||
if (valueIndex > 1) {
|
||||
PointValue linePoint = line.getValues().get(valueIndex - 2);
|
||||
prePreviousPointX = computator.computeRawX(linePoint.getX());
|
||||
prePreviousPointY = computator.computeRawY(linePoint.getY());
|
||||
} else {
|
||||
prePreviousPointX = previousPointX;
|
||||
prePreviousPointY = previousPointY;
|
||||
}
|
||||
}
|
||||
|
||||
// nextPoint is always new one or it is equal currentPoint.
|
||||
if (valueIndex < lineSize - 1) {
|
||||
PointValue linePoint = line.getValues().get(valueIndex + 1);
|
||||
nextPointX = computator.computeRawX(linePoint.getX());
|
||||
nextPointY = computator.computeRawY(linePoint.getY());
|
||||
} else {
|
||||
nextPointX = currentPointX;
|
||||
nextPointY = currentPointY;
|
||||
}
|
||||
|
||||
if (valueIndex == 0) {
|
||||
// Move to start point.
|
||||
path.moveTo(currentPointX, currentPointY);
|
||||
} else {
|
||||
// Calculate control points.
|
||||
final float firstDiffX = (currentPointX - prePreviousPointX);
|
||||
final float firstDiffY = (currentPointY - prePreviousPointY);
|
||||
final float secondDiffX = (nextPointX - previousPointX);
|
||||
final float secondDiffY = (nextPointY - previousPointY);
|
||||
final float firstControlPointX = previousPointX + (LINE_SMOOTHNESS * firstDiffX);
|
||||
final float firstControlPointY = previousPointY + (LINE_SMOOTHNESS * firstDiffY);
|
||||
final float secondControlPointX = currentPointX - (LINE_SMOOTHNESS * secondDiffX);
|
||||
final float secondControlPointY = currentPointY - (LINE_SMOOTHNESS * secondDiffY);
|
||||
path.cubicTo(firstControlPointX, firstControlPointY, secondControlPointX, secondControlPointY,
|
||||
currentPointX, currentPointY);
|
||||
}
|
||||
|
||||
// Shift values by one back to prevent recalculation of values that have
|
||||
// been already calculated.
|
||||
prePreviousPointX = previousPointX;
|
||||
prePreviousPointY = previousPointY;
|
||||
previousPointX = currentPointX;
|
||||
previousPointY = currentPointY;
|
||||
currentPointX = nextPointX;
|
||||
currentPointY = nextPointY;
|
||||
}
|
||||
|
||||
canvas.drawPath(path, linePaint);
|
||||
if (line.isFilled()) {
|
||||
drawArea(canvas, line);
|
||||
}
|
||||
path.reset();
|
||||
}
|
||||
|
||||
private void prepareLinePaint(final Line line) {
|
||||
linePaint.setStrokeWidth(ChartUtils.dp2px(density, line.getStrokeWidth()));
|
||||
linePaint.setColor(line.getColor());
|
||||
linePaint.setPathEffect(line.getPathEffect());
|
||||
linePaint.setShader(null);
|
||||
}
|
||||
|
||||
// TODO Drawing points can be done in the same loop as drawing lines but it
|
||||
// may cause problems in the future with
|
||||
// implementing point styles.
|
||||
private void drawPoints(Canvas canvas, Line line, int lineIndex, int mode) {
|
||||
pointPaint.setColor(line.getPointColor());
|
||||
int valueIndex = 0;
|
||||
for (PointValue pointValue : line.getValues()) {
|
||||
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
|
||||
final float rawX = computator.computeRawX(pointValue.getX());
|
||||
final float rawY = computator.computeRawY(pointValue.getY());
|
||||
if (computator.isWithinContentRect(rawX, rawY, checkPrecision)) {
|
||||
// Draw points only if they are within contentRectMinusAllMargins, using contentRectMinusAllMargins
|
||||
// instead of viewport to avoid some
|
||||
// float rounding problems.
|
||||
if (MODE_DRAW == mode) {
|
||||
drawPoint(canvas, line, pointValue, rawX, rawY, pointRadius);
|
||||
if (line.hasLabels()) {
|
||||
drawLabel(canvas, line, pointValue, rawX, rawY, pointRadius + labelOffset);
|
||||
}
|
||||
} else if (MODE_HIGHLIGHT == mode) {
|
||||
highlightPoint(canvas, line, pointValue, rawX, rawY, lineIndex, valueIndex);
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot process points in mode: " + mode);
|
||||
}
|
||||
}
|
||||
++valueIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPoint(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY,
|
||||
float pointRadius) {
|
||||
if (ValueShape.SQUARE.equals(line.getShape())) {
|
||||
canvas.drawRect(rawX - pointRadius, rawY - pointRadius, rawX + pointRadius, rawY + pointRadius,
|
||||
pointPaint);
|
||||
} else if (ValueShape.CIRCLE.equals(line.getShape())) {
|
||||
canvas.drawCircle(rawX, rawY, pointRadius, pointPaint);
|
||||
} else if (ValueShape.DIAMOND.equals(line.getShape())) {
|
||||
canvas.save();
|
||||
canvas.rotate(45, rawX, rawY);
|
||||
canvas.drawRect(rawX - pointRadius, rawY - pointRadius, rawX + pointRadius, rawY + pointRadius,
|
||||
pointPaint);
|
||||
canvas.restore();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid point shape: " + line.getShape());
|
||||
}
|
||||
}
|
||||
|
||||
private void highlightPoints(Canvas canvas) {
|
||||
int lineIndex = selectedValue.getFirstIndex();
|
||||
Line line = dataProvider.getLineChartData().getLines().get(lineIndex);
|
||||
drawPoints(canvas, line, lineIndex, MODE_HIGHLIGHT);
|
||||
}
|
||||
|
||||
private void highlightPoint(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY, int lineIndex,
|
||||
int valueIndex) {
|
||||
if (selectedValue.getFirstIndex() == lineIndex && selectedValue.getSecondIndex() == valueIndex) {
|
||||
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
|
||||
pointPaint.setColor(line.getDarkenColor());
|
||||
drawPoint(canvas, line, pointValue, rawX, rawY, pointRadius + touchToleranceMargin);
|
||||
if (line.hasLabels() || line.hasLabelsOnlyForSelected()) {
|
||||
drawLabel(canvas, line, pointValue, rawX, rawY, pointRadius + labelOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLabel(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY, float offset) {
|
||||
final Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
final int numChars = line.getFormatter().formatChartValue(labelBuffer, pointValue);
|
||||
if (numChars == 0) {
|
||||
// No need to draw empty label
|
||||
return;
|
||||
}
|
||||
|
||||
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
|
||||
final int labelHeight = Math.abs(fontMetrics.ascent);
|
||||
float left = rawX - labelWidth / 2 - labelMargin;
|
||||
float right = rawX + labelWidth / 2 + labelMargin;
|
||||
|
||||
float top;
|
||||
float bottom;
|
||||
|
||||
if (pointValue.getY() >= baseValue) {
|
||||
top = rawY - offset - labelHeight - labelMargin * 2;
|
||||
bottom = rawY - offset;
|
||||
} else {
|
||||
top = rawY + offset;
|
||||
bottom = rawY + offset + labelHeight + labelMargin * 2;
|
||||
}
|
||||
|
||||
if (top < contentRect.top) {
|
||||
top = rawY + offset;
|
||||
bottom = rawY + offset + labelHeight + labelMargin * 2;
|
||||
}
|
||||
if (bottom > contentRect.bottom) {
|
||||
top = rawY - offset - labelHeight - labelMargin * 2;
|
||||
bottom = rawY - offset;
|
||||
}
|
||||
if (left < contentRect.left) {
|
||||
left = rawX;
|
||||
right = rawX + labelWidth + labelMargin * 2;
|
||||
}
|
||||
if (right > contentRect.right) {
|
||||
left = rawX - labelWidth - labelMargin * 2;
|
||||
right = rawX;
|
||||
}
|
||||
|
||||
labelBackgroundRect.set(left, top, right, bottom);
|
||||
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
|
||||
line.getDarkenColor());
|
||||
}
|
||||
|
||||
private void drawArea(Canvas canvas, Line line) {
|
||||
final int lineSize = line.getValues().size();
|
||||
if (lineSize < 2) {
|
||||
//No point to draw area for one point or empty line.
|
||||
return;
|
||||
}
|
||||
|
||||
final Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
final float baseRawValue = Math.min(contentRect.bottom, Math.max(computator.computeRawY(baseValue),
|
||||
contentRect.top));
|
||||
//That checks works only if the last point is the right most one.
|
||||
final float left = Math.max(computator.computeRawX(line.getValues().get(0).getX()), contentRect.left);
|
||||
final float right = Math.min(computator.computeRawX(line.getValues().get(lineSize - 1).getX()),
|
||||
contentRect.right);
|
||||
|
||||
path.lineTo(right, baseRawValue);
|
||||
path.lineTo(left, baseRawValue);
|
||||
path.close();
|
||||
|
||||
linePaint.setStyle(Paint.Style.FILL);
|
||||
linePaint.setAlpha(line.getAreaTransparency());
|
||||
linePaint.setShader(line.getGradientToTransparent() ?
|
||||
new LinearGradient(0, 0, 0, canvas.getHeight(), line.getColor(),
|
||||
line.getColor() & 0x00ffffff, Shader.TileMode.MIRROR) :
|
||||
null);
|
||||
canvas.drawPath(path, linePaint);
|
||||
linePaint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
|
||||
private boolean isInArea(float x, float y, float touchX, float touchY, float radius) {
|
||||
float diffX = touchX - x;
|
||||
float diffY = touchY - y;
|
||||
return Math.pow(diffX, 2) + Math.pow(diffY, 2) <= 2 * Math.pow(radius, 2);
|
||||
}
|
||||
|
||||
}
|
||||
+513
@@ -0,0 +1,513 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
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.FontMetricsInt;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import lecho.lib.hellocharts.formatter.PieChartValueFormatter;
|
||||
import lecho.lib.hellocharts.model.PieChartData;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
|
||||
import lecho.lib.hellocharts.model.SliceValue;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.provider.PieChartDataProvider;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Default renderer for PieChart. PieChart doesn't use viewport concept so it a little different than others chart
|
||||
* types.
|
||||
*/
|
||||
public class PieChartRenderer extends AbstractChartRenderer {
|
||||
private static final float MAX_WIDTH_HEIGHT = 100f;
|
||||
private static final int DEFAULT_START_ROTATION = 45;
|
||||
private static final float DEFAULT_LABEL_INSIDE_RADIUS_FACTOR = 0.7f;
|
||||
private static final float DEFAULT_LABEL_OUTSIDE_RADIUS_FACTOR = 1.0f;
|
||||
private static final int DEFAULT_TOUCH_ADDITIONAL_DP = 8;
|
||||
private static final int MODE_DRAW = 0;
|
||||
private static final int MODE_HIGHLIGHT = 1;
|
||||
private int rotation = DEFAULT_START_ROTATION;
|
||||
private PieChartDataProvider dataProvider;
|
||||
private Paint slicePaint = new Paint();
|
||||
private float maxSum;
|
||||
private RectF originCircleOval = new RectF();
|
||||
private RectF drawCircleOval = new RectF();
|
||||
private PointF sliceVector = new PointF();
|
||||
private int touchAdditional;
|
||||
private float circleFillRatio = 1.0f;
|
||||
|
||||
// Center circle related attributes
|
||||
private boolean hasCenterCircle;
|
||||
private float centerCircleScale;
|
||||
private Paint centerCirclePaint = new Paint();
|
||||
// Text1
|
||||
private Paint centerCircleText1Paint = new Paint();
|
||||
private FontMetricsInt centerCircleText1FontMetrics = new FontMetricsInt();
|
||||
// Text2
|
||||
private Paint centerCircleText2Paint = new Paint();
|
||||
private FontMetricsInt centerCircleText2FontMetrics = new FontMetricsInt();
|
||||
// Separation lines
|
||||
private Paint separationLinesPaint = new Paint();
|
||||
|
||||
private boolean hasLabelsOutside;
|
||||
private boolean hasLabels;
|
||||
private boolean hasLabelsOnlyForSelected;
|
||||
private PieChartValueFormatter valueFormatter;
|
||||
private Viewport tempMaximumViewport = new Viewport();
|
||||
|
||||
private Bitmap softwareBitmap;
|
||||
private Canvas softwareCanvas = new Canvas();
|
||||
|
||||
public PieChartRenderer(Context context, Chart chart, PieChartDataProvider dataProvider) {
|
||||
super(context, chart);
|
||||
this.dataProvider = dataProvider;
|
||||
touchAdditional = ChartUtils.dp2px(density, DEFAULT_TOUCH_ADDITIONAL_DP);
|
||||
|
||||
slicePaint.setAntiAlias(true);
|
||||
slicePaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
centerCirclePaint.setAntiAlias(true);
|
||||
centerCirclePaint.setStyle(Paint.Style.FILL);
|
||||
centerCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
|
||||
|
||||
centerCircleText1Paint.setAntiAlias(true);
|
||||
centerCircleText1Paint.setTextAlign(Align.CENTER);
|
||||
|
||||
centerCircleText2Paint.setAntiAlias(true);
|
||||
centerCircleText2Paint.setTextAlign(Align.CENTER);
|
||||
|
||||
separationLinesPaint.setAntiAlias(true);
|
||||
separationLinesPaint.setStyle(Paint.Style.STROKE);
|
||||
separationLinesPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
separationLinesPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
separationLinesPaint.setColor(Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartSizeChanged() {
|
||||
calculateCircleOval();
|
||||
|
||||
if (computator.getChartWidth() > 0 && computator.getChartHeight() > 0) {
|
||||
softwareBitmap = Bitmap.createBitmap(computator.getChartWidth(), computator.getChartHeight(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
softwareCanvas.setBitmap(softwareBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartDataChanged() {
|
||||
super.onChartDataChanged();
|
||||
final PieChartData data = dataProvider.getPieChartData();
|
||||
hasLabelsOutside = data.hasLabelsOutside();
|
||||
hasLabels = data.hasLabels();
|
||||
hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected();
|
||||
valueFormatter = data.getFormatter();
|
||||
hasCenterCircle = data.hasCenterCircle();
|
||||
centerCircleScale = data.getCenterCircleScale();
|
||||
centerCirclePaint.setColor(data.getCenterCircleColor());
|
||||
if (null != data.getCenterText1Typeface()) {
|
||||
centerCircleText1Paint.setTypeface(data.getCenterText1Typeface());
|
||||
}
|
||||
centerCircleText1Paint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getCenterText1FontSize()));
|
||||
centerCircleText1Paint.setColor(data.getCenterText1Color());
|
||||
centerCircleText1Paint.getFontMetricsInt(centerCircleText1FontMetrics);
|
||||
if (null != data.getCenterText2Typeface()) {
|
||||
centerCircleText2Paint.setTypeface(data.getCenterText2Typeface());
|
||||
}
|
||||
centerCircleText2Paint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getCenterText2FontSize()));
|
||||
centerCircleText2Paint.setColor(data.getCenterText2Color());
|
||||
centerCircleText2Paint.getFontMetricsInt(centerCircleText2FontMetrics);
|
||||
|
||||
onChartViewportChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChartViewportChanged() {
|
||||
if (isViewportCalculationEnabled) {
|
||||
calculateMaxViewport();
|
||||
computator.setMaxViewport(tempMaximumViewport);
|
||||
computator.setCurrentViewport(computator.getMaximumViewport());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
// softwareBitmap can be null if chart is rendered in layout editor. In that case use default canvas and not
|
||||
// softwareCanvas.
|
||||
final Canvas drawCanvas;
|
||||
if (null != softwareBitmap) {
|
||||
drawCanvas = softwareCanvas;
|
||||
drawCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
} else {
|
||||
drawCanvas = canvas;
|
||||
}
|
||||
|
||||
drawSlices(drawCanvas);
|
||||
drawSeparationLines(drawCanvas);
|
||||
if (hasCenterCircle) {
|
||||
drawCenterCircle(drawCanvas);
|
||||
}
|
||||
drawLabels(drawCanvas);
|
||||
|
||||
if (null != softwareBitmap) {
|
||||
canvas.drawBitmap(softwareBitmap, 0, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawUnclipped(Canvas canvas) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkTouch(float touchX, float touchY) {
|
||||
selectedValue.clear();
|
||||
final PieChartData data = dataProvider.getPieChartData();
|
||||
final float centerX = originCircleOval.centerX();
|
||||
final float centerY = originCircleOval.centerY();
|
||||
final float circleRadius = originCircleOval.width() / 2f;
|
||||
|
||||
sliceVector.set(touchX - centerX, touchY - centerY);
|
||||
// Check if touch is on circle area, if not return false;
|
||||
if (sliceVector.length() > circleRadius + touchAdditional) {
|
||||
return false;
|
||||
}
|
||||
// Check if touch is not in center circle, if yes return false;
|
||||
if (data.hasCenterCircle() && sliceVector.length() < circleRadius * data.getCenterCircleScale()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get touchAngle and align touch 0 degrees with chart 0 degrees, that why I subtracting start angle,
|
||||
// adding 360
|
||||
// and modulo 360 translates i.e -20 degrees to 340 degrees.
|
||||
final float touchAngle = (pointToAngle(touchX, touchY, centerX, centerY) - rotation + 360f) % 360f;
|
||||
final float sliceScale = 360f / maxSum;
|
||||
float lastAngle = 0f; // No start angle here, see above
|
||||
int sliceIndex = 0;
|
||||
for (SliceValue sliceValue : data.getValues()) {
|
||||
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
|
||||
if (touchAngle >= lastAngle) {
|
||||
selectedValue.set(sliceIndex, sliceIndex, SelectedValueType.NONE);
|
||||
}
|
||||
lastAngle += angle;
|
||||
++sliceIndex;
|
||||
}
|
||||
return isTouched();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw center circle with text if {@link PieChartData#hasCenterCircle()} is set true.
|
||||
*/
|
||||
private void drawCenterCircle(Canvas canvas) {
|
||||
final PieChartData data = dataProvider.getPieChartData();
|
||||
final float circleRadius = originCircleOval.width() / 2f;
|
||||
final float centerRadius = circleRadius * data.getCenterCircleScale();
|
||||
final float centerX = originCircleOval.centerX();
|
||||
final float centerY = originCircleOval.centerY();
|
||||
|
||||
canvas.drawCircle(centerX, centerY, centerRadius, centerCirclePaint);
|
||||
|
||||
// Draw center text1 and text2 if not empty.
|
||||
if (!TextUtils.isEmpty(data.getCenterText1())) {
|
||||
|
||||
final int text1Height = Math.abs(centerCircleText1FontMetrics.ascent);
|
||||
|
||||
if (!TextUtils.isEmpty(data.getCenterText2())) {
|
||||
// Draw text 2 only if text 1 is not empty.
|
||||
final int text2Height = Math.abs(centerCircleText2FontMetrics.ascent);
|
||||
canvas.drawText(data.getCenterText1(), centerX, centerY - text1Height * 0.2f, centerCircleText1Paint);
|
||||
canvas.drawText(data.getCenterText2(), centerX, centerY + text2Height, centerCircleText2Paint);
|
||||
} else {
|
||||
canvas.drawText(data.getCenterText1(), centerX, centerY + text1Height / 4, centerCircleText1Paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw all slices for this PieChart, if mode == {@link #MODE_HIGHLIGHT} currently selected slices will be redrawn
|
||||
* and
|
||||
* highlighted.
|
||||
*
|
||||
* @param canvas
|
||||
*/
|
||||
private void drawSlices(Canvas canvas) {
|
||||
final PieChartData data = dataProvider.getPieChartData();
|
||||
final float sliceScale = 360f / maxSum;
|
||||
float lastAngle = rotation;
|
||||
int sliceIndex = 0;
|
||||
for (SliceValue sliceValue : data.getValues()) {
|
||||
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
|
||||
if (isTouched() && selectedValue.getFirstIndex() == sliceIndex) {
|
||||
drawSlice(canvas, sliceValue, lastAngle, angle, MODE_HIGHLIGHT);
|
||||
} else {
|
||||
drawSlice(canvas, sliceValue, lastAngle, angle, MODE_DRAW);
|
||||
}
|
||||
lastAngle += angle;
|
||||
++sliceIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSeparationLines(Canvas canvas) {
|
||||
final PieChartData data = dataProvider.getPieChartData();
|
||||
if (data.getValues().size() < 2) {
|
||||
//No need for separation lines for 0 or 1 slices.
|
||||
return;
|
||||
}
|
||||
final int sliceSpacing = ChartUtils.dp2px(density, data.getSlicesSpacing());
|
||||
if (sliceSpacing < 1) {
|
||||
//No need for separation lines
|
||||
return;
|
||||
}
|
||||
final float sliceScale = 360f / maxSum;
|
||||
float lastAngle = rotation;
|
||||
final float circleRadius = originCircleOval.width() / 2f;
|
||||
separationLinesPaint.setStrokeWidth(sliceSpacing);
|
||||
for (SliceValue sliceValue : data.getValues()) {
|
||||
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
|
||||
|
||||
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle))),
|
||||
(float) (Math.sin(Math.toRadians(lastAngle))));
|
||||
normalizeVector(sliceVector);
|
||||
|
||||
float x1 = sliceVector.x * (circleRadius + touchAdditional) + originCircleOval.centerX();
|
||||
float y1 = sliceVector.y * (circleRadius + touchAdditional) + originCircleOval.centerY();
|
||||
|
||||
canvas.drawLine(originCircleOval.centerX(), originCircleOval.centerY(), x1, y1, separationLinesPaint);
|
||||
|
||||
lastAngle += angle;
|
||||
}
|
||||
}
|
||||
|
||||
public void drawLabels(Canvas canvas) {
|
||||
final PieChartData data = dataProvider.getPieChartData();
|
||||
final float sliceScale = 360f / maxSum;
|
||||
float lastAngle = rotation;
|
||||
int sliceIndex = 0;
|
||||
for (SliceValue sliceValue : data.getValues()) {
|
||||
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
|
||||
if (isTouched()) {
|
||||
if (hasLabels) {
|
||||
drawLabel(canvas, sliceValue, lastAngle, angle);
|
||||
} else if (hasLabelsOnlyForSelected && selectedValue.getFirstIndex() == sliceIndex) {
|
||||
drawLabel(canvas, sliceValue, lastAngle, angle);
|
||||
}
|
||||
} else {
|
||||
if (hasLabels) {
|
||||
drawLabel(canvas, sliceValue, lastAngle, angle);
|
||||
}
|
||||
}
|
||||
lastAngle += angle;
|
||||
++sliceIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method draws single slice from lastAngle to lastAngle+angle, if mode = {@link #MODE_HIGHLIGHT} slice will be
|
||||
* darken
|
||||
* and will have bigger radius.
|
||||
*/
|
||||
private void drawSlice(Canvas canvas, SliceValue sliceValue, float lastAngle, float angle, int mode) {
|
||||
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle + angle / 2))),
|
||||
(float) (Math.sin(Math.toRadians(lastAngle + angle / 2))));
|
||||
normalizeVector(sliceVector);
|
||||
drawCircleOval.set(originCircleOval);
|
||||
if (MODE_HIGHLIGHT == mode) {
|
||||
// Add additional touch feedback by setting bigger radius for that slice and darken color.
|
||||
drawCircleOval.inset(-touchAdditional, -touchAdditional);
|
||||
slicePaint.setColor(sliceValue.getDarkenColor());
|
||||
canvas.drawArc(drawCircleOval, lastAngle, angle, true, slicePaint);
|
||||
} else {
|
||||
slicePaint.setColor(sliceValue.getColor());
|
||||
canvas.drawArc(drawCircleOval, lastAngle, angle, true, slicePaint);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLabel(Canvas canvas, SliceValue sliceValue, float lastAngle, float angle) {
|
||||
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle + angle / 2))),
|
||||
(float) (Math.sin(Math.toRadians(lastAngle + angle / 2))));
|
||||
normalizeVector(sliceVector);
|
||||
|
||||
final int numChars = valueFormatter.formatChartValue(labelBuffer, sliceValue);
|
||||
|
||||
if (numChars == 0) {
|
||||
// No need to draw empty label
|
||||
return;
|
||||
}
|
||||
|
||||
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
|
||||
final int labelHeight = Math.abs(fontMetrics.ascent);
|
||||
|
||||
final float centerX = originCircleOval.centerX();
|
||||
final float centerY = originCircleOval.centerY();
|
||||
final float circleRadius = originCircleOval.width() / 2f;
|
||||
final float labelRadius;
|
||||
|
||||
if (hasLabelsOutside) {
|
||||
labelRadius = circleRadius * DEFAULT_LABEL_OUTSIDE_RADIUS_FACTOR;
|
||||
} else {
|
||||
if (hasCenterCircle) {
|
||||
labelRadius = circleRadius - (circleRadius - (circleRadius * centerCircleScale)) / 2;
|
||||
} else {
|
||||
labelRadius = circleRadius * DEFAULT_LABEL_INSIDE_RADIUS_FACTOR;
|
||||
}
|
||||
}
|
||||
|
||||
final float rawX = labelRadius * sliceVector.x + centerX;
|
||||
final float rawY = labelRadius * sliceVector.y + centerY;
|
||||
|
||||
float left;
|
||||
float right;
|
||||
float top;
|
||||
float bottom;
|
||||
|
||||
if (hasLabelsOutside) {
|
||||
if (rawX > centerX) {
|
||||
// Right half.
|
||||
left = rawX + labelMargin;
|
||||
right = rawX + labelWidth + labelMargin * 3;
|
||||
} else {
|
||||
left = rawX - labelWidth - labelMargin * 3;
|
||||
right = rawX - labelMargin;
|
||||
}
|
||||
|
||||
if (rawY > centerY) {
|
||||
// Lower half.
|
||||
top = rawY + labelMargin;
|
||||
bottom = rawY + labelHeight + labelMargin * 3;
|
||||
} else {
|
||||
top = rawY - labelHeight - labelMargin * 3;
|
||||
bottom = rawY - labelMargin;
|
||||
}
|
||||
} else {
|
||||
left = rawX - labelWidth / 2 - labelMargin;
|
||||
right = rawX + labelWidth / 2 + labelMargin;
|
||||
top = rawY - labelHeight / 2 - labelMargin;
|
||||
bottom = rawY + labelHeight / 2 + labelMargin;
|
||||
}
|
||||
|
||||
labelBackgroundRect.set(left, top, right, bottom);
|
||||
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
|
||||
sliceValue.getDarkenColor());
|
||||
}
|
||||
|
||||
private void normalizeVector(PointF point) {
|
||||
final float abs = point.length();
|
||||
point.set(point.x / abs, point.y / abs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates angle of touched point.
|
||||
*/
|
||||
private float pointToAngle(float x, float y, float centerX, float centerY) {
|
||||
double diffX = x - centerX;
|
||||
double diffY = y - centerY;
|
||||
// Pass -diffX to get clockwise degrees order.
|
||||
double radian = Math.atan2(-diffX, diffY);
|
||||
|
||||
float angle = ((float) Math.toDegrees(radian) + 360) % 360;
|
||||
// Add 90 because atan2 returns 0 degrees at 6 o'clock.
|
||||
angle += 90f;
|
||||
return angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates rectangle(square) that will constraint chart circle.
|
||||
*/
|
||||
private void calculateCircleOval() {
|
||||
Rect contentRect = computator.getContentRectMinusAllMargins();
|
||||
final float circleRadius = Math.min(contentRect.width() / 2f, contentRect.height() / 2f);
|
||||
final float centerX = contentRect.centerX();
|
||||
final float centerY = contentRect.centerY();
|
||||
final float left = centerX - circleRadius + touchAdditional;
|
||||
final float top = centerY - circleRadius + touchAdditional;
|
||||
final float right = centerX + circleRadius - touchAdditional;
|
||||
final float bottom = centerY + circleRadius - touchAdditional;
|
||||
originCircleOval.set(left, top, right, bottom);
|
||||
final float inest = 0.5f * originCircleOval.width() * (1.0f - circleFillRatio);
|
||||
originCircleOval.inset(inest, inest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewport is not really important for PieChart, this kind of chart doesn't relay on viewport but uses pixels
|
||||
* coordinates instead. This method also calculates sum of all SliceValues.
|
||||
*/
|
||||
private void calculateMaxViewport() {
|
||||
tempMaximumViewport.set(0, MAX_WIDTH_HEIGHT, MAX_WIDTH_HEIGHT, 0);
|
||||
maxSum = 0.0f;
|
||||
for (SliceValue sliceValue : dataProvider.getPieChartData().getValues()) {
|
||||
maxSum += Math.abs(sliceValue.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public RectF getCircleOval() {
|
||||
return originCircleOval;
|
||||
}
|
||||
|
||||
public void setCircleOval(RectF orginCircleOval) {
|
||||
this.originCircleOval = orginCircleOval;
|
||||
}
|
||||
|
||||
public int getChartRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public void setChartRotation(int rotation) {
|
||||
rotation = (rotation % 360 + 360) % 360;
|
||||
this.rotation = rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns SliceValue that is under given angle, selectedValue (if not null) will be hold slice index.
|
||||
*/
|
||||
public SliceValue getValueForAngle(int angle, SelectedValue selectedValue) {
|
||||
final PieChartData data = dataProvider.getPieChartData();
|
||||
final float touchAngle = (angle - rotation + 360f) % 360f;
|
||||
final float sliceScale = 360f / maxSum;
|
||||
float lastAngle = 0f;
|
||||
int sliceIndex = 0;
|
||||
for (SliceValue sliceValue : data.getValues()) {
|
||||
final float tempAngle = Math.abs(sliceValue.getValue()) * sliceScale;
|
||||
if (touchAngle >= lastAngle) {
|
||||
if (null != selectedValue) {
|
||||
selectedValue.set(sliceIndex, sliceIndex, SelectedValueType.NONE);
|
||||
}
|
||||
return sliceValue;
|
||||
}
|
||||
lastAngle += tempAngle;
|
||||
++sliceIndex;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setCircleFillRatio(float)
|
||||
*/
|
||||
public float getCircleFillRatio() {
|
||||
return this.circleFillRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set how much of view area should be taken by chart circle. Value should be between 0 and 1. Default is 1 so
|
||||
* circle will have radius equals min(View.width, View.height).
|
||||
*/
|
||||
public void setCircleFillRatio(float fillRatio) {
|
||||
if (fillRatio < 0) {
|
||||
fillRatio = 0;
|
||||
} else if (fillRatio > 1) {
|
||||
fillRatio = 1;
|
||||
}
|
||||
|
||||
this.circleFillRatio = fillRatio;
|
||||
calculateCircleOval();
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Renderer for preview chart based on ColumnChart. In addition to drawing chart data it also draw current viewport as
|
||||
* preview area.
|
||||
*/
|
||||
public class PreviewColumnChartRenderer extends ColumnChartRenderer {
|
||||
private static final int DEFAULT_PREVIEW_TRANSPARENCY = 64;
|
||||
private static final int FULL_ALPHA = 255;
|
||||
private static final int DEFAULT_PREVIEW_STROKE_WIDTH_DP = 2;
|
||||
|
||||
private Paint previewPaint = new Paint();
|
||||
|
||||
public PreviewColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider dataProvider) {
|
||||
super(context, chart, dataProvider);
|
||||
previewPaint.setAntiAlias(true);
|
||||
previewPaint.setColor(Color.LTGRAY);
|
||||
previewPaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_PREVIEW_STROKE_WIDTH_DP));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawUnclipped(Canvas canvas) {
|
||||
super.drawUnclipped(canvas);
|
||||
final Viewport currentViewport = computator.getCurrentViewport();
|
||||
final float left = computator.computeRawX(currentViewport.left);
|
||||
final float top = computator.computeRawY(currentViewport.top);
|
||||
final float right = computator.computeRawX(currentViewport.right);
|
||||
final float bottom = computator.computeRawY(currentViewport.bottom);
|
||||
previewPaint.setAlpha(DEFAULT_PREVIEW_TRANSPARENCY);
|
||||
previewPaint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawRect(left, top, right, bottom, previewPaint);
|
||||
previewPaint.setStyle(Paint.Style.STROKE);
|
||||
previewPaint.setAlpha(FULL_ALPHA);
|
||||
canvas.drawRect(left, top, right, bottom, previewPaint);
|
||||
}
|
||||
|
||||
public int getPreviewColor() {
|
||||
return previewPaint.getColor();
|
||||
}
|
||||
|
||||
public void setPreviewColor(int color) {
|
||||
previewPaint.setColor(color);
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package lecho.lib.hellocharts.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.provider.LineChartDataProvider;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
import lecho.lib.hellocharts.view.Chart;
|
||||
|
||||
/**
|
||||
* Renderer for preview chart based on LineChart. In addition to drawing chart data it also draw current viewport as
|
||||
* preview area.
|
||||
*/
|
||||
public class PreviewLineChartRenderer extends LineChartRenderer {
|
||||
private static final int DEFAULT_PREVIEW_TRANSPARENCY = 64;
|
||||
private static final int FULL_ALPHA = 255;
|
||||
private static final int DEFAULT_PREVIEW_STROKE_WIDTH_DP = 2;
|
||||
|
||||
private Paint previewPaint = new Paint();
|
||||
|
||||
public PreviewLineChartRenderer(Context context, Chart chart, LineChartDataProvider dataProvider) {
|
||||
super(context, chart, dataProvider);
|
||||
previewPaint.setAntiAlias(true);
|
||||
previewPaint.setColor(Color.LTGRAY);
|
||||
previewPaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_PREVIEW_STROKE_WIDTH_DP));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawUnclipped(Canvas canvas) {
|
||||
super.drawUnclipped(canvas);
|
||||
final Viewport currentViewport = computator.getCurrentViewport();
|
||||
final float left = computator.computeRawX(currentViewport.left);
|
||||
final float top = computator.computeRawY(currentViewport.top);
|
||||
final float right = computator.computeRawX(currentViewport.right);
|
||||
final float bottom = computator.computeRawY(currentViewport.bottom);
|
||||
previewPaint.setAlpha(DEFAULT_PREVIEW_TRANSPARENCY);
|
||||
previewPaint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawRect(left, top, right, bottom, previewPaint);
|
||||
previewPaint.setStyle(Paint.Style.STROKE);
|
||||
previewPaint.setAlpha(FULL_ALPHA);
|
||||
canvas.drawRect(left, top, right, bottom, previewPaint);
|
||||
}
|
||||
|
||||
public int getPreviewColor() {
|
||||
return previewPaint.getColor();
|
||||
}
|
||||
|
||||
public void setPreviewColor(int color) {
|
||||
previewPaint.setColor(color);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package lecho.lib.hellocharts.util;
|
||||
|
||||
/**
|
||||
* A simple class representing axis label values used only for auto generated axes.
|
||||
*/
|
||||
public class AxisAutoValues {
|
||||
public float[] values = new float[]{};
|
||||
public int valuesNumber;
|
||||
public int decimals;
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package lecho.lib.hellocharts.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.util.TypedValue;
|
||||
|
||||
public abstract class ChartUtils {
|
||||
|
||||
public static final int DEFAULT_COLOR = Color.parseColor("#DFDFDF");
|
||||
public static final int DEFAULT_DARKEN_COLOR = Color.parseColor("#DDDDDD");
|
||||
public static final int COLOR_BLUE = Color.parseColor("#33B5E5");
|
||||
public static final int COLOR_VIOLET = Color.parseColor("#AA66CC");
|
||||
public static final int COLOR_GREEN = Color.parseColor("#99CC00");
|
||||
public static final int COLOR_ORANGE = Color.parseColor("#FFBB33");
|
||||
public static final int COLOR_RED = Color.parseColor("#FF4444");
|
||||
public static final int[] COLORS = new int[]{COLOR_BLUE, COLOR_VIOLET, COLOR_GREEN, COLOR_ORANGE, COLOR_RED};
|
||||
private static final float DARKEN_SATURATION = 1.1f;
|
||||
private static final float DARKEN_INTENSITY = 0.9f;
|
||||
private static int COLOR_INDEX = 0;
|
||||
|
||||
public static final int pickColor() {
|
||||
return COLORS[(int) Math.round(Math.random() * (COLORS.length - 1))];
|
||||
}
|
||||
|
||||
public static final int nextColor() {
|
||||
if (COLOR_INDEX >= COLORS.length) {
|
||||
COLOR_INDEX = 0;
|
||||
}
|
||||
return COLORS[COLOR_INDEX++];
|
||||
}
|
||||
|
||||
public static int dp2px(float density, int dp) {
|
||||
if (dp == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int) (dp * density + 0.5f);
|
||||
|
||||
}
|
||||
|
||||
public static int px2dp(float density, int px) {
|
||||
return (int) Math.ceil(px / density);
|
||||
}
|
||||
|
||||
public static int sp2px(float scaledDensity, int sp) {
|
||||
if (sp == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int) (sp * scaledDensity + 0.5f);
|
||||
}
|
||||
|
||||
public static int px2sp(float scaledDensity, int px) {
|
||||
return (int) Math.ceil(px / scaledDensity);
|
||||
}
|
||||
|
||||
public static int mm2px(Context context, int mm) {
|
||||
return (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, mm, context.getResources()
|
||||
.getDisplayMetrics()) + 0.5f);
|
||||
}
|
||||
|
||||
public static int darkenColor(int color) {
|
||||
float[] hsv = new float[3];
|
||||
int alpha = Color.alpha(color);
|
||||
Color.colorToHSV(color, hsv);
|
||||
hsv[1] = Math.min(hsv[1] * DARKEN_SATURATION, 1.0f);
|
||||
hsv[2] = hsv[2] * DARKEN_INTENSITY;
|
||||
int tempColor = Color.HSVToColor(hsv);
|
||||
return Color.argb(alpha, Color.red(tempColor), Color.green(tempColor), Color.blue(tempColor));
|
||||
}
|
||||
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package lecho.lib.hellocharts.util;
|
||||
|
||||
public class FloatUtils {
|
||||
public static final int POW10[] = {1, 10, 100, 1000, 10000, 100000, 1000000};
|
||||
|
||||
/**
|
||||
* Returns next bigger float value considering precision of the argument.
|
||||
*/
|
||||
public static float nextUpF(float f) {
|
||||
if (Float.isNaN(f) || f == Float.POSITIVE_INFINITY) {
|
||||
return f;
|
||||
} else {
|
||||
f += 0.0f;
|
||||
return Float.intBitsToFloat(Float.floatToRawIntBits(f) + ((f >= 0.0f) ? +1 : -1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next smaller float value considering precision of the argument.
|
||||
*/
|
||||
public static float nextDownF(float f) {
|
||||
if (Float.isNaN(f) || f == Float.NEGATIVE_INFINITY) {
|
||||
return f;
|
||||
} else {
|
||||
if (f == 0.0f) {
|
||||
return -Float.MIN_VALUE;
|
||||
} else {
|
||||
return Float.intBitsToFloat(Float.floatToRawIntBits(f) + ((f > 0.0f) ? -1 : +1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next bigger double value considering precision of the argument.
|
||||
*/
|
||||
public static double nextUp(double d) {
|
||||
if (Double.isNaN(d) || d == Double.POSITIVE_INFINITY) {
|
||||
return d;
|
||||
} else {
|
||||
d += 0.0;
|
||||
return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + ((d >= 0.0) ? +1 : -1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next smaller float value considering precision of the argument.
|
||||
*/
|
||||
public static double nextDown(double d) {
|
||||
if (Double.isNaN(d) || d == Double.NEGATIVE_INFINITY) {
|
||||
return d;
|
||||
} else {
|
||||
if (d == 0.0f) {
|
||||
return -Float.MIN_VALUE;
|
||||
} else {
|
||||
return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + ((d > 0.0f) ? -1 : +1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method checks if two float numbers are similar.
|
||||
*/
|
||||
public static boolean almostEqual(float a, float b, float absoluteDiff, float relativeDiff) {
|
||||
float diff = Math.abs(a - b);
|
||||
if (diff <= absoluteDiff) {
|
||||
return true;
|
||||
}
|
||||
|
||||
a = Math.abs(a);
|
||||
b = Math.abs(b);
|
||||
float largest = (a > b) ? a : b;
|
||||
|
||||
if (diff <= largest * relativeDiff) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the given number to the given number of significant digits. Based on an answer on <a
|
||||
* href="http://stackoverflow.com/questions/202302">Stack Overflow</a>.
|
||||
*/
|
||||
public static float roundToOneSignificantFigure(double num) {
|
||||
final float d = (float) Math.ceil((float) Math.log10(num < 0 ? -num : num));
|
||||
final int power = 1 - (int) d;
|
||||
final float magnitude = (float) Math.pow(10, power);
|
||||
final long shifted = Math.round(num * magnitude);
|
||||
return shifted / magnitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a float value to the given number of decimals. Returns the length of the string. The string begins at
|
||||
* [endIndex] - [return value] and ends at [endIndex]. It's up to you to check indexes correctness.
|
||||
* Parameter [endIndex] can be helpful when you want to append some text to formatted value.
|
||||
*
|
||||
* @return number of characters of formatted value
|
||||
*/
|
||||
public static int formatFloat(final char[] formattedValue, float value, int endIndex, int digits, char separator) {
|
||||
if (digits >= POW10.length) {
|
||||
formattedValue[endIndex - 1] = '.';
|
||||
return 1;
|
||||
}
|
||||
boolean negative = false;
|
||||
if (value == 0) {
|
||||
formattedValue[endIndex - 1] = '0';
|
||||
return 1;
|
||||
}
|
||||
if (value < 0) {
|
||||
negative = true;
|
||||
value = -value;
|
||||
}
|
||||
if (digits > POW10.length) {
|
||||
digits = POW10.length - 1;
|
||||
}
|
||||
value *= POW10[digits];
|
||||
long lval = Math.round(value);
|
||||
int index = endIndex - 1;
|
||||
int charsNumber = 0;
|
||||
while (lval != 0 || charsNumber < (digits + 1)) {
|
||||
int digit = (int) (lval % 10);
|
||||
lval = lval / 10;
|
||||
formattedValue[index--] = (char) (digit + '0');
|
||||
charsNumber++;
|
||||
if (charsNumber == digits) {
|
||||
formattedValue[index--] = separator;
|
||||
charsNumber++;
|
||||
}
|
||||
}
|
||||
if (formattedValue[index + 1] == separator) {
|
||||
formattedValue[index--] = '0';
|
||||
charsNumber++;
|
||||
}
|
||||
if (negative) {
|
||||
formattedValue[index--] = '-';
|
||||
charsNumber++;
|
||||
}
|
||||
return charsNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the set of axis labels to show given start and stop boundaries and an ideal number of stops between
|
||||
* these boundaries.
|
||||
*
|
||||
* @param start The minimum extreme (e.g. the left edge) for the axis.
|
||||
* @param stop The maximum extreme (e.g. the right edge) for the axis.
|
||||
* @param steps The ideal number of stops to create. This should be based on available screen space; the more
|
||||
* space
|
||||
* there is, the more stops should be shown.
|
||||
* @param outValues The destination {@link AxisAutoValues} object to populate.
|
||||
*/
|
||||
public static void computeAutoGeneratedAxisValues(float start, float stop, int steps, AxisAutoValues outValues) {
|
||||
double range = stop - start;
|
||||
if (steps == 0 || range <= 0) {
|
||||
outValues.values = new float[]{};
|
||||
outValues.valuesNumber = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double rawInterval = range / steps;
|
||||
double interval = roundToOneSignificantFigure(rawInterval);
|
||||
double intervalMagnitude = 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
|
||||
interval = Math.floor(10 * intervalMagnitude);
|
||||
}
|
||||
|
||||
double first = Math.ceil(start / interval) * interval;
|
||||
double last = nextUp(Math.floor(stop / interval) * interval);
|
||||
|
||||
double intervalValue;
|
||||
int valueIndex;
|
||||
int valuesNum = 0;
|
||||
for (intervalValue = first; intervalValue <= last; intervalValue += interval) {
|
||||
++valuesNum;
|
||||
}
|
||||
|
||||
outValues.valuesNumber = valuesNum;
|
||||
|
||||
if (outValues.values.length < valuesNum) {
|
||||
// Ensure stops contains at least numStops elements.
|
||||
outValues.values = new float[valuesNum];
|
||||
}
|
||||
|
||||
for (intervalValue = first, valueIndex = 0; valueIndex < valuesNum; intervalValue += interval, ++valueIndex) {
|
||||
outValues.values[valueIndex] = (float) intervalValue;
|
||||
}
|
||||
|
||||
if (interval < 1) {
|
||||
outValues.decimals = (int) Math.ceil(-Math.log10(interval));
|
||||
} else {
|
||||
outValues.decimals = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+507
@@ -0,0 +1,507 @@
|
||||
package lecho.lib.hellocharts.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import lecho.lib.hellocharts.animation.ChartAnimationListener;
|
||||
import lecho.lib.hellocharts.animation.ChartDataAnimator;
|
||||
import lecho.lib.hellocharts.animation.ChartDataAnimatorV14;
|
||||
import lecho.lib.hellocharts.animation.ChartDataAnimatorV8;
|
||||
import lecho.lib.hellocharts.animation.ChartViewportAnimator;
|
||||
import lecho.lib.hellocharts.animation.ChartViewportAnimatorV14;
|
||||
import lecho.lib.hellocharts.animation.ChartViewportAnimatorV8;
|
||||
import lecho.lib.hellocharts.computator.ChartComputator;
|
||||
import lecho.lib.hellocharts.gesture.ChartTouchHandler;
|
||||
import lecho.lib.hellocharts.gesture.ContainerScrollType;
|
||||
import lecho.lib.hellocharts.gesture.ZoomType;
|
||||
import lecho.lib.hellocharts.listener.ViewportChangeListener;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.renderer.AxesRenderer;
|
||||
import lecho.lib.hellocharts.renderer.ChartRenderer;
|
||||
import lecho.lib.hellocharts.util.ChartUtils;
|
||||
|
||||
/**
|
||||
* Abstract class for charts views.
|
||||
*
|
||||
* @author Leszek Wach
|
||||
*/
|
||||
public abstract class AbstractChartView extends View implements Chart {
|
||||
protected ChartComputator chartComputator;
|
||||
protected AxesRenderer axesRenderer;
|
||||
protected ChartTouchHandler touchHandler;
|
||||
protected ChartRenderer chartRenderer;
|
||||
protected ChartDataAnimator dataAnimator;
|
||||
protected ChartViewportAnimator viewportAnimator;
|
||||
protected boolean isInteractive = true;
|
||||
protected boolean isContainerScrollEnabled = false;
|
||||
protected ContainerScrollType containerScrollType;
|
||||
|
||||
public AbstractChartView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public AbstractChartView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public AbstractChartView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
chartComputator = new ChartComputator();
|
||||
touchHandler = new ChartTouchHandler(context, this);
|
||||
axesRenderer = new AxesRenderer(context, this);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
this.dataAnimator = new ChartDataAnimatorV8(this);
|
||||
this.viewportAnimator = new ChartViewportAnimatorV8(this);
|
||||
} else {
|
||||
this.viewportAnimator = new ChartViewportAnimatorV14(this);
|
||||
this.dataAnimator = new ChartDataAnimatorV14(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||
chartComputator.setContentRect(getWidth(), getHeight(), getPaddingLeft(), getPaddingTop(), getPaddingRight(),
|
||||
getPaddingBottom());
|
||||
chartRenderer.onChartSizeChanged();
|
||||
axesRenderer.onChartSizeChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (isEnabled()) {
|
||||
axesRenderer.drawInBackground(canvas);
|
||||
int clipRestoreCount = canvas.save();
|
||||
canvas.clipRect(chartComputator.getContentRectMinusAllMargins());
|
||||
chartRenderer.draw(canvas);
|
||||
canvas.restoreToCount(clipRestoreCount);
|
||||
chartRenderer.drawUnclipped(canvas);
|
||||
axesRenderer.drawInForeground(canvas);
|
||||
} else {
|
||||
canvas.drawColor(ChartUtils.DEFAULT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
super.onTouchEvent(event);
|
||||
|
||||
if (isInteractive) {
|
||||
|
||||
boolean needInvalidate;
|
||||
|
||||
if (isContainerScrollEnabled) {
|
||||
needInvalidate = touchHandler.handleTouchEvent(event, getParent(), containerScrollType);
|
||||
} else {
|
||||
needInvalidate = touchHandler.handleTouchEvent(event);
|
||||
}
|
||||
|
||||
if (needInvalidate) {
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void computeScroll() {
|
||||
super.computeScroll();
|
||||
if (isInteractive) {
|
||||
if (touchHandler.computeScroll()) {
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startDataAnimation() {
|
||||
dataAnimator.startAnimation(Long.MIN_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startDataAnimation(long duration) {
|
||||
dataAnimator.startAnimation(duration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelDataAnimation() {
|
||||
dataAnimator.cancelAnimation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animationDataUpdate(float scale) {
|
||||
getChartData().update(scale);
|
||||
chartRenderer.onChartViewportChanged();
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animationDataFinished() {
|
||||
getChartData().finish();
|
||||
chartRenderer.onChartViewportChanged();
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDataAnimationListener(ChartAnimationListener animationListener) {
|
||||
dataAnimator.setChartAnimationListener(animationListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setViewportAnimationListener(ChartAnimationListener animationListener) {
|
||||
viewportAnimator.setChartAnimationListener(animationListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setViewportChangeListener(ViewportChangeListener viewportChangeListener) {
|
||||
chartComputator.setViewportChangeListener(viewportChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChartRenderer getChartRenderer() {
|
||||
return chartRenderer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChartRenderer(ChartRenderer renderer) {
|
||||
chartRenderer = renderer;
|
||||
resetRendererAndTouchHandler();
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AxesRenderer getAxesRenderer() {
|
||||
return axesRenderer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChartComputator getChartComputator() {
|
||||
return chartComputator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChartTouchHandler getTouchHandler() {
|
||||
return touchHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInteractive() {
|
||||
return isInteractive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInteractive(boolean isInteractive) {
|
||||
this.isInteractive = isInteractive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isZoomEnabled() {
|
||||
return touchHandler.isZoomEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZoomEnabled(boolean isZoomEnabled) {
|
||||
touchHandler.setZoomEnabled(isZoomEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScrollEnabled() {
|
||||
return touchHandler.isScrollEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScrollEnabled(boolean isScrollEnabled) {
|
||||
touchHandler.setScrollEnabled(isScrollEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(float x, float y) {
|
||||
Viewport scrollViewport = computeScrollViewport(x, y);
|
||||
setCurrentViewport(scrollViewport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveToWithAnimation(float x, float y) {
|
||||
Viewport scrollViewport = computeScrollViewport(x, y);
|
||||
setCurrentViewportWithAnimation(scrollViewport);
|
||||
}
|
||||
|
||||
private Viewport computeScrollViewport(float x, float y) {
|
||||
Viewport maxViewport = getMaximumViewport();
|
||||
Viewport currentViewport = getCurrentViewport();
|
||||
Viewport scrollViewport = new Viewport(currentViewport);
|
||||
|
||||
if (maxViewport.contains(x, y)) {
|
||||
final float width = currentViewport.width();
|
||||
final float height = currentViewport.height();
|
||||
|
||||
final float halfWidth = width / 2;
|
||||
final float halfHeight = height / 2;
|
||||
|
||||
float left = x - halfWidth;
|
||||
float top = y + halfHeight;
|
||||
|
||||
left = Math.max(maxViewport.left, Math.min(left, maxViewport.right - width));
|
||||
top = Math.max(maxViewport.bottom + height, Math.min(top, maxViewport.top));
|
||||
|
||||
scrollViewport.set(left, top, left + width, top - height);
|
||||
}
|
||||
|
||||
return scrollViewport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValueTouchEnabled() {
|
||||
return touchHandler.isValueTouchEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueTouchEnabled(boolean isValueTouchEnabled) {
|
||||
touchHandler.setValueTouchEnabled(isValueTouchEnabled);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZoomType getZoomType() {
|
||||
return touchHandler.getZoomType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZoomType(ZoomType zoomType) {
|
||||
touchHandler.setZoomType(zoomType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMaxZoom() {
|
||||
return chartComputator.getMaxZoom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxZoom(float maxZoom) {
|
||||
chartComputator.setMaxZoom(maxZoom);
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getZoomLevel() {
|
||||
Viewport maxViewport = getMaximumViewport();
|
||||
Viewport currentViewport = getCurrentViewport();
|
||||
|
||||
return Math.max(maxViewport.width() / currentViewport.width(), maxViewport.height() / currentViewport.height());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZoomLevel(float x, float y, float zoomLevel) {
|
||||
Viewport zoomViewport = computeZoomViewport(x, y, zoomLevel);
|
||||
setCurrentViewport(zoomViewport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZoomLevelWithAnimation(float x, float y, float zoomLevel) {
|
||||
Viewport zoomViewport = computeZoomViewport(x, y, zoomLevel);
|
||||
setCurrentViewportWithAnimation(zoomViewport);
|
||||
}
|
||||
|
||||
private Viewport computeZoomViewport(float x, float y, float zoomLevel) {
|
||||
final Viewport maxViewport = getMaximumViewport();
|
||||
Viewport zoomViewport = new Viewport(getMaximumViewport());
|
||||
|
||||
if (maxViewport.contains(x, y)) {
|
||||
|
||||
if (zoomLevel < 1) {
|
||||
zoomLevel = 1;
|
||||
} else if (zoomLevel > getMaxZoom()) {
|
||||
zoomLevel = getMaxZoom();
|
||||
}
|
||||
|
||||
final float newWidth = zoomViewport.width() / zoomLevel;
|
||||
final float newHeight = zoomViewport.height() / zoomLevel;
|
||||
|
||||
final float halfWidth = newWidth / 2;
|
||||
final float halfHeight = newHeight / 2;
|
||||
|
||||
float left = x - halfWidth;
|
||||
float right = x + halfWidth;
|
||||
float top = y + halfHeight;
|
||||
float bottom = y - halfHeight;
|
||||
|
||||
if (left < maxViewport.left) {
|
||||
left = maxViewport.left;
|
||||
right = left + newWidth;
|
||||
} else if (right > maxViewport.right) {
|
||||
right = maxViewport.right;
|
||||
left = right - newWidth;
|
||||
}
|
||||
|
||||
if (top > maxViewport.top) {
|
||||
top = maxViewport.top;
|
||||
bottom = top - newHeight;
|
||||
} else if (bottom < maxViewport.bottom) {
|
||||
bottom = maxViewport.bottom;
|
||||
top = bottom + newHeight;
|
||||
}
|
||||
|
||||
ZoomType zoomType = getZoomType();
|
||||
if (ZoomType.HORIZONTAL_AND_VERTICAL == zoomType) {
|
||||
zoomViewport.set(left, top, right, bottom);
|
||||
} else if (ZoomType.HORIZONTAL == zoomType) {
|
||||
zoomViewport.left = left;
|
||||
zoomViewport.right = right;
|
||||
} else if (ZoomType.VERTICAL == zoomType) {
|
||||
zoomViewport.top = top;
|
||||
zoomViewport.bottom = bottom;
|
||||
}
|
||||
|
||||
}
|
||||
return zoomViewport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Viewport getMaximumViewport() {
|
||||
return chartRenderer.getMaximumViewport();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaximumViewport(Viewport maxViewport) {
|
||||
chartRenderer.setMaximumViewport(maxViewport);
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentViewportWithAnimation(Viewport targetViewport) {
|
||||
if (null != targetViewport) {
|
||||
viewportAnimator.cancelAnimation();
|
||||
viewportAnimator.startAnimation(getCurrentViewport(), targetViewport);
|
||||
}
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentViewportWithAnimation(Viewport targetViewport, long duration) {
|
||||
if (null != targetViewport) {
|
||||
viewportAnimator.cancelAnimation();
|
||||
viewportAnimator.startAnimation(getCurrentViewport(), targetViewport, duration);
|
||||
}
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Viewport getCurrentViewport() {
|
||||
return getChartRenderer().getCurrentViewport();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentViewport(Viewport targetViewport) {
|
||||
if (null != targetViewport) {
|
||||
chartRenderer.setCurrentViewport(targetViewport);
|
||||
}
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetViewports() {
|
||||
chartRenderer.setMaximumViewport(null);
|
||||
chartRenderer.setCurrentViewport(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewportCalculationEnabled() {
|
||||
return chartRenderer.isViewportCalculationEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setViewportCalculationEnabled(boolean isEnabled) {
|
||||
chartRenderer.setViewportCalculationEnabled(isEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValueSelectionEnabled() {
|
||||
return touchHandler.isValueSelectionEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueSelectionEnabled(boolean isValueSelectionEnabled) {
|
||||
touchHandler.setValueSelectionEnabled(isValueSelectionEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectValue(SelectedValue selectedValue) {
|
||||
chartRenderer.selectValue(selectedValue);
|
||||
callTouchListener();
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SelectedValue getSelectedValue() {
|
||||
return chartRenderer.getSelectedValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isContainerScrollEnabled() {
|
||||
return isContainerScrollEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContainerScrollEnabled(boolean isContainerScrollEnabled, ContainerScrollType containerScrollType) {
|
||||
this.isContainerScrollEnabled = isContainerScrollEnabled;
|
||||
this.containerScrollType = containerScrollType;
|
||||
}
|
||||
|
||||
protected void onChartDataChange() {
|
||||
chartComputator.resetContentRect();
|
||||
chartRenderer.onChartDataChanged();
|
||||
axesRenderer.onChartDataChanged();
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* You should call this method in derived classes, most likely from constructor if you changed chart/axis renderer,
|
||||
* touch handler or chart computator
|
||||
*/
|
||||
protected void resetRendererAndTouchHandler() {
|
||||
this.chartRenderer.resetRenderer();
|
||||
this.axesRenderer.resetRenderer();
|
||||
this.touchHandler.resetTouchHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* When embedded in a ViewPager, this will be called in order to know if we can scroll.
|
||||
* If this returns true, the ViewPager will ignore the drag so that we can scroll our content.
|
||||
* If this return false, the ViewPager will assume we won't be able to scroll and will consume the drag
|
||||
*
|
||||
* @param direction Amount of pixels being scrolled (x axis)
|
||||
* @return true if the chart can be scrolled (ie. zoomed and not against the edge of the chart)
|
||||
*/
|
||||
@Override
|
||||
public boolean canScrollHorizontally(int direction) {
|
||||
if (getZoomLevel() <= 1.0) {
|
||||
return false;
|
||||
}
|
||||
final Viewport currentViewport = getCurrentViewport();
|
||||
final Viewport maximumViewport = getMaximumViewport();
|
||||
if (direction < 0) {
|
||||
return currentViewport.left > maximumViewport.left;
|
||||
} else {
|
||||
return currentViewport.right < maximumViewport.right;
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package lecho.lib.hellocharts.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import lecho.lib.hellocharts.BuildConfig;
|
||||
import lecho.lib.hellocharts.listener.BubbleChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.listener.DummyBubbleChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.model.BubbleChartData;
|
||||
import lecho.lib.hellocharts.model.BubbleValue;
|
||||
import lecho.lib.hellocharts.model.ChartData;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.provider.BubbleChartDataProvider;
|
||||
import lecho.lib.hellocharts.renderer.BubbleChartRenderer;
|
||||
|
||||
/**
|
||||
* BubbleChart, supports circle bubbles and square bubbles.
|
||||
*
|
||||
* @author lecho
|
||||
*/
|
||||
public class BubbleChartView extends AbstractChartView implements BubbleChartDataProvider {
|
||||
private static final String TAG = "BubbleChartView";
|
||||
protected BubbleChartData data;
|
||||
protected BubbleChartOnValueSelectListener onValueTouchListener = new DummyBubbleChartOnValueSelectListener();
|
||||
|
||||
protected BubbleChartRenderer bubbleChartRenderer;
|
||||
|
||||
public BubbleChartView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public BubbleChartView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public BubbleChartView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
bubbleChartRenderer = new BubbleChartRenderer(context, this, this);
|
||||
setChartRenderer(bubbleChartRenderer);
|
||||
setBubbleChartData(BubbleChartData.generateDummyData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BubbleChartData getBubbleChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBubbleChartData(BubbleChartData data) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Setting data for BubbleChartView");
|
||||
}
|
||||
|
||||
if (null == data) {
|
||||
this.data = BubbleChartData.generateDummyData();
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
super.onChartDataChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChartData getChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callTouchListener() {
|
||||
SelectedValue selectedValue = chartRenderer.getSelectedValue();
|
||||
|
||||
if (selectedValue.isSet()) {
|
||||
BubbleValue value = data.getValues().get(selectedValue.getFirstIndex());
|
||||
onValueTouchListener.onValueSelected(selectedValue.getFirstIndex(), value);
|
||||
} else {
|
||||
onValueTouchListener.onValueDeselected();
|
||||
}
|
||||
}
|
||||
|
||||
public BubbleChartOnValueSelectListener getOnValueTouchListener() {
|
||||
return onValueTouchListener;
|
||||
}
|
||||
|
||||
public void setOnValueTouchListener(BubbleChartOnValueSelectListener touchListener) {
|
||||
if (null != touchListener) {
|
||||
this.onValueTouchListener = touchListener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes empty spaces, top-bottom for portrait orientation and left-right for landscape. This method has to be
|
||||
* called after view View#onSizeChanged() method is called and chart data is set. This method may be inaccurate.
|
||||
*
|
||||
* @see BubbleChartRenderer#removeMargins()
|
||||
*/
|
||||
public void removeMargins() {
|
||||
bubbleChartRenderer.removeMargins();
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package lecho.lib.hellocharts.view;
|
||||
|
||||
import lecho.lib.hellocharts.animation.ChartAnimationListener;
|
||||
import lecho.lib.hellocharts.computator.ChartComputator;
|
||||
import lecho.lib.hellocharts.gesture.ChartTouchHandler;
|
||||
import lecho.lib.hellocharts.gesture.ContainerScrollType;
|
||||
import lecho.lib.hellocharts.gesture.ZoomType;
|
||||
import lecho.lib.hellocharts.listener.ViewportChangeListener;
|
||||
import lecho.lib.hellocharts.model.ChartData;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.model.Viewport;
|
||||
import lecho.lib.hellocharts.renderer.AxesRenderer;
|
||||
import lecho.lib.hellocharts.renderer.ChartRenderer;
|
||||
|
||||
/**
|
||||
* Interface for all charts. Every chart must implements this interface but chart doesn't really have to extends View or
|
||||
* ViewGroup class. It can be any java class for example chart that only draw on in-memory bitmap and saves it on sd
|
||||
* card.
|
||||
*/
|
||||
public interface Chart {
|
||||
|
||||
/**
|
||||
* Returns generic chart data. For specific class call get*ChartData method from data provider implementation.
|
||||
*/
|
||||
public ChartData getChartData();
|
||||
|
||||
public ChartRenderer getChartRenderer();
|
||||
|
||||
public void setChartRenderer(ChartRenderer renderer);
|
||||
|
||||
public AxesRenderer getAxesRenderer();
|
||||
|
||||
public ChartComputator getChartComputator();
|
||||
|
||||
public ChartTouchHandler getTouchHandler();
|
||||
|
||||
/**
|
||||
* Updates chart data with given scale. Called during chart data animation update.
|
||||
*/
|
||||
public void animationDataUpdate(float scale);
|
||||
|
||||
/**
|
||||
* Called when data animation finished.
|
||||
*/
|
||||
public void animationDataFinished();
|
||||
|
||||
/**
|
||||
* Starts chart data animation for given duration. Before you call this method you should change target values of
|
||||
* chart data.
|
||||
*/
|
||||
public void startDataAnimation();
|
||||
|
||||
/**
|
||||
* Starts chart data animation for given duration. If duration is negative the default value of 500ms will be used.
|
||||
* Before you call this method you should change target values of chart data.
|
||||
*/
|
||||
public void startDataAnimation(long duration);
|
||||
|
||||
/**
|
||||
* Stops chart data animation. All chart data values are set to their target values.
|
||||
*/
|
||||
public void cancelDataAnimation();
|
||||
|
||||
/**
|
||||
* Return true if auto viewports recalculations are enabled, false otherwise.
|
||||
*/
|
||||
public boolean isViewportCalculationEnabled();
|
||||
|
||||
/**
|
||||
* Set true to enable viewports(max and current) recalculations during animations or after set*ChartData method is
|
||||
* called. If you disable viewports calculations viewports will not change until you change them manually or enable
|
||||
* calculations again. Disabled viewport calculations is usefull if you want show only part of chart by setting
|
||||
* custom viewport and don't want any operation to change that viewport
|
||||
*/
|
||||
public void setViewportCalculationEnabled(boolean isEnabled);
|
||||
|
||||
/**
|
||||
* Set listener for data animation to be notified when data animation started and finished. By default that flag is
|
||||
* set to true so be careful with animation and custom viewports.
|
||||
*/
|
||||
public void setDataAnimationListener(ChartAnimationListener animationListener);
|
||||
|
||||
/**
|
||||
* Set listener for viewport animation to be notified when viewport animation started and finished.
|
||||
*/
|
||||
public void setViewportAnimationListener(ChartAnimationListener animationListener);
|
||||
|
||||
/**
|
||||
* Set listener for current viewport changes. It will be called when viewport change either by gesture or
|
||||
* programmatically. Note! This method works only for preview charts. It is intentionally disabled for other types
|
||||
* of charts to avoid unnecessary method calls during invalidation.
|
||||
*/
|
||||
public void setViewportChangeListener(ViewportChangeListener viewportChangeListener);
|
||||
|
||||
public void callTouchListener();
|
||||
|
||||
/**
|
||||
* Returns true if chart is interactive.
|
||||
*
|
||||
* @see #setInteractive(boolean)
|
||||
*/
|
||||
public boolean isInteractive();
|
||||
|
||||
/**
|
||||
* Set true to allow user use touch gestures. If set to false user will not be able zoom, scroll or select/touch
|
||||
* value. By default true.
|
||||
*/
|
||||
public void setInteractive(boolean isInteractive);
|
||||
|
||||
/**
|
||||
* Returns true if pitch to zoom and double tap zoom is enabled.
|
||||
*
|
||||
* @see #setZoomEnabled(boolean)
|
||||
*/
|
||||
public boolean isZoomEnabled();
|
||||
|
||||
/**
|
||||
* Set true to enable zoom, false to disable, by default true;
|
||||
*/
|
||||
public void setZoomEnabled(boolean isZoomEnabled);
|
||||
|
||||
/**
|
||||
* Returns true if scrolling is enabled.
|
||||
*
|
||||
* @see #setScrollEnabled(boolean)
|
||||
*/
|
||||
public boolean isScrollEnabled();
|
||||
|
||||
/**
|
||||
* Set true to enable touch scroll/fling, false to disable touch scroll/fling, by default true;
|
||||
*/
|
||||
public void setScrollEnabled(boolean isScrollEnabled);
|
||||
|
||||
/**
|
||||
* Move/Srcoll viewport to position x,y(that position must be within maximum chart viewport). If possible viewport
|
||||
* will be centered at this point. Width and height of viewport will not be modified.
|
||||
*
|
||||
* @see #setCurrentViewport(lecho.lib.hellocharts.model.Viewport)
|
||||
*/
|
||||
public void moveTo(float x, float y);
|
||||
|
||||
/**
|
||||
* Animate viewport to position x,y(that position must be within maximum chart viewport). If possible viewport
|
||||
* will be centered at this point. Width and height of viewport will not be modified.
|
||||
*
|
||||
* @see #setCurrentViewport(lecho.lib.hellocharts.model.Viewport) ;
|
||||
*/
|
||||
public void moveToWithAnimation(float x, float y);
|
||||
|
||||
/**
|
||||
* Returns current zoom type for this chart.
|
||||
*
|
||||
* @see #setZoomType(ZoomType)
|
||||
*/
|
||||
public ZoomType getZoomType();
|
||||
|
||||
/**
|
||||
* Set zoom type, available options: ZoomType.HORIZONTAL_AND_VERTICAL, ZoomType.HORIZONTAL, ZoomType.VERTICAL. By
|
||||
* default HORIZONTAL_AND_VERTICAL.
|
||||
*/
|
||||
public void setZoomType(ZoomType zoomType);
|
||||
|
||||
/**
|
||||
* Returns current maximum zoom value.
|
||||
*/
|
||||
public float getMaxZoom();
|
||||
|
||||
/**
|
||||
* Set max zoom value. Default maximum zoom is 20.
|
||||
*/
|
||||
public void setMaxZoom(float maxZoom);
|
||||
|
||||
/**
|
||||
* Returns current zoom level.
|
||||
*/
|
||||
public float getZoomLevel();
|
||||
|
||||
/**
|
||||
* Programatically zoom chart to given point(viewport point). Call this method after chart data had been set.
|
||||
*
|
||||
* @param x x within chart maximum viewport
|
||||
* @param y y within chart maximum viewport
|
||||
* @param zoomLevel value from 1 to maxZoom(default 20). 1 means chart has no zoom.
|
||||
*/
|
||||
public void setZoomLevel(float x, float y, float zoomLevel);
|
||||
|
||||
/**
|
||||
* Programatically zoom chart to given point(viewport point) with animation. Call this method after chart data
|
||||
* had been set.
|
||||
*
|
||||
* @param x x within chart maximum viewport
|
||||
* @param y y within chart maximum viewport
|
||||
* @param zoomLevel value from 1 to maxZoom(default 20). 1 means chart has no zoom.
|
||||
*/
|
||||
public void setZoomLevelWithAnimation(float x, float y, float zoomLevel);
|
||||
|
||||
/**
|
||||
* Return true if chart value can be touched.
|
||||
*
|
||||
* @see #setValueTouchEnabled(boolean)
|
||||
*/
|
||||
public boolean isValueTouchEnabled();
|
||||
|
||||
/**
|
||||
* Set true if you want allow user to click value on chart, set false to disable that option. By default true.
|
||||
*/
|
||||
public void setValueTouchEnabled(boolean isValueTouchEnabled);
|
||||
|
||||
/**
|
||||
* Returns maximum viewport for this chart. Don't modify it directly, use {@link #setMaximumViewport(Viewport)}
|
||||
* instead.
|
||||
*
|
||||
* @see #setMaximumViewport(Viewport)
|
||||
*/
|
||||
public Viewport getMaximumViewport();
|
||||
|
||||
/**
|
||||
* Set maximum viewport. If you set bigger maximum viewport data will be more concentrate and there will be more
|
||||
* empty spaces on sides. Note. MaxViewport have to be set after chartData has been set.
|
||||
*/
|
||||
public void setMaximumViewport(Viewport maxViewport);
|
||||
|
||||
/**
|
||||
* Returns current viewport. Don't modify it directly, use {@link #setCurrentViewport(Viewport)} instead.
|
||||
*
|
||||
* @see #setCurrentViewport(Viewport)
|
||||
*/
|
||||
public Viewport getCurrentViewport();
|
||||
|
||||
/**
|
||||
* Sets current viewport. Note. viewport have to be set after chartData has been set.
|
||||
*/
|
||||
public void setCurrentViewport(Viewport targetViewport);
|
||||
|
||||
/**
|
||||
* Sets current viewport with animation. Note. viewport have to be set after chartData has been set.
|
||||
*/
|
||||
public void setCurrentViewportWithAnimation(Viewport targetViewport);
|
||||
|
||||
/**
|
||||
* Sets current viewport with animation. Note. viewport have to be set after chartData has been set.
|
||||
*/
|
||||
public void setCurrentViewportWithAnimation(Viewport targetViewport, long duration);
|
||||
|
||||
/**
|
||||
* Reset maximum viewport and current viewport. Values for both viewports will be auto-calculated using current
|
||||
* chart data ranges.
|
||||
*/
|
||||
public void resetViewports();
|
||||
|
||||
/**
|
||||
* Return true if value selection mode is enabled.
|
||||
*
|
||||
* @see #setValueSelectionEnabled(boolean)
|
||||
*/
|
||||
public boolean isValueSelectionEnabled();
|
||||
|
||||
/**
|
||||
* Set true if you want value selection with touch - value will stay selected until you touch somewhere else on the
|
||||
* chart area. By default false and value is automatically unselected when user stop pressing on it.
|
||||
*/
|
||||
public void setValueSelectionEnabled(boolean isValueSelectionEnabled);
|
||||
|
||||
/**
|
||||
* Select single value on chart. If indexes are not valid IndexOutOfBoundsException will be thrown.
|
||||
*/
|
||||
public void selectValue(SelectedValue selectedValue);
|
||||
|
||||
/**
|
||||
* Return currently selected value indexes.
|
||||
*/
|
||||
public SelectedValue getSelectedValue();
|
||||
|
||||
/**
|
||||
* @see #setContainerScrollEnabled(boolean, ContainerScrollType)
|
||||
*/
|
||||
public boolean isContainerScrollEnabled();
|
||||
|
||||
/**
|
||||
* Set isContainerScrollEnabled to true and containerScrollType to HORIZONTAL or VERTICAL if you are using chart
|
||||
* within scroll container.
|
||||
*/
|
||||
public void setContainerScrollEnabled(boolean isContainerScrollEnabled, ContainerScrollType containerScrollType);
|
||||
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package lecho.lib.hellocharts.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import lecho.lib.hellocharts.BuildConfig;
|
||||
import lecho.lib.hellocharts.listener.ColumnChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.listener.DummyColumnChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.model.ColumnChartData;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
|
||||
import lecho.lib.hellocharts.renderer.ColumnChartRenderer;
|
||||
|
||||
/**
|
||||
* ColumnChart/BarChart, supports subcolumns, stacked collumns and negative values.
|
||||
*
|
||||
* @author Leszek Wach
|
||||
*/
|
||||
public class ColumnChartView extends AbstractChartView implements ColumnChartDataProvider {
|
||||
private static final String TAG = "ColumnChartView";
|
||||
private ColumnChartData data;
|
||||
private ColumnChartOnValueSelectListener onValueTouchListener = new DummyColumnChartOnValueSelectListener();
|
||||
|
||||
public ColumnChartView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public ColumnChartView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ColumnChartView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
setChartRenderer(new ColumnChartRenderer(context, this, this));
|
||||
setColumnChartData(ColumnChartData.generateDummyData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnChartData getColumnChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColumnChartData(ColumnChartData data) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Setting data for ColumnChartView");
|
||||
}
|
||||
|
||||
if (null == data) {
|
||||
this.data = ColumnChartData.generateDummyData();
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
super.onChartDataChange();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnChartData getChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callTouchListener() {
|
||||
SelectedValue selectedValue = chartRenderer.getSelectedValue();
|
||||
|
||||
if (selectedValue.isSet()) {
|
||||
SubcolumnValue value = data.getColumns().get(selectedValue.getFirstIndex()).getValues()
|
||||
.get(selectedValue.getSecondIndex());
|
||||
onValueTouchListener.onValueSelected(selectedValue.getFirstIndex(), selectedValue.getSecondIndex(), value);
|
||||
} else {
|
||||
onValueTouchListener.onValueDeselected();
|
||||
}
|
||||
}
|
||||
|
||||
public ColumnChartOnValueSelectListener getOnValueTouchListener() {
|
||||
return onValueTouchListener;
|
||||
}
|
||||
|
||||
public void setOnValueTouchListener(ColumnChartOnValueSelectListener touchListener) {
|
||||
if (null != touchListener) {
|
||||
this.onValueTouchListener = touchListener;
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package lecho.lib.hellocharts.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import lecho.lib.hellocharts.BuildConfig;
|
||||
import lecho.lib.hellocharts.listener.ComboLineColumnChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.listener.DummyCompoLineColumnChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.model.ChartData;
|
||||
import lecho.lib.hellocharts.model.ColumnChartData;
|
||||
import lecho.lib.hellocharts.model.ComboLineColumnChartData;
|
||||
import lecho.lib.hellocharts.model.LineChartData;
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
|
||||
import lecho.lib.hellocharts.model.SubcolumnValue;
|
||||
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
|
||||
import lecho.lib.hellocharts.provider.ComboLineColumnChartDataProvider;
|
||||
import lecho.lib.hellocharts.provider.LineChartDataProvider;
|
||||
import lecho.lib.hellocharts.renderer.ColumnChartRenderer;
|
||||
import lecho.lib.hellocharts.renderer.ComboLineColumnChartRenderer;
|
||||
import lecho.lib.hellocharts.renderer.LineChartRenderer;
|
||||
|
||||
/**
|
||||
* ComboChart, supports ColumnChart combined with LineChart. Lines are always drawn on top.
|
||||
*
|
||||
* @author Leszek Wach
|
||||
*/
|
||||
public class ComboLineColumnChartView extends AbstractChartView implements ComboLineColumnChartDataProvider {
|
||||
private static final String TAG = "ComboLineColumnChartView";
|
||||
protected ComboLineColumnChartData data;
|
||||
protected ColumnChartDataProvider columnChartDataProvider = new ComboColumnChartDataProvider();
|
||||
protected LineChartDataProvider lineChartDataProvider = new ComboLineChartDataProvider();
|
||||
protected ComboLineColumnChartOnValueSelectListener onValueTouchListener = new
|
||||
DummyCompoLineColumnChartOnValueSelectListener();
|
||||
|
||||
public ComboLineColumnChartView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public ComboLineColumnChartView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ComboLineColumnChartView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
setChartRenderer(new ComboLineColumnChartRenderer(context, this, columnChartDataProvider,
|
||||
lineChartDataProvider));
|
||||
setComboLineColumnChartData(ComboLineColumnChartData.generateDummyData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComboLineColumnChartData getComboLineColumnChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setComboLineColumnChartData(ComboLineColumnChartData data) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Setting data for ComboLineColumnChartView");
|
||||
}
|
||||
|
||||
if (null == data) {
|
||||
this.data = null;// generateDummyData();
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
super.onChartDataChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChartData getChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callTouchListener() {
|
||||
SelectedValue selectedValue = chartRenderer.getSelectedValue();
|
||||
|
||||
if (selectedValue.isSet()) {
|
||||
|
||||
if (SelectedValueType.COLUMN.equals(selectedValue.getType())) {
|
||||
|
||||
SubcolumnValue value = data.getColumnChartData().getColumns().get(selectedValue.getFirstIndex())
|
||||
.getValues().get(selectedValue.getSecondIndex());
|
||||
onValueTouchListener.onColumnValueSelected(selectedValue.getFirstIndex(),
|
||||
selectedValue.getSecondIndex(), value);
|
||||
|
||||
} else if (SelectedValueType.LINE.equals(selectedValue.getType())) {
|
||||
|
||||
PointValue value = data.getLineChartData().getLines().get(selectedValue.getFirstIndex()).getValues()
|
||||
.get(selectedValue.getSecondIndex());
|
||||
onValueTouchListener.onPointValueSelected(selectedValue.getFirstIndex(), selectedValue.getSecondIndex(),
|
||||
value);
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid selected value type " + selectedValue.getType().name());
|
||||
}
|
||||
} else {
|
||||
onValueTouchListener.onValueDeselected();
|
||||
}
|
||||
}
|
||||
|
||||
public ComboLineColumnChartOnValueSelectListener getOnValueTouchListener() {
|
||||
return onValueTouchListener;
|
||||
}
|
||||
|
||||
public void setOnValueTouchListener(ComboLineColumnChartOnValueSelectListener touchListener) {
|
||||
if (null != touchListener) {
|
||||
this.onValueTouchListener = touchListener;
|
||||
}
|
||||
}
|
||||
|
||||
public void setColumnChartRenderer(Context context, ColumnChartRenderer columnChartRenderer){
|
||||
setChartRenderer(new ComboLineColumnChartRenderer(context, this , columnChartRenderer, lineChartDataProvider));
|
||||
}
|
||||
|
||||
public void setLineChartRenderer(Context context, LineChartRenderer lineChartRenderer){
|
||||
setChartRenderer(new ComboLineColumnChartRenderer(context, this, columnChartDataProvider, lineChartRenderer));
|
||||
}
|
||||
|
||||
private class ComboLineChartDataProvider implements LineChartDataProvider {
|
||||
|
||||
@Override
|
||||
public LineChartData getLineChartData() {
|
||||
return ComboLineColumnChartView.this.data.getLineChartData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLineChartData(LineChartData data) {
|
||||
ComboLineColumnChartView.this.data.setLineChartData(data);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ComboColumnChartDataProvider implements ColumnChartDataProvider {
|
||||
|
||||
@Override
|
||||
public ColumnChartData getColumnChartData() {
|
||||
return ComboLineColumnChartView.this.data.getColumnChartData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColumnChartData(ColumnChartData data) {
|
||||
ComboLineColumnChartView.this.data.setColumnChartData(data);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package lecho.lib.hellocharts.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import lecho.lib.hellocharts.BuildConfig;
|
||||
import lecho.lib.hellocharts.listener.DummyLineChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.listener.LineChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.model.ChartData;
|
||||
import lecho.lib.hellocharts.model.LineChartData;
|
||||
import lecho.lib.hellocharts.model.PointValue;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.provider.LineChartDataProvider;
|
||||
import lecho.lib.hellocharts.renderer.LineChartRenderer;
|
||||
|
||||
/**
|
||||
* LineChart, supports cubic lines, filled lines, circle and square points. Point radius and stroke width can be
|
||||
* adjusted using LineChartData attributes.
|
||||
*
|
||||
* @author Leszek Wach
|
||||
*/
|
||||
public class LineChartView extends AbstractChartView implements LineChartDataProvider {
|
||||
private static final String TAG = "LineChartView";
|
||||
protected LineChartData data;
|
||||
protected LineChartOnValueSelectListener onValueTouchListener = new DummyLineChartOnValueSelectListener();
|
||||
|
||||
public LineChartView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public LineChartView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public LineChartView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
setChartRenderer(new LineChartRenderer(context, this, this));
|
||||
setLineChartData(LineChartData.generateDummyData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LineChartData getLineChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLineChartData(LineChartData data) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Setting data for LineChartView");
|
||||
}
|
||||
|
||||
if (null == data) {
|
||||
this.data = LineChartData.generateDummyData();
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
super.onChartDataChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChartData getChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callTouchListener() {
|
||||
SelectedValue selectedValue = chartRenderer.getSelectedValue();
|
||||
|
||||
if (selectedValue.isSet()) {
|
||||
PointValue point = data.getLines().get(selectedValue.getFirstIndex()).getValues()
|
||||
.get(selectedValue.getSecondIndex());
|
||||
onValueTouchListener.onValueSelected(selectedValue.getFirstIndex(), selectedValue.getSecondIndex(), point);
|
||||
} else {
|
||||
onValueTouchListener.onValueDeselected();
|
||||
}
|
||||
}
|
||||
|
||||
public LineChartOnValueSelectListener getOnValueTouchListener() {
|
||||
return onValueTouchListener;
|
||||
}
|
||||
|
||||
public void setOnValueTouchListener(LineChartOnValueSelectListener touchListener) {
|
||||
if (null != touchListener) {
|
||||
this.onValueTouchListener = touchListener;
|
||||
}
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package lecho.lib.hellocharts.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import lecho.lib.hellocharts.BuildConfig;
|
||||
import lecho.lib.hellocharts.animation.PieChartRotationAnimator;
|
||||
import lecho.lib.hellocharts.animation.PieChartRotationAnimatorV14;
|
||||
import lecho.lib.hellocharts.animation.PieChartRotationAnimatorV8;
|
||||
import lecho.lib.hellocharts.gesture.PieChartTouchHandler;
|
||||
import lecho.lib.hellocharts.listener.DummyPieChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.listener.PieChartOnValueSelectListener;
|
||||
import lecho.lib.hellocharts.model.ChartData;
|
||||
import lecho.lib.hellocharts.model.PieChartData;
|
||||
import lecho.lib.hellocharts.model.SelectedValue;
|
||||
import lecho.lib.hellocharts.model.SliceValue;
|
||||
import lecho.lib.hellocharts.provider.PieChartDataProvider;
|
||||
import lecho.lib.hellocharts.renderer.PieChartRenderer;
|
||||
|
||||
/**
|
||||
* PieChart is a little different than others charts. It doesn't have axes. It doesn't support viewport so changing
|
||||
* viewport wont work. Instead it support "Circle Oval". Pinch-to-Zoom and double tap zoom wont work either. Instead of
|
||||
* scroll there is chart rotation if isChartRotationEnabled is set to true. PieChart looks the best when it has the same
|
||||
* width and height, drawing chart on rectangle with proportions other than 1:1 will left some empty spaces.
|
||||
*
|
||||
* @author Leszek Wach
|
||||
*/
|
||||
public class PieChartView extends AbstractChartView implements PieChartDataProvider {
|
||||
private static final String TAG = "PieChartView";
|
||||
protected PieChartData data;
|
||||
protected PieChartOnValueSelectListener onValueTouchListener = new DummyPieChartOnValueSelectListener();
|
||||
protected PieChartRenderer pieChartRenderer;
|
||||
protected PieChartRotationAnimator rotationAnimator;
|
||||
|
||||
public PieChartView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public PieChartView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public PieChartView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
pieChartRenderer = new PieChartRenderer(context, this, this);
|
||||
touchHandler = new PieChartTouchHandler(context, this);
|
||||
setChartRenderer(pieChartRenderer);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
this.rotationAnimator = new PieChartRotationAnimatorV8(this);
|
||||
} else {
|
||||
this.rotationAnimator = new PieChartRotationAnimatorV14(this);
|
||||
}
|
||||
setPieChartData(PieChartData.generateDummyData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PieChartData getPieChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPieChartData(PieChartData data) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Setting data for ColumnChartView");
|
||||
}
|
||||
|
||||
if (null == data) {
|
||||
this.data = PieChartData.generateDummyData();
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
super.onChartDataChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChartData getChartData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callTouchListener() {
|
||||
SelectedValue selectedValue = chartRenderer.getSelectedValue();
|
||||
|
||||
if (selectedValue.isSet()) {
|
||||
SliceValue sliceValue = data.getValues().get(selectedValue.getFirstIndex());
|
||||
onValueTouchListener.onValueSelected(selectedValue.getFirstIndex(), sliceValue);
|
||||
} else {
|
||||
onValueTouchListener.onValueDeselected();
|
||||
}
|
||||
}
|
||||
|
||||
public PieChartOnValueSelectListener getOnValueTouchListener() {
|
||||
return onValueTouchListener;
|
||||
}
|
||||
|
||||
public void setOnValueTouchListener(PieChartOnValueSelectListener touchListener) {
|
||||
if (null != touchListener) {
|
||||
this.onValueTouchListener = touchListener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rectangle that will constraint pie chart area.
|
||||
*/
|
||||
public RectF getCircleOval() {
|
||||
return pieChartRenderer.getCircleOval();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to change pie chart area. Because by default CircleOval is calculated onSizeChanged() you must call this
|
||||
* method after size of PieChartView is calculated. In most cases it will probably be easier to use
|
||||
* {@link #setCircleFillRatio(float)} to change chart area or just use view padding.
|
||||
*/
|
||||
public void setCircleOval(RectF orginCircleOval) {
|
||||
pieChartRenderer.setCircleOval(orginCircleOval);
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns pie chart rotation, 0 rotation means that 0 degrees is at 3 o'clock. Don't confuse with
|
||||
* {@link View#getRotation()}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getChartRotation() {
|
||||
return pieChartRenderer.getChartRotation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set pie chart rotation. Don't confuse with {@link View#getRotation()}.
|
||||
*
|
||||
* @param rotation
|
||||
* @see #getChartRotation()
|
||||
*/
|
||||
public void setChartRotation(int rotation, boolean isAnimated) {
|
||||
if (isAnimated) {
|
||||
rotationAnimator.cancelAnimation();
|
||||
rotationAnimator.startAnimation(pieChartRenderer.getChartRotation(), rotation);
|
||||
} else {
|
||||
pieChartRenderer.setChartRotation(rotation);
|
||||
}
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
|
||||
public boolean isChartRotationEnabled() {
|
||||
if (touchHandler instanceof PieChartTouchHandler) {
|
||||
return ((PieChartTouchHandler) touchHandler).isRotationEnabled();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set false if you don't wont the chart to be rotated by touch gesture. Rotating programmatically will still work.
|
||||
*
|
||||
* @param isRotationEnabled
|
||||
*/
|
||||
public void setChartRotationEnabled(boolean isRotationEnabled) {
|
||||
if (touchHandler instanceof PieChartTouchHandler) {
|
||||
((PieChartTouchHandler) touchHandler).setRotationEnabled(isRotationEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns SliceValue that is under given angle, selectedValue (if not null) will be hold slice index.
|
||||
*/
|
||||
public SliceValue getValueForAngle(int angle, SelectedValue selectedValue) {
|
||||
return pieChartRenderer.getValueForAngle(angle, selectedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setCircleFillRatio(float)
|
||||
*/
|
||||
public float getCircleFillRatio() {
|
||||
return pieChartRenderer.getCircleFillRatio();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set how much of view area should be taken by chart circle. Value should be between 0 and 1. Default is 1 so
|
||||
* circle will have radius equals min(View.width, View.height).
|
||||
*/
|
||||
public void setCircleFillRatio(float fillRatio) {
|
||||
pieChartRenderer.setCircleFillRatio(fillRatio);
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user