的
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'com.dicedmelon.gradle.jacoco-android'
|
||||
id 'maven-publish'
|
||||
id 'signing'
|
||||
}
|
||||
|
||||
class AttrMarkdown extends DefaultTask {
|
||||
|
||||
def inFile
|
||||
def outFile
|
||||
|
||||
@TaskAction
|
||||
def generate() {
|
||||
def input = project.file(inFile)
|
||||
|
||||
def output = project.file(outFile)
|
||||
if(output.exists()) {
|
||||
output.delete()
|
||||
}
|
||||
output.parentFile.mkdirs()
|
||||
|
||||
input.text.findAll(/<!--\n?([\s\S]*?)\n?-->/) { match, g1 -> g1
|
||||
if(!g1.startsWith("NODOC")) {
|
||||
output.append(g1)
|
||||
output.append "\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates xml attrs markdown docs. To run:
|
||||
* at the command line from the project root dir type:
|
||||
* ./gradlew generateAttrsMarkdown
|
||||
*
|
||||
* The generated doc will appear in / replace androidplot/docs/attrs.md
|
||||
*/
|
||||
task generateAttrsMarkdown(type: AttrMarkdown) {
|
||||
inFile = { "src/main/res/values/attrs.xml"}
|
||||
outFile = { "../docs/attrs.md" }
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion theCompileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion theMinSdkVersion
|
||||
targetSdkVersion theTargetSdkVersion
|
||||
testApplicationId "com.androidplot.test"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
jacoco {
|
||||
includeNoLocationClasses = true
|
||||
jacoco.excludes = ['jdk.internal.*']
|
||||
}
|
||||
}
|
||||
}
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
group = 'com.androidplot'
|
||||
version = theVersionName
|
||||
|
||||
def siteUrl = 'http://androidplot.com'
|
||||
def gitUrl = 'https://github.com/halfhp/androidplot.git'
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'com.halfhp.fig:figlib:1.0.11'
|
||||
implementation 'com.android.support:support-annotations:28.0.0'
|
||||
|
||||
testImplementation "org.mockito:mockito-core:4.0.0"
|
||||
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
|
||||
testImplementation "org.robolectric:robolectric:4.7.3"
|
||||
}
|
||||
|
||||
task javadoc(type: Javadoc) {
|
||||
source = android.sourceSets.main.java.srcDirs
|
||||
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
|
||||
failOnError false
|
||||
options {
|
||||
links "http://docs.oracle.com/javase/7/docs/api/"
|
||||
linksOffline "http://d.android.com/reference","${android.sdkDirectory}/docs/reference"
|
||||
}
|
||||
exclude '**/BuildConfig.java'
|
||||
exclude '**/R.java'
|
||||
}
|
||||
|
||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||
classifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar) {
|
||||
classifier = 'sources'
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
}
|
||||
|
||||
javadoc {
|
||||
options.overview = "src/main/java/overview.html"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "Maven Central"
|
||||
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
|
||||
credentials {
|
||||
username = System.getenv("OSSRH_ACTOR")
|
||||
password = System.getenv("OSSRH_TOKEN")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publications {
|
||||
release(MavenPublication) {
|
||||
|
||||
from components.release
|
||||
|
||||
// You can then customize attributes of the publication as shown below.
|
||||
groupId = 'com.androidplot'
|
||||
artifactId = 'androidplot-core'
|
||||
version = theVersionName
|
||||
|
||||
pom {
|
||||
packaging 'aar'
|
||||
name = 'Androidplot'
|
||||
description = "Configure any object from XML."
|
||||
url = gitUrl
|
||||
licenses {
|
||||
license {
|
||||
name = 'The Apache Software License, Version 2.0'
|
||||
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = 'halfhp'
|
||||
name = 'Nick Fellows'
|
||||
email = 'halfhp@gmail.com'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = gitUrl
|
||||
developerConnection = gitUrl
|
||||
url = gitUrl
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
signing {
|
||||
def signingKey = System.getenv("SIGNING_KEY")
|
||||
def signingPassword = System.getenv("SIGNING_PASSWORD")
|
||||
useInMemoryPgpKeys(signingKey, signingPassword)
|
||||
sign publishing.publications.release
|
||||
}
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives javadocJar
|
||||
archives sourcesJar
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
~ Copyright 2015 AndroidPlot.com
|
||||
~
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<!-- This file has been intentionally left empty; values should be
|
||||
set in android.defaultConfig in build.gradle wherever possible.-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.androidplot">
|
||||
</manifest>
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot;
|
||||
|
||||
import android.graphics.Paint;
|
||||
|
||||
public interface LineLabelFormatter {
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value The value being rendered by this formatter.
|
||||
* @return Paint instance that should be used to render the specified value.
|
||||
*/
|
||||
Paint getPaint(Number value);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
|
||||
/**
|
||||
* Defines methods used for monitoring events generated by a Plot.
|
||||
*/
|
||||
public interface PlotListener {
|
||||
|
||||
/**
|
||||
* Fired immediately before the Plot "source" is drawn onto canvas.
|
||||
* Commonly used by implementing Series instances to activate a read
|
||||
* lock on it's self in preparation for the Plot's imminent reading
|
||||
* of that series.
|
||||
* @param source
|
||||
* @param canvas
|
||||
*/
|
||||
void onBeforeDraw(Plot source, Canvas canvas);
|
||||
|
||||
/**
|
||||
* Fired immediately after the Plot "source" is drawn onto canvas.
|
||||
* Just as onBeforeDraw(...) is commonly used by Series implementations
|
||||
* to activate a read lock, this method is commonly used to release that
|
||||
* same lock.
|
||||
* @param source
|
||||
* @param canvas
|
||||
*/
|
||||
void onAfterDraw(Plot source, Canvas canvas);
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.androidplot;
|
||||
|
||||
import com.androidplot.util.FastNumber;
|
||||
|
||||
/**
|
||||
* A one dimensional region represented by a starting and ending value.
|
||||
*/
|
||||
public class Region {
|
||||
private FastNumber min;
|
||||
private FastNumber max;
|
||||
private FastNumber cachedLength;
|
||||
|
||||
private Region defaults = this;
|
||||
|
||||
public Region() {}
|
||||
|
||||
public static Region withDefaults(Region defaults) {
|
||||
if(defaults == null || !defaults.isDefined()) {
|
||||
throw new IllegalArgumentException("When specifying default min and max must both be non-null values");
|
||||
}
|
||||
Region r = new Region();
|
||||
r.defaults = defaults;
|
||||
return r;
|
||||
}
|
||||
|
||||
public Region(Number v1, Number v2) {
|
||||
if (v1 != null && v2 != null && v1.doubleValue() < v2.doubleValue()) {
|
||||
this.setMin(v1);
|
||||
this.setMax(v2);
|
||||
} else {
|
||||
this.setMin(v2);
|
||||
this.setMax(v1);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMinMax(Region region) {
|
||||
setMin(region.getMin());
|
||||
setMax(region.getMax());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param v1
|
||||
* @param v2
|
||||
* @return The distance between val1 and val2 or null if either parameters are null.
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public static Number measure(Number v1, Number v2) {
|
||||
return new Region(v1, v2).length();
|
||||
}
|
||||
|
||||
public Number length() {
|
||||
if(cachedLength == null) {
|
||||
Number l = getMax() == null || getMin() == null ?
|
||||
null : getMax().doubleValue() - getMin().doubleValue();
|
||||
if(l != null) {
|
||||
cachedLength = FastNumber.orNull(l);
|
||||
}
|
||||
}
|
||||
return cachedLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether a value is within the given range
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public boolean contains(Number value) {
|
||||
return value.doubleValue() >= getMin().doubleValue() && value.doubleValue() <= getMax().doubleValue();
|
||||
}
|
||||
|
||||
public boolean intersects(Region region) {
|
||||
return intersects(region.getMin(), region.getMax());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return Middle value within this region
|
||||
*/
|
||||
public Number center() {
|
||||
return getMax().doubleValue() - (length().doubleValue() / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a value relative to this region into it's corresponding value relative to the
|
||||
* specified region.
|
||||
* @param value
|
||||
* @param region2
|
||||
* @return
|
||||
*/
|
||||
public Number transform(double value, Region region2) {
|
||||
return transform(value, region2, false);
|
||||
}
|
||||
|
||||
public Number transform(double value, Region region2, boolean flip) {
|
||||
return transform(value, region2.getMin().doubleValue(), region2.getMax().doubleValue(), flip);
|
||||
}
|
||||
|
||||
public double transform(double value, double min, double max, boolean flip) {
|
||||
double range = length().doubleValue();
|
||||
final double r2 = max - min;
|
||||
|
||||
// TODO: refactor to use ratio here
|
||||
final double scale = r2 / range;
|
||||
if(!flip) {
|
||||
return min + (scale * (value - this.getMin().doubleValue()));
|
||||
} else {
|
||||
return max - (scale * (value - this.getMin().doubleValue()));
|
||||
}
|
||||
}
|
||||
|
||||
public Number ratio(Region r2) {
|
||||
return ratio(r2.getMin().doubleValue(), r2.getMax().doubleValue());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param min
|
||||
* @param max
|
||||
* @return length of this series divided by the length of the distance between min and max.
|
||||
*/
|
||||
public double ratio(double min, double max) {
|
||||
return length().doubleValue() / (max - min);
|
||||
}
|
||||
|
||||
|
||||
public void union(Number value) {
|
||||
if(value == null) {
|
||||
return;
|
||||
}
|
||||
double val = value.doubleValue();
|
||||
if(getMin() == null ||
|
||||
val < getMin().doubleValue()) {
|
||||
setMin(value);
|
||||
}
|
||||
if(getMax() == null || val >
|
||||
getMax().doubleValue()) {
|
||||
setMax(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the input bounds min/max against this instance's current min/max.
|
||||
* If the input.min is less than this.min then this.min will be set to input.min.
|
||||
* If the input.max is greater than this.max then this.max will be set to input.max
|
||||
*
|
||||
* The result of a union will always be an equal or larger size region.
|
||||
* @param input
|
||||
*/
|
||||
public void union(Region input) {
|
||||
union(input.getMin());
|
||||
union(input.getMax());
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of an intersect will always be an equal or smaller size region.
|
||||
* @param input
|
||||
*/
|
||||
public void intersect(Region input) {
|
||||
if(getMin().doubleValue() < input.getMin().doubleValue()) {
|
||||
setMin(input.getMin());
|
||||
}
|
||||
|
||||
if(getMax().doubleValue() > input.getMax().doubleValue()) {
|
||||
setMax(input.getMax());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether this segment intersects another
|
||||
* @param line2Min
|
||||
* @param line2Max
|
||||
* @return
|
||||
*/
|
||||
public boolean intersects(Number line2Min, Number line2Max) {
|
||||
|
||||
// is this line completely within line2?
|
||||
if(line2Min.doubleValue() <= getMin().doubleValue() && line2Max.doubleValue() >= getMax().doubleValue()) {
|
||||
return true;
|
||||
// is line1 partially within line2
|
||||
} else return contains(line2Min) || contains(line2Max);
|
||||
}
|
||||
|
||||
public boolean isMinSet() {
|
||||
return min != null;
|
||||
}
|
||||
|
||||
public Number getMin() {
|
||||
return isMinSet() ? min : defaults.min;
|
||||
}
|
||||
|
||||
public void setMin(Number min) {
|
||||
cachedLength = null;
|
||||
if(min == null) {
|
||||
if(defaults == null) {
|
||||
throw new NullPointerException(
|
||||
"Region values cannot be null unless defaults have been set.");
|
||||
} else {
|
||||
this.min = null;
|
||||
}
|
||||
} else if (this.min == null || !this.min.equals(min)) {
|
||||
this.min = FastNumber.orNull(min);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMaxSet() {
|
||||
return max != null;
|
||||
}
|
||||
|
||||
public Number getMax() {
|
||||
return isMaxSet() ? max : defaults.max;
|
||||
}
|
||||
|
||||
public void setMax(Number max) {
|
||||
cachedLength = null;
|
||||
if(max == null) {
|
||||
if(defaults == null) {
|
||||
throw new NullPointerException(
|
||||
"Region values can never be null unless defaults have been set.");
|
||||
} else {
|
||||
this.max = null;
|
||||
}
|
||||
} else if (this.max == null || !this.max.equals(max)) {
|
||||
this.max = FastNumber.orNull(max);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return True if both min and max values are non-null, false otherwise. Does *not* consider defaults.
|
||||
*/
|
||||
public boolean isDefined() {
|
||||
return min != null && max != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("Region{");
|
||||
sb.append("min=").append(min);
|
||||
sb.append(", max=").append(max);
|
||||
sb.append(", cachedLength=").append(cachedLength);
|
||||
sb.append(", defaults=");
|
||||
if (defaults != this) {
|
||||
sb.append(defaults);
|
||||
} else {
|
||||
sb.append("this");
|
||||
}
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot;
|
||||
|
||||
/**
|
||||
* Base interface for all Series implementations
|
||||
*/
|
||||
public interface Series {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The title of this Series.
|
||||
*/
|
||||
String getTitle();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot;
|
||||
|
||||
import com.androidplot.ui.Formatter;
|
||||
import com.androidplot.ui.SeriesBundle;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Manages a list of {@link Series} and their associated {@link Formatter} in the context of a {@link Plot}.
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public abstract class SeriesRegistry
|
||||
<BundleType extends SeriesBundle<SeriesType, FormatterType>,
|
||||
SeriesType extends Series, FormatterType extends Formatter> implements Serializable {
|
||||
|
||||
private ArrayList<BundleType> registry = new ArrayList<>();
|
||||
|
||||
public List<BundleType> getSeriesAndFormatterList() {
|
||||
return registry;
|
||||
}
|
||||
public List<SeriesType> getSeriesList() {
|
||||
List<SeriesType> result = new ArrayList<>(registry.size());
|
||||
for(SeriesBundle<SeriesType, FormatterType> sfPair : registry) {
|
||||
result.add(sfPair.getSeries());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return registry.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return registry.isEmpty();
|
||||
}
|
||||
|
||||
public boolean add(SeriesType series, FormatterType formatter) {
|
||||
if(series == null || formatter == null) {
|
||||
throw new IllegalArgumentException("Neither series nor formatter param may be null.");
|
||||
}
|
||||
return registry.add(newSeriesBundle(series, formatter));
|
||||
}
|
||||
|
||||
protected abstract BundleType newSeriesBundle(SeriesType series, FormatterType formatter);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param series
|
||||
* @return A List of {@link SeriesBundle} instances that reference series.
|
||||
*/
|
||||
protected List<SeriesBundle<SeriesType, FormatterType>> get(SeriesType series) {
|
||||
List<SeriesBundle<SeriesType, FormatterType>> results =
|
||||
new ArrayList<>();
|
||||
for(SeriesBundle<SeriesType, FormatterType> thisPair : registry) {
|
||||
if(thisPair.getSeries() == series) {
|
||||
results.add(thisPair);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public synchronized List<BundleType> remove(SeriesType series, Class rendererClass) {
|
||||
ArrayList<BundleType> removedItems = new ArrayList<>();
|
||||
for(Iterator<BundleType> it = registry.iterator(); it.hasNext();) {
|
||||
BundleType b = it.next();
|
||||
if(b.getSeries() == series && b.getFormatter().getRendererClass() == rendererClass) {
|
||||
it.remove();
|
||||
removedItems.add(b);
|
||||
}
|
||||
}
|
||||
return removedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all occurrences of series regardless of the associated Renderer.
|
||||
* @param series
|
||||
*/
|
||||
public synchronized boolean remove(SeriesType series) {
|
||||
boolean result = false;
|
||||
for(Iterator<BundleType> it = registry.iterator(); it.hasNext();) {
|
||||
if(it.next().getSeries() == series) {
|
||||
it.remove();
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all series from the plot.
|
||||
*/
|
||||
public void clear() {
|
||||
for(Iterator<BundleType> it
|
||||
= registry.iterator(); it.hasNext();) {
|
||||
it.next();
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public List<SeriesBundle<SeriesType, FormatterType>> getLegendEnabledItems() {
|
||||
List<SeriesBundle<SeriesType, FormatterType>> sfList = new ArrayList<>();
|
||||
for(SeriesBundle<SeriesType, FormatterType> sf : registry) {
|
||||
if(sf.getFormatter().isLegendIconEnabled()) {
|
||||
sfList.add(sf);
|
||||
}
|
||||
}
|
||||
return sfList;
|
||||
}
|
||||
|
||||
public boolean contains(SeriesType series, Class<? extends FormatterType> formatterClass) {
|
||||
for(BundleType b : registry) {
|
||||
if(b.getFormatter().getClass() == formatterClass && b.getSeries() == series) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
/**
|
||||
* A basic implementation of a {@link LineLabelFormatter}.
|
||||
*/
|
||||
public class SimpleLineLabelFormatter implements LineLabelFormatter {
|
||||
|
||||
private static final int DEFAULT_TEXT_SIZE_SP = 12;
|
||||
private static final int DEFAULT_STROKE_SIZE_DP = 2;
|
||||
private Paint paint;
|
||||
|
||||
public SimpleLineLabelFormatter() {
|
||||
this(new Paint());
|
||||
getPaint().setColor(Color.WHITE);
|
||||
getPaint().setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_SP));
|
||||
getPaint().setStrokeWidth(PixelUtils.dpToPix(DEFAULT_STROKE_SIZE_DP));
|
||||
}
|
||||
|
||||
public SimpleLineLabelFormatter(int color) {
|
||||
this();
|
||||
getPaint().setColor(color);
|
||||
}
|
||||
|
||||
public SimpleLineLabelFormatter(Paint paint) {
|
||||
this.paint = paint;
|
||||
}
|
||||
|
||||
public Paint getPaint() {
|
||||
return paint;
|
||||
}
|
||||
|
||||
public void setPaint(Paint paint) {
|
||||
this.paint = paint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Paint getPaint(Number value) {
|
||||
return getPaint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.pie;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.androidplot.*;
|
||||
import com.androidplot.ui.*;
|
||||
import com.androidplot.util.AttrUtils;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
/**
|
||||
* Basic representation of a Pie Chart that displays a title and pie widget.
|
||||
*/
|
||||
public class PieChart extends Plot<Segment, SegmentFormatter, PieRenderer,
|
||||
SegmentBundle, SegmentRegistry> {
|
||||
|
||||
private static final int DEFAULT_PIE_WIDGET_H_DP = 18;
|
||||
private static final int DEFAULT_PIE_WIDGET_W_DP = 10;
|
||||
|
||||
private static final int DEFAULT_PIE_WIDGET_Y_OFFSET_DP = 0;
|
||||
private static final int DEFAULT_PIE_WIDGET_X_OFFSET_DP = 0;
|
||||
|
||||
private static final int DEFAULT_LEGEND_WIDGET_H_DP = 30;
|
||||
private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP = 18;
|
||||
private static final int DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP = 0;
|
||||
private static final int DEFAULT_LEGEND_WIDGET_X_OFFSET_DP = 40;
|
||||
|
||||
private static final int DEFAULT_PADDING_DP = 5;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private PieWidget pie;
|
||||
|
||||
private PieLegendWidget legend;
|
||||
|
||||
@Override
|
||||
protected SegmentRegistry getRegistryInstance() {
|
||||
return new SegmentRegistry();
|
||||
}
|
||||
|
||||
public PieChart(Context context, String title) {
|
||||
super(context, title);
|
||||
}
|
||||
|
||||
public PieChart(Context context, String title, RenderMode mode) {
|
||||
super(context, title, mode);
|
||||
}
|
||||
|
||||
public PieChart(Context context, AttributeSet attributes) {
|
||||
super(context, attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreInit() {
|
||||
pie = new PieWidget(
|
||||
getLayoutManager(),
|
||||
this,
|
||||
new Size(
|
||||
PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_H_DP),
|
||||
SizeMode.FILL,
|
||||
PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_W_DP),
|
||||
SizeMode.FILL));
|
||||
|
||||
pie.position(
|
||||
PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_X_OFFSET_DP),
|
||||
HorizontalPositioning.ABSOLUTE_FROM_CENTER,
|
||||
PixelUtils.dpToPix(DEFAULT_PIE_WIDGET_Y_OFFSET_DP),
|
||||
VerticalPositioning.ABSOLUTE_FROM_CENTER,
|
||||
Anchor.CENTER);
|
||||
|
||||
legend = new PieLegendWidget(
|
||||
getLayoutManager(),
|
||||
this,
|
||||
new Size(
|
||||
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_H_DP),
|
||||
SizeMode.ABSOLUTE, 0.5f, SizeMode.RELATIVE),
|
||||
new DynamicTableModel(0, 1),
|
||||
new Size(
|
||||
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP),
|
||||
SizeMode.ABSOLUTE,
|
||||
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP),
|
||||
SizeMode.ABSOLUTE));
|
||||
|
||||
legend.position(
|
||||
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_X_OFFSET_DP),
|
||||
HorizontalPositioning.ABSOLUTE_FROM_RIGHT,
|
||||
PixelUtils.dpToPix(DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP),
|
||||
VerticalPositioning.ABSOLUTE_FROM_BOTTOM,
|
||||
Anchor.RIGHT_BOTTOM);
|
||||
|
||||
legend.setVisible(false);
|
||||
|
||||
final float padding = PixelUtils.dpToPix(DEFAULT_PADDING_DP);
|
||||
pie.setPadding(padding, padding, padding, padding);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processAttrs(TypedArray attrs) {
|
||||
|
||||
// borderPaint
|
||||
AttrUtils.configureLinePaint(attrs, getBorderPaint(),
|
||||
R.styleable.pie_PieChart_pieBorderColor, R.styleable.pie_PieChart_pieBorderThickness);
|
||||
}
|
||||
|
||||
public void setPie(PieWidget pie) {
|
||||
this.pie = pie;
|
||||
}
|
||||
|
||||
public PieWidget getPie() {
|
||||
return pie;
|
||||
}
|
||||
|
||||
public void addSegment(Segment segment, SegmentFormatter formatter) {
|
||||
addSeries(segment, formatter);
|
||||
}
|
||||
|
||||
public void removeSegment(Segment segment) {
|
||||
removeSeries(segment);
|
||||
}
|
||||
|
||||
public PieLegendWidget getLegend() {
|
||||
return legend;
|
||||
}
|
||||
|
||||
public void setLegend(PieLegendWidget legend) {
|
||||
this.legend = legend;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.androidplot.pie;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.androidplot.ui.widget.LegendItem;
|
||||
|
||||
/**
|
||||
* An item in a {@link PieLegendWidget} corresponding to a {@link Segment} in a {@link PieChart}.
|
||||
*/
|
||||
public class PieLegendItem implements LegendItem {
|
||||
|
||||
public SegmentFormatter formatter;
|
||||
|
||||
public Segment segment;
|
||||
|
||||
public PieLegendItem(@NonNull Segment segment, @NonNull SegmentFormatter formatter) {
|
||||
this.segment = segment;
|
||||
this.formatter = formatter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return segment.getTitle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.androidplot.pie;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.RectF;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.androidplot.ui.LayoutManager;
|
||||
import com.androidplot.ui.SeriesBundle;
|
||||
import com.androidplot.ui.Size;
|
||||
import com.androidplot.ui.TableModel;
|
||||
import com.androidplot.ui.widget.LegendWidget;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PieLegendWidget extends LegendWidget<PieLegendItem> {
|
||||
|
||||
private PieChart pieChart;
|
||||
|
||||
public PieLegendWidget(LayoutManager layoutManager, PieChart pieChart,
|
||||
Size widgetSize,
|
||||
TableModel tableModel,
|
||||
Size iconSize) {
|
||||
super(tableModel, layoutManager, widgetSize, iconSize);
|
||||
this.pieChart = pieChart;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull PieLegendItem item) {
|
||||
canvas.drawRect(iconRect, item.formatter.getFillPaint());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<PieLegendItem> getLegendItems() {
|
||||
final List<PieLegendItem> legendItems = new ArrayList<>();
|
||||
for(SeriesBundle<Segment, SegmentFormatter> item : pieChart.getRegistry().getLegendEnabledItems()) {
|
||||
legendItems.add(new PieLegendItem(item.getSeries(), item.getFormatter()));
|
||||
}
|
||||
return legendItems;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.pie;
|
||||
|
||||
import android.graphics.*;
|
||||
|
||||
import com.androidplot.ui.SeriesBundle;
|
||||
import com.androidplot.ui.SeriesRenderer;
|
||||
import com.androidplot.ui.RenderStack;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Basic renderer for drawing pie charts.
|
||||
*/
|
||||
public class PieRenderer extends SeriesRenderer<PieChart, Segment, SegmentFormatter> {
|
||||
|
||||
private static final float FULL_PIE_DEGS = 360f;
|
||||
private static final float HALF_PIE_DEGS = 180f;
|
||||
|
||||
// starting angle to use when drawing the first radial line of the first segment.
|
||||
private float startDegs = 0;
|
||||
|
||||
// number of degrees to extend from startDegs; can be used to "shape" the pie chart.
|
||||
private float extentDegs = FULL_PIE_DEGS;
|
||||
|
||||
// TODO: express donut in units other than px.
|
||||
private float donutSize = 0.5f;
|
||||
private DonutMode donutMode = DonutMode.PERCENT;
|
||||
|
||||
public enum DonutMode {
|
||||
PERCENT,
|
||||
PIXELS
|
||||
}
|
||||
|
||||
public PieRenderer(PieChart plot) {
|
||||
super(plot);
|
||||
}
|
||||
|
||||
public float getRadius(RectF rect) {
|
||||
return rect.width() < rect.height() ? rect.width() / 2 : rect.height() / 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRender(Canvas canvas, RectF plotArea, Segment series, SegmentFormatter formatter,
|
||||
RenderStack stack) {
|
||||
|
||||
// This renderer renders all series in one shot, so exclude any remaining series
|
||||
// from causing subsequent invocations of onRender:
|
||||
stack.disable(getClass());
|
||||
|
||||
float radius = getRadius(plotArea);
|
||||
PointF origin = new PointF(plotArea.centerX(), plotArea.centerY());
|
||||
|
||||
double[] values = getValues();
|
||||
double scale = calculateScale(values);
|
||||
float offset = degsToScreenDegs(startDegs);
|
||||
|
||||
RectF rec = new RectF(origin.x - radius, origin.y - radius, origin.x + radius,
|
||||
origin.y + radius);
|
||||
|
||||
int i = 0;
|
||||
for (SeriesBundle<Segment, ? extends SegmentFormatter> sfPair : getSeriesAndFormatterList()) {
|
||||
float lastOffset = offset;
|
||||
float sweep = (float) (scale * (values[i]) * extentDegs);
|
||||
offset += sweep;
|
||||
drawSegment(canvas, rec, sfPair.getSeries(), sfPair.getFormatter(), radius, lastOffset,
|
||||
sweep);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f,
|
||||
float rad, float startAngle, float sweep) {
|
||||
canvas.save();
|
||||
startAngle = startAngle + f.getRadialInset();
|
||||
sweep = sweep - (f.getRadialInset() * 2);
|
||||
|
||||
// midpoint angle between startAngle and endAngle
|
||||
final float halfSweepEndAngle = startAngle + (sweep / 2);
|
||||
|
||||
PointF translated = calculateLineEnd(
|
||||
bounds.centerX(), bounds.centerY(), f.getOffset(), halfSweepEndAngle);
|
||||
|
||||
final float cx = translated.x;
|
||||
final float cy = translated.y;
|
||||
|
||||
float donutSizePx;
|
||||
switch (donutMode) {
|
||||
case PERCENT:
|
||||
donutSizePx = donutSize * rad;
|
||||
break;
|
||||
case PIXELS:
|
||||
donutSizePx = (donutSize > 0) ? donutSize : (rad + donutSize);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unsupported DonutMde: " + donutMode);
|
||||
}
|
||||
|
||||
final float outerRad = rad - f.getOuterInset();
|
||||
final float innerRad = donutSizePx == 0 ? 0 : donutSizePx + f.getInnerInset();
|
||||
|
||||
// do we have a segment of less than 100%
|
||||
if (Math.abs(sweep - extentDegs) > Float.MIN_VALUE) {
|
||||
// vertices of the first radial:
|
||||
PointF r1Outer = calculateLineEnd(cx, cy, outerRad, startAngle);
|
||||
PointF r1Inner = calculateLineEnd(cx, cy, innerRad, startAngle);
|
||||
|
||||
// vertices of the second radial:
|
||||
PointF r2Outer = calculateLineEnd(cx, cy, outerRad, startAngle + sweep);
|
||||
PointF r2Inner = calculateLineEnd(cx, cy, innerRad, startAngle + sweep);
|
||||
|
||||
Path clip = new Path();
|
||||
|
||||
// outer arc:
|
||||
// leave plenty of room on the outside for stroked borders;
|
||||
// necessary because the clipping border is ugly
|
||||
// and cannot be easily anti aliased. Really we only care about masking off the
|
||||
// radial edges.
|
||||
clip.arcTo(new RectF(bounds.left - outerRad,
|
||||
bounds.top - outerRad,
|
||||
bounds.right + outerRad,
|
||||
bounds.bottom + outerRad),
|
||||
startAngle, sweep);
|
||||
clip.lineTo(cx, cy);
|
||||
clip.close();
|
||||
canvas.clipPath(clip);
|
||||
|
||||
Path p = new Path();
|
||||
|
||||
// outer arc:
|
||||
p.arcTo(new RectF(
|
||||
cx - outerRad,
|
||||
cy - outerRad,
|
||||
cx + outerRad,
|
||||
cy + outerRad)
|
||||
, startAngle, sweep);
|
||||
p.lineTo(r2Inner.x, r2Inner.y);
|
||||
|
||||
// inner arc:
|
||||
// sweep back to original angle:
|
||||
p.arcTo(new RectF(
|
||||
cx - innerRad,
|
||||
cy - innerRad,
|
||||
cx + innerRad,
|
||||
cy + innerRad),
|
||||
startAngle + sweep, -sweep);
|
||||
|
||||
p.close();
|
||||
|
||||
// fill segment:
|
||||
canvas.drawPath(p, f.getFillPaint());
|
||||
|
||||
// draw radial lines
|
||||
canvas.drawLine(r1Inner.x, r1Inner.y, r1Outer.x, r1Outer.y, f.getRadialEdgePaint());
|
||||
canvas.drawLine(r2Inner.x, r2Inner.y, r2Outer.x, r2Outer.y, f.getRadialEdgePaint());
|
||||
} else {
|
||||
canvas.save();
|
||||
Path chart = new Path();
|
||||
chart.addCircle(cx, cy, outerRad, Path.Direction.CW);
|
||||
Path inside = new Path();
|
||||
inside.addCircle(cx, cy, innerRad, Path.Direction.CW);
|
||||
canvas.clipPath(inside, Region.Op.DIFFERENCE);
|
||||
canvas.drawPath(chart, f.getFillPaint());
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// draw inner line:
|
||||
canvas.drawCircle(cx, cy, innerRad, f.getInnerEdgePaint());
|
||||
|
||||
// draw outer line:
|
||||
canvas.drawCircle(cx, cy, outerRad, f.getOuterEdgePaint());
|
||||
canvas.restore();
|
||||
|
||||
PointF labelOrigin = calculateLineEnd(cx, cy,
|
||||
(outerRad - ((outerRad - innerRad) / 2)), halfSweepEndAngle);
|
||||
|
||||
// TODO: move segment labelling outside the segment drawing loop
|
||||
// TODO: so that the labels will not be clipped by the edge of the next
|
||||
// TODO: segment being drawn.
|
||||
if (f.getLabelPaint() != null) {
|
||||
drawSegmentLabel(canvas, labelOrigin, seg, f);
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawSegmentLabel(Canvas canvas, PointF origin,
|
||||
Segment seg, SegmentFormatter f) {
|
||||
canvas.drawText(seg.getTitle(), origin.x, origin.y, f.getLabelPaint());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDrawLegendIcon(Canvas canvas, RectF rect, SegmentFormatter formatter) {
|
||||
throw new UnsupportedOperationException("Not yet implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines how many counts there are per cent of whatever the
|
||||
* pie chart is displaying as a fraction, 1 being 100%.
|
||||
*/
|
||||
protected double calculateScale(double[] values) {
|
||||
double total = 0;
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
total += values[i];
|
||||
}
|
||||
|
||||
return (1d / total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive the raw values being rendered from each {@link Segment}.
|
||||
* @return
|
||||
*/
|
||||
protected double[] getValues() {
|
||||
List<SeriesBundle<Segment, ? extends SegmentFormatter>> seriesList = getSeriesAndFormatterList();
|
||||
double[] result = new double[seriesList.size()];
|
||||
int i = 0;
|
||||
for (SeriesBundle<Segment, ? extends SegmentFormatter> sfPair : seriesList) {
|
||||
result[i] = sfPair.getSeries().getValue().doubleValue();
|
||||
i++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected PointF calculateLineEnd(float x, float y, float rad, float deg) {
|
||||
return calculateLineEnd(new PointF(x, y), rad, deg);
|
||||
}
|
||||
|
||||
protected PointF calculateLineEnd(PointF origin, float rad, float deg) {
|
||||
|
||||
double radians = deg * Math.PI / HALF_PIE_DEGS;
|
||||
double x = rad * Math.cos(radians);
|
||||
double y = rad * Math.sin(radians);
|
||||
|
||||
// convert to screen space:
|
||||
return new PointF(origin.x + (float) x, origin.y + (float) y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the size of the pie's empty inner space. May be specified as either pixels or as a percentage.
|
||||
* If using {@link DonutMode#PIXELS}, the best practice is to specify values in dp using
|
||||
* {@link com.androidplot.util.PixelUtils#dpToPix(float)}.
|
||||
*
|
||||
* If using {@link DonutMode#PERCENT} the value must be within the range 0 - 1. The value being
|
||||
* set corresponds to the size of the donut radius relative to the pie's total radius.
|
||||
* @param size
|
||||
* @param mode
|
||||
*/
|
||||
public void setDonutSize(float size, DonutMode mode) {
|
||||
switch (mode) {
|
||||
case PERCENT:
|
||||
if (size < 0 || size > 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Size parameter must be between 0 and 1 when operating in PERCENT mode.");
|
||||
}
|
||||
break;
|
||||
case PIXELS:
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Not yet implemented.");
|
||||
}
|
||||
donutMode = mode;
|
||||
donutSize = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the segment containing the specified point. This current implementation
|
||||
* only matches against angle; clicks outside of the pie/donut inner/outer boundaries
|
||||
* will still trigger a match on the segment whose beginning and ending angle contains
|
||||
* the angle of the line drawn between the pie chart's center point and the clicked point.
|
||||
* @param point The clicked point
|
||||
* @return Segment containing the clicked point.
|
||||
*/
|
||||
public Segment getContainingSegment(PointF point) {
|
||||
|
||||
RectF plotArea = getPlot().getPie().getWidgetDimensions().marginatedRect;
|
||||
// figure out the angle in degrees of the line between the clicked point
|
||||
// and the origin of the plotArea:
|
||||
PointF origin = new PointF(plotArea.centerX(), plotArea.centerY());
|
||||
float dx = point.x - origin.x;
|
||||
float dy = point.y - origin.y;
|
||||
double theta = Math.atan2(dy, dx);
|
||||
double angle = (theta * (HALF_PIE_DEGS / Math.PI));
|
||||
if (angle < 0) {
|
||||
// bring into 0-360 range
|
||||
angle += FULL_PIE_DEGS;
|
||||
}
|
||||
|
||||
// find the segment whose starting and ending angle (degs) contains
|
||||
// the angle calculated above
|
||||
List<SeriesBundle<Segment, ? extends SegmentFormatter>> seriesList = getSeriesAndFormatterList();
|
||||
int i = 0;
|
||||
double[] values = getValues();
|
||||
double scale = calculateScale(values);
|
||||
float offset = degsToScreenDegs(startDegs);
|
||||
for (SeriesBundle<Segment, ? extends SegmentFormatter> sfPair : seriesList) {
|
||||
float lastOffset = offset;
|
||||
float sweep = (float) (scale * (values[i]) * extentDegs);
|
||||
offset += sweep;
|
||||
offset = offset % FULL_PIE_DEGS;
|
||||
|
||||
final double dist = signedDistance(offset, angle);
|
||||
double endDist = signedDistance(offset, lastOffset);
|
||||
if(endDist < 0) {
|
||||
// segment accounts for more than 50% of the pie and wrapped around
|
||||
// need to correct:
|
||||
endDist = FULL_PIE_DEGS + endDist;
|
||||
}
|
||||
if(dist > 0 && dist <= endDist) {
|
||||
return sfPair.getSeries();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* convert conventional degrees (90 degrees is north) to screen degrees (90 degrees is south)
|
||||
* Values >= 369 will be converted back into the range of 0 - 359.999...
|
||||
* @param degs
|
||||
* @return
|
||||
*/
|
||||
protected static float degsToScreenDegs(float degs) {
|
||||
degs = degs % FULL_PIE_DEGS;
|
||||
|
||||
if (degs > 0) {
|
||||
return FULL_PIE_DEGS - degs;
|
||||
} else {
|
||||
return degs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the signed shortest angular distance between two angles
|
||||
* @param angle1
|
||||
* @param angle2
|
||||
* @return
|
||||
*/
|
||||
protected static double signedDistance(double angle1, double angle2) {
|
||||
double d = Math.abs(angle1 - angle2) % FULL_PIE_DEGS;
|
||||
double r = d > HALF_PIE_DEGS ? FULL_PIE_DEGS - d : d;
|
||||
|
||||
//calculate sign
|
||||
int sign = (angle1 - angle2 >= 0 && angle1 - angle2 <= HALF_PIE_DEGS)
|
||||
|| (angle1 - angle2 <= -HALF_PIE_DEGS && angle1 - angle2 >= -FULL_PIE_DEGS) ? 1 : -1;
|
||||
r *= sign;
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an IllegalArgumentException if the input value is outside of the range of 0.0 to 360.0
|
||||
* @param degs
|
||||
*/
|
||||
protected static void validateInputDegs(float degs) {
|
||||
if(degs < 0 || degs > FULL_PIE_DEGS) {
|
||||
throw new IllegalArgumentException("Degrees values must be between 0.0 and 360.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the starting point in degrees from which series will be drawn in order.
|
||||
* The input value must be within the range 0 - 360.
|
||||
* @param degs
|
||||
*/
|
||||
public void setStartDegs(float degs) {
|
||||
validateInputDegs(degs);
|
||||
startDegs = degs;
|
||||
}
|
||||
|
||||
public float getStartDegs() {
|
||||
return startDegs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the size in degrees of the pie chart, extending from the startDegs. The input value
|
||||
* must be within the range 0 - 360. An input value would represent 100% of the pie as a half
|
||||
* circle while an input value of 360 would be a full circle.
|
||||
* @param degs
|
||||
*/
|
||||
public void setExtentDegs(float degs) {
|
||||
validateInputDegs(degs);
|
||||
extentDegs = degs;
|
||||
}
|
||||
|
||||
public float getExtentDegs() {
|
||||
return extentDegs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.pie;
|
||||
|
||||
import android.graphics.*;
|
||||
import com.androidplot.ui.LayoutManager;
|
||||
import com.androidplot.ui.Size;
|
||||
import com.androidplot.ui.widget.Widget;
|
||||
import com.androidplot.ui.RenderStack;
|
||||
|
||||
/**
|
||||
* Visualizes data as a pie chart.
|
||||
*/
|
||||
public class PieWidget extends Widget {
|
||||
|
||||
private PieChart pieChart;
|
||||
private RenderStack<? extends Segment, ? extends SegmentFormatter> renderStack;
|
||||
|
||||
public PieWidget(LayoutManager layoutManager, PieChart pieChart, Size metrics) {
|
||||
super(layoutManager, metrics);
|
||||
this.pieChart = pieChart;
|
||||
renderStack = new RenderStack(pieChart);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doOnDraw(Canvas canvas, RectF widgetRect) {
|
||||
renderStack.sync();
|
||||
for(RenderStack.StackElement thisElement : renderStack.getElements()) {
|
||||
if(thisElement.isEnabled()) {
|
||||
pieChart.getRenderer(thisElement.get().getFormatter().getRendererClass()).
|
||||
render(canvas, widgetRect, thisElement.get(), renderStack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.pie;
|
||||
|
||||
import com.androidplot.Series;
|
||||
|
||||
/**
|
||||
* An implementation of Series representing a segment in a pie chart.
|
||||
*/
|
||||
public class Segment implements Series {
|
||||
|
||||
private String title;
|
||||
|
||||
private Number value;
|
||||
|
||||
public Segment(String title, Number value) {
|
||||
this.title = title;
|
||||
this.setValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public Number getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(Number value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.androidplot.pie;
|
||||
|
||||
import com.androidplot.ui.*;
|
||||
|
||||
/**
|
||||
* Manages the association between a given {@link Segment} and the {@link SegmentFormatter} that
|
||||
* will be used to render it.
|
||||
*/
|
||||
public class SegmentBundle extends SeriesBundle<Segment, SegmentFormatter> {
|
||||
|
||||
public SegmentBundle(Segment series, SegmentFormatter formatter) {
|
||||
super(series, formatter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.pie;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import com.androidplot.ui.SeriesRenderer;
|
||||
import com.androidplot.ui.Formatter;
|
||||
|
||||
public class SegmentFormatter extends Formatter<PieChart> {
|
||||
|
||||
private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT;
|
||||
private static final int DEFAULT_EDGE_COLOR = Color.BLACK;
|
||||
private static final int DEFAULT_LABEL_COLOR = Color.WHITE;
|
||||
private static final float DEFAULT_EDGE_THICKNESS = 3;
|
||||
private static final float DEFAULT_LABEL_MARKER_THICKNESS = 3;
|
||||
private static final float DEFAULT_LABEL_FONT_SIZE = 18;
|
||||
|
||||
private Paint innerEdgePaint;
|
||||
private Paint outerEdgePaint;
|
||||
private Paint radialEdgePaint;
|
||||
private Paint fillPaint;
|
||||
|
||||
private Paint labelPaint;
|
||||
private Paint labelMarkerPaint;
|
||||
|
||||
private float offset;
|
||||
private float radialInset;
|
||||
private float innerInset;
|
||||
private float outerInset;
|
||||
|
||||
{
|
||||
setFillPaint(new Paint());
|
||||
// outer edge:
|
||||
setOuterEdgePaint(new Paint());
|
||||
getOuterEdgePaint().setStyle(Paint.Style.STROKE);
|
||||
getOuterEdgePaint().setStrokeWidth(DEFAULT_EDGE_THICKNESS);
|
||||
getOuterEdgePaint().setAntiAlias(true);
|
||||
|
||||
// inner edge:
|
||||
setInnerEdgePaint(new Paint());
|
||||
getInnerEdgePaint().setStyle(Paint.Style.STROKE);
|
||||
getInnerEdgePaint().setStrokeWidth(DEFAULT_EDGE_THICKNESS);
|
||||
getInnerEdgePaint().setAntiAlias(true);
|
||||
|
||||
// radial edge:
|
||||
setRadialEdgePaint(new Paint());
|
||||
getRadialEdgePaint().setStyle(Paint.Style.STROKE);
|
||||
getRadialEdgePaint().setStrokeWidth(DEFAULT_EDGE_THICKNESS);
|
||||
getRadialEdgePaint().setAntiAlias(true);
|
||||
|
||||
// label paint:
|
||||
setLabelPaint(new Paint());
|
||||
getLabelPaint().setColor(DEFAULT_LABEL_COLOR);
|
||||
getLabelPaint().setTextSize(DEFAULT_LABEL_FONT_SIZE);
|
||||
getLabelPaint().setAntiAlias(true);
|
||||
getLabelPaint().setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
// label marker paint:
|
||||
setLabelMarkerPaint(new Paint());
|
||||
getLabelMarkerPaint().setColor(DEFAULT_LABEL_COLOR);
|
||||
getLabelMarkerPaint().setStrokeWidth(DEFAULT_LABEL_MARKER_THICKNESS);
|
||||
}
|
||||
|
||||
public SegmentFormatter(Integer fillColor) {
|
||||
if(fillColor != null) {
|
||||
getFillPaint().setColor(fillColor);
|
||||
} else {
|
||||
getFillPaint().setColor(DEFAULT_FILL_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
public SegmentFormatter(Context context, int xmlCfgId) {
|
||||
configure(context, xmlCfgId);
|
||||
}
|
||||
|
||||
public SegmentFormatter(Integer fillColor, Integer borderColor) {
|
||||
this(fillColor);
|
||||
getInnerEdgePaint().setColor(borderColor);
|
||||
getOuterEdgePaint().setColor(borderColor);
|
||||
getRadialEdgePaint().setColor(borderColor);
|
||||
}
|
||||
|
||||
public SegmentFormatter(Integer fillColor, Integer outerEdgeColor,
|
||||
Integer innerEdgeColor, Integer radialEdgeColor) {
|
||||
this(fillColor);
|
||||
if(getOuterEdgePaint() != null) {
|
||||
getOuterEdgePaint().setColor(outerEdgeColor);
|
||||
} else {
|
||||
outerEdgePaint = new Paint();
|
||||
getOuterEdgePaint().setColor(DEFAULT_EDGE_COLOR);
|
||||
}
|
||||
|
||||
if (getInnerEdgePaint() != null) {
|
||||
getInnerEdgePaint().setColor(innerEdgeColor);
|
||||
} else {
|
||||
outerEdgePaint = new Paint();
|
||||
getInnerEdgePaint().setColor(DEFAULT_EDGE_COLOR);
|
||||
}
|
||||
|
||||
if (getRadialEdgePaint() != null) {
|
||||
getRadialEdgePaint().setColor(radialEdgeColor);
|
||||
} else {
|
||||
radialEdgePaint = new Paint();
|
||||
getRadialEdgePaint().setColor(DEFAULT_EDGE_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends SeriesRenderer> getRendererClass() {
|
||||
return PieRenderer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeriesRenderer doGetRendererInstance(PieChart plot) {
|
||||
return new PieRenderer(plot);
|
||||
}
|
||||
|
||||
public Paint getInnerEdgePaint() {
|
||||
return innerEdgePaint;
|
||||
}
|
||||
|
||||
public void setInnerEdgePaint(Paint innerEdgePaint) {
|
||||
this.innerEdgePaint = innerEdgePaint;
|
||||
}
|
||||
|
||||
public Paint getOuterEdgePaint() {
|
||||
return outerEdgePaint;
|
||||
}
|
||||
|
||||
public void setOuterEdgePaint(Paint outerEdgePaint) {
|
||||
this.outerEdgePaint = outerEdgePaint;
|
||||
}
|
||||
|
||||
public Paint getRadialEdgePaint() {
|
||||
return radialEdgePaint;
|
||||
}
|
||||
|
||||
public void setRadialEdgePaint(Paint radialEdgePaint) {
|
||||
this.radialEdgePaint = radialEdgePaint;
|
||||
}
|
||||
|
||||
public Paint getFillPaint() {
|
||||
return fillPaint;
|
||||
}
|
||||
|
||||
public void setFillPaint(Paint fillPaint) {
|
||||
this.fillPaint = fillPaint;
|
||||
}
|
||||
|
||||
public Paint getLabelPaint() {
|
||||
return labelPaint;
|
||||
}
|
||||
|
||||
public void setLabelPaint(Paint labelPaint) {
|
||||
this.labelPaint = labelPaint;
|
||||
}
|
||||
|
||||
public Paint getLabelMarkerPaint() {
|
||||
return labelMarkerPaint;
|
||||
}
|
||||
|
||||
public void setLabelMarkerPaint(Paint labelMarkerPaint) {
|
||||
this.labelMarkerPaint = labelMarkerPaint;
|
||||
}
|
||||
|
||||
public float getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an offset relative to the center of the pie chart at which this segment should be drawn;
|
||||
* generally used to highlight specific segments.
|
||||
* @param offset
|
||||
*/
|
||||
public void setOffset(float offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public float getRadialInset() {
|
||||
return radialInset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an inset in degrees for the radial edges of this segment.
|
||||
* generally used to highlight specific segments.
|
||||
* @param radialInset
|
||||
*/
|
||||
public void setRadialInset(float radialInset) {
|
||||
this.radialInset = radialInset;
|
||||
}
|
||||
|
||||
public float getInnerInset() {
|
||||
return innerInset;
|
||||
}
|
||||
|
||||
public void setInnerInset(float innerInset) {
|
||||
this.innerInset = innerInset;
|
||||
}
|
||||
|
||||
public float getOuterInset() {
|
||||
return outerInset;
|
||||
}
|
||||
|
||||
public void setOuterInset(float outerInset) {
|
||||
this.outerInset = outerInset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.androidplot.pie;
|
||||
|
||||
import com.androidplot.*;
|
||||
|
||||
/**
|
||||
* SeriesRegistry implementation to be used in a {@link PieChart}.
|
||||
*/
|
||||
public class SegmentRegistry extends SeriesRegistry<SegmentBundle, Segment, SegmentFormatter> {
|
||||
|
||||
@Override
|
||||
protected SegmentBundle newSeriesBundle(Segment series, SegmentFormatter formatter) {
|
||||
return new SegmentBundle(series, formatter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
/**
|
||||
* Enumeration of possible anchor positions that a {@link com.androidplot.ui.widget.Widget} can use. There are a total
|
||||
* 8 possible anchor positions representing each corner of the Widget and the point exactly between each corner.
|
||||
*/
|
||||
public enum Anchor {
|
||||
TOP_MIDDLE,
|
||||
LEFT_TOP, // default
|
||||
LEFT_MIDDLE,
|
||||
LEFT_BOTTOM,
|
||||
RIGHT_TOP,
|
||||
RIGHT_MIDDLE,
|
||||
RIGHT_BOTTOM,
|
||||
BOTTOM_MIDDLE,
|
||||
CENTER
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* Convenience implementation of {@link BoxModelable}.
|
||||
*/
|
||||
public class BoxModel implements BoxModelable{
|
||||
|
||||
private float marginLeft;
|
||||
private float marginTop;
|
||||
private float marginRight;
|
||||
private float marginBottom;
|
||||
|
||||
|
||||
private float paddingLeft;
|
||||
private float paddingTop;
|
||||
private float paddingRight;
|
||||
private float paddingBottom;
|
||||
|
||||
/**
|
||||
* Default with 0 for all padding / margin values
|
||||
*/
|
||||
public BoxModel() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
public BoxModel(float marginLeft, float marginTop, float marginRight, float marginBottom,
|
||||
float paddingLeft, float paddingTop, float paddingRight, float paddingBottom) {
|
||||
this.marginLeft = marginLeft;
|
||||
this.marginTop = marginTop;
|
||||
this.marginRight = marginRight;
|
||||
this.marginBottom = marginBottom;
|
||||
|
||||
this.paddingLeft = paddingLeft;
|
||||
this.paddingTop = paddingTop;
|
||||
this.paddingRight = paddingRight;
|
||||
this.paddingBottom = paddingBottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a RectF instance describing the inner edge of the margin layer.
|
||||
* @param boundsRect
|
||||
* @return
|
||||
*/
|
||||
public RectF getMarginatedRect(RectF boundsRect) {
|
||||
return new RectF( boundsRect.left + getMarginLeft(),
|
||||
boundsRect.top + getMarginTop(),
|
||||
boundsRect.right - getMarginRight(),
|
||||
boundsRect.bottom - getMarginBottom());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a RectF instance describing the inner edge of the padding layer.
|
||||
* @param marginRect
|
||||
* @return
|
||||
*/
|
||||
public RectF getPaddedRect(RectF marginRect) {
|
||||
return new RectF(marginRect.left + getPaddingLeft(),
|
||||
marginRect.top + getPaddingTop(),
|
||||
marginRect.right - getPaddingRight(),
|
||||
marginRect.bottom - getPaddingBottom());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMargins(float left, float top, float right, float bottom) {
|
||||
setMarginLeft(left);
|
||||
setMarginTop(top);
|
||||
setMarginRight(right);
|
||||
setMarginBottom(bottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPadding(float left, float top, float right, float bottom) {
|
||||
setPaddingLeft(left);
|
||||
setPaddingTop(top);
|
||||
setPaddingRight(right);
|
||||
setPaddingBottom(bottom);
|
||||
}
|
||||
|
||||
|
||||
public float getMarginLeft() {
|
||||
return marginLeft;
|
||||
}
|
||||
|
||||
public void setMarginLeft(float marginLeft) {
|
||||
this.marginLeft = marginLeft;
|
||||
}
|
||||
|
||||
public float getMarginTop() {
|
||||
return marginTop;
|
||||
}
|
||||
|
||||
public void setMarginTop(float marginTop) {
|
||||
this.marginTop = marginTop;
|
||||
}
|
||||
|
||||
public float getMarginRight() {
|
||||
return marginRight;
|
||||
}
|
||||
|
||||
public void setMarginRight(float marginRight) {
|
||||
this.marginRight = marginRight;
|
||||
}
|
||||
|
||||
public float getMarginBottom() {
|
||||
return marginBottom;
|
||||
}
|
||||
|
||||
public void setMarginBottom(float marginBottom) {
|
||||
this.marginBottom = marginBottom;
|
||||
}
|
||||
|
||||
public float getPaddingLeft() {
|
||||
return paddingLeft;
|
||||
}
|
||||
|
||||
public void setPaddingLeft(float paddingLeft) {
|
||||
this.paddingLeft = paddingLeft;
|
||||
}
|
||||
|
||||
public float getPaddingTop() {
|
||||
return paddingTop;
|
||||
}
|
||||
|
||||
public void setPaddingTop(float paddingTop) {
|
||||
this.paddingTop = paddingTop;
|
||||
}
|
||||
|
||||
public float getPaddingRight() {
|
||||
return paddingRight;
|
||||
}
|
||||
|
||||
public void setPaddingRight(float paddingRight) {
|
||||
this.paddingRight = paddingRight;
|
||||
}
|
||||
|
||||
public float getPaddingBottom() {
|
||||
return paddingBottom;
|
||||
}
|
||||
|
||||
public void setPaddingBottom(float paddingBottom) {
|
||||
this.paddingBottom = paddingBottom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* Defines the properties of a <a href="http://www.w3.org/TR/CSS21/box.html">BoxModel</a> as used
|
||||
* by Androidplot. Essentially, the BoxModel composes three nested (but not necessarily concentric) rectangles:
|
||||
* * The bounding box, which is the outer-most box.
|
||||
* * The marginated box, which is calculated by applying the margin insets to the bounding box.
|
||||
* * The padded box, which is calculated by applying the padding insets to the marginated box.
|
||||
*/
|
||||
public interface BoxModelable {
|
||||
/**
|
||||
* Returns a RectF instance describing the inner edge of the margin layer.
|
||||
* @param boundsRect
|
||||
* @return
|
||||
*/
|
||||
RectF getMarginatedRect(RectF boundsRect);
|
||||
|
||||
/**
|
||||
* Returns a RectF instance describing the inner edge of the padding layer.
|
||||
* @param marginRect
|
||||
* @return
|
||||
*/
|
||||
RectF getPaddedRect(RectF marginRect);
|
||||
|
||||
|
||||
void setMargins(float left, float top, float right, float bottom);
|
||||
|
||||
void setPadding(float left, float top, float right, float bottom);
|
||||
|
||||
float getMarginLeft();
|
||||
|
||||
void setMarginLeft(float marginLeft);
|
||||
|
||||
float getMarginTop();
|
||||
|
||||
void setMarginTop(float marginTop);
|
||||
|
||||
float getMarginRight();
|
||||
|
||||
void setMarginRight(float marginRight);
|
||||
|
||||
float getMarginBottom();
|
||||
|
||||
void setMarginBottom(float marginBottm);
|
||||
|
||||
float getPaddingLeft();
|
||||
|
||||
void setPaddingLeft(float paddingLeft);
|
||||
|
||||
float getPaddingTop();
|
||||
|
||||
void setPaddingTop(float paddingTop);
|
||||
|
||||
float getPaddingRight();
|
||||
|
||||
void setPaddingRight(float paddingRight);
|
||||
|
||||
float getPaddingBottom();
|
||||
|
||||
void setPaddingBottom(float paddingBottom);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Encapsulates the visual aspects of a table; number of rows and columns
|
||||
* and the height and width in pixels of each element within the table.
|
||||
* There is no support (yet) for variable size cells within a table; all
|
||||
* cells within a table share the same dimensions.
|
||||
*
|
||||
* The DynamicTableModel provides an Iterator implementation which returns a RectF
|
||||
* of each subsequent cell, based on the order of the plot. Tables with
|
||||
* an order of COLUMN_MAJOR are traversed left to right column by column until
|
||||
* the end of the row is reached, then proceeding to the next row.
|
||||
* Tables with an order of ROW_MAJOR are traversed top to bottom row by row
|
||||
* until the end of the row is reached, then proceeding to the next column.
|
||||
*/
|
||||
public class DynamicTableModel extends TableModel {
|
||||
|
||||
private int numRows;
|
||||
private int numColumns;
|
||||
|
||||
/**
|
||||
* Convenience method. Sets order to ROW_MAJOR.
|
||||
* @param numColumns
|
||||
* @param numRows
|
||||
*/
|
||||
public DynamicTableModel(int numColumns, int numRows) {
|
||||
this(numColumns, numRows, TableOrder.ROW_MAJOR);
|
||||
|
||||
}
|
||||
|
||||
public DynamicTableModel(int numColumns, int numRows, TableOrder order) {
|
||||
super(order);
|
||||
this.numColumns = numColumns;
|
||||
this.numRows = numRows;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableModelIterator getIterator(RectF tableRect, int totalElements) {
|
||||
return new TableModelIterator(this, tableRect, totalElements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dimensions of a single element of this table with
|
||||
* tableRect representing the overall dimensions of the table.
|
||||
* @param tableRect Dimensions/position of the table
|
||||
* @return a RectF representing the first (top-left) element in
|
||||
* the tableRect passed in.
|
||||
*/
|
||||
public RectF getCellRect(RectF tableRect, int numElements) {
|
||||
RectF cellRect = new RectF();
|
||||
cellRect.left = tableRect.left;
|
||||
cellRect.top = tableRect.top;
|
||||
cellRect.bottom = tableRect.top + calculateCellSize(tableRect, TableModel.Axis.ROW, numElements);
|
||||
cellRect.right = tableRect.left + calculateCellSize(tableRect, TableModel.Axis.COLUMN, numElements);
|
||||
return cellRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out the size of a single cell across the specified axis.
|
||||
* @param tableRect
|
||||
* @param axis
|
||||
* @param numElementsInTable
|
||||
* @return
|
||||
*/
|
||||
private float calculateCellSize(RectF tableRect,
|
||||
Axis axis,
|
||||
int numElementsInTable) {
|
||||
int axisElements = 0;
|
||||
|
||||
float axisSizePix = 0;
|
||||
switch (axis) {
|
||||
case ROW:
|
||||
axisElements = numRows;
|
||||
axisSizePix = tableRect.height();
|
||||
break;
|
||||
case COLUMN:
|
||||
axisElements = numColumns;
|
||||
axisSizePix = tableRect.width();
|
||||
break;
|
||||
}
|
||||
if(axisElements != 0) {
|
||||
return axisSizePix / axisElements;
|
||||
} else {
|
||||
return axisSizePix / numElementsInTable;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getNumRows() {
|
||||
return numRows;
|
||||
}
|
||||
|
||||
public void setNumRows(int numRows) {
|
||||
this.numRows = numRows;
|
||||
}
|
||||
|
||||
public int getNumColumns() {
|
||||
return numColumns;
|
||||
}
|
||||
|
||||
public void setNumColumns(int numColumns) {
|
||||
this.numColumns = numColumns;
|
||||
}
|
||||
|
||||
private class TableModelIterator implements Iterator<RectF> {
|
||||
private boolean isOk = true;
|
||||
int lastColumn = 0; // most recent column iterated
|
||||
int lastRow = 0; // most recent row iterated
|
||||
int lastElement = 0; // last element index iterated
|
||||
private DynamicTableModel dynamicTableModel;
|
||||
private RectF tableRect;
|
||||
private RectF lastElementRect;
|
||||
private int totalElements;
|
||||
private TableOrder order;
|
||||
|
||||
private int calculatedNumElements;
|
||||
private int calculatedRows; // number of rows to be iterated
|
||||
private int calculatedColumns; // number of columns to be iterated
|
||||
|
||||
public TableModelIterator(DynamicTableModel dynamicTableModel, RectF tableRect, int totalElements) {
|
||||
this.dynamicTableModel = dynamicTableModel;
|
||||
this.tableRect = tableRect;
|
||||
this.totalElements = totalElements;
|
||||
order = dynamicTableModel.getOrder();
|
||||
|
||||
// unlimited columns:
|
||||
if(dynamicTableModel.getNumColumns() == 0 && dynamicTableModel.getNumRows() >= 1) {
|
||||
calculatedRows = dynamicTableModel.getNumRows();
|
||||
|
||||
// round up:
|
||||
calculatedColumns = Float.valueOf((totalElements / (float) calculatedRows) + 0.5f).intValue();
|
||||
} else if(dynamicTableModel.getNumRows() == 0 && dynamicTableModel.getNumColumns() >= 1) {
|
||||
calculatedColumns = dynamicTableModel.getNumColumns();
|
||||
calculatedRows = Float.valueOf((totalElements / (float) calculatedColumns) + 0.5f).intValue();
|
||||
// unlimited rows and columns (impossible) so default a single row with n columns:
|
||||
}else if(dynamicTableModel.getNumColumns() == 0 && dynamicTableModel.getNumRows() == 0) {
|
||||
calculatedRows = 1;
|
||||
calculatedColumns = totalElements;
|
||||
} else {
|
||||
calculatedRows = dynamicTableModel.getNumRows();
|
||||
calculatedColumns = dynamicTableModel.getNumColumns();
|
||||
}
|
||||
calculatedNumElements = calculatedRows * calculatedColumns;
|
||||
lastElementRect = dynamicTableModel.getCellRect(tableRect, totalElements);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return isOk && lastElement < calculatedNumElements;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RectF next() {
|
||||
if(!hasNext()) {
|
||||
isOk = false;
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
|
||||
if (lastElement == 0) {
|
||||
lastElement++;
|
||||
return lastElementRect;
|
||||
}
|
||||
|
||||
RectF nextElementRect = new RectF(lastElementRect);
|
||||
|
||||
switch (order) {
|
||||
case ROW_MAJOR:
|
||||
if (dynamicTableModel.getNumColumns() > 0 && lastColumn >= (dynamicTableModel.getNumColumns() - 1)) {
|
||||
// move to the begining of the next row down:// move to the begining of the next row down:
|
||||
nextElementRect.offsetTo(tableRect.left, lastElementRect.bottom);
|
||||
lastColumn = 0;
|
||||
lastRow++;
|
||||
} else {
|
||||
// move to the next column over:
|
||||
nextElementRect.offsetTo(lastElementRect.right, lastElementRect.top);
|
||||
lastColumn++;
|
||||
}
|
||||
break;
|
||||
case COLUMN_MAJOR:
|
||||
if (dynamicTableModel.getNumRows() > 0 && lastRow >= (dynamicTableModel.getNumRows() - 1)) {
|
||||
// move to the top of the next column over:
|
||||
nextElementRect.offsetTo(lastElementRect.right, tableRect.top);
|
||||
lastRow = 0;
|
||||
lastColumn++;
|
||||
} else {
|
||||
// move to the next row down:
|
||||
nextElementRect.offsetTo(lastElementRect.left, lastElementRect.bottom);
|
||||
lastRow++;
|
||||
}
|
||||
break;
|
||||
// unknown/unsupported enum val:
|
||||
default:
|
||||
isOk = false;
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
lastElement++;
|
||||
lastElementRect = nextElementRect;
|
||||
return nextElementRect;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
public class FixedTableModel extends TableModel {
|
||||
private float cellWidth;
|
||||
private float cellHeight;
|
||||
|
||||
public FixedTableModel(float cellWidth, float cellHeight, TableOrder order) {
|
||||
super(order);
|
||||
setCellWidth(cellWidth);
|
||||
setCellHeight(cellHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<RectF> getIterator(RectF tableRect, int totalElements) {
|
||||
return new FixedTableModelIterator(this, tableRect, totalElements);
|
||||
}
|
||||
|
||||
public float getCellWidth() {
|
||||
return cellWidth;
|
||||
}
|
||||
|
||||
public void setCellWidth(float cellWidth) {
|
||||
this.cellWidth = cellWidth;
|
||||
}
|
||||
|
||||
public float getCellHeight() {
|
||||
return cellHeight;
|
||||
}
|
||||
|
||||
public void setCellHeight(float cellHeight) {
|
||||
this.cellHeight = cellHeight;
|
||||
}
|
||||
|
||||
private class FixedTableModelIterator implements Iterator<RectF> {
|
||||
|
||||
private FixedTableModel model;
|
||||
private RectF tableRect;
|
||||
private RectF lastRect;
|
||||
private int numElements;
|
||||
private int lastElement;
|
||||
protected FixedTableModelIterator(FixedTableModel model, RectF tableRect, int numElements) {
|
||||
this.model = model;
|
||||
this.tableRect = tableRect;
|
||||
this.numElements = numElements;
|
||||
lastRect = new RectF(
|
||||
tableRect.left,
|
||||
tableRect.top,
|
||||
tableRect.left + model.getCellWidth(),
|
||||
tableRect.top + model.getCellHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
// was this the last element or is there no room in either axis for another cell?
|
||||
return !(lastElement >= numElements || (isColumnFinished() && isRowFinished()));
|
||||
}
|
||||
|
||||
private boolean isColumnFinished() {
|
||||
return lastRect.bottom + model.getCellHeight() > tableRect.height();
|
||||
}
|
||||
|
||||
private boolean isRowFinished() {
|
||||
return lastRect.right + model.getCellWidth() > tableRect.width();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RectF next() {
|
||||
try {
|
||||
if (lastElement == 0) {
|
||||
return lastRect;
|
||||
}
|
||||
|
||||
if (lastElement >= numElements) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
switch (model.getOrder()) {
|
||||
case ROW_MAJOR:
|
||||
if (isColumnFinished()) {
|
||||
moveOverAndUp();
|
||||
} else {
|
||||
moveDown();
|
||||
}
|
||||
break;
|
||||
case COLUMN_MAJOR:
|
||||
if (isRowFinished()) {
|
||||
moveDownAndBack();
|
||||
} else {
|
||||
moveOver();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
return lastRect;
|
||||
} finally {
|
||||
lastElement++;
|
||||
}
|
||||
}
|
||||
|
||||
private void moveDownAndBack() {
|
||||
//RectF rect = new RectF(lastRect);
|
||||
lastRect.offsetTo(tableRect.left, lastRect.bottom);
|
||||
//return rect;
|
||||
}
|
||||
|
||||
private void moveOverAndUp() {
|
||||
//RectF rect = new RectF(lastRect);
|
||||
lastRect.offsetTo(lastRect.right, tableRect.top);
|
||||
//return rect;
|
||||
}
|
||||
|
||||
private void moveOver() {
|
||||
//RectF rect = new RectF(lastRect);
|
||||
lastRect.offsetTo(lastRect.right, lastRect.top);
|
||||
//return rect;
|
||||
}
|
||||
|
||||
private void moveDown() {
|
||||
//RectF rect = new RectF(lastRect);
|
||||
lastRect.offsetTo(lastRect.left, lastRect.bottom);
|
||||
//return rect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import com.androidplot.Plot;
|
||||
import com.halfhp.fig.*;
|
||||
|
||||
/**
|
||||
* Base class of all Formatters. Encapsulates visual elements of a series; line style, color etc.
|
||||
* Implementors of this class should include both a default constructor and a one argument
|
||||
* constructor in the following form:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* // provided as a convenience to users; allows instantiation and
|
||||
* // xml configuration in a single line.
|
||||
* public MyFormatter(Context ctx, int xmlCfgId) {
|
||||
* // prevent configuration of classes derived from this one:
|
||||
* if (getClass().equals(MyFormatter.class)) {
|
||||
* Configurator.configure(ctx, this, xmlCfgId);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public abstract class Formatter<PlotType extends Plot> {
|
||||
|
||||
private boolean isLegendIconEnabled = true;
|
||||
|
||||
public Formatter() {}
|
||||
|
||||
public Formatter(Context ctx, int xmlCfgId) {
|
||||
configure(ctx, xmlCfgId);
|
||||
}
|
||||
|
||||
public void configure(Context ctx, int xmlCfgId) {
|
||||
try {
|
||||
Fig.configure(ctx, this, xmlCfgId);
|
||||
} catch (FigException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param plot
|
||||
* @param <T>
|
||||
* @return @return An instance of SeriesRenderer constructed with the specified plot.
|
||||
*/
|
||||
public <T extends SeriesRenderer> T getRendererInstance(PlotType plot) {
|
||||
return (T) doGetRendererInstance(plot);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The Class of SeriesRenderer that should be used when rendering series associated
|
||||
* with instances of this formatter.
|
||||
*/
|
||||
public abstract Class<? extends SeriesRenderer> getRendererClass();
|
||||
|
||||
/**
|
||||
*
|
||||
* @return An instance of SeriesRenderer constructed with the specified plot.
|
||||
*/
|
||||
protected abstract SeriesRenderer doGetRendererInstance(PlotType plot);
|
||||
|
||||
public boolean isLegendIconEnabled() {
|
||||
return isLegendIconEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not a legend icon should be drawn for the series associated with this formatter.
|
||||
* @param legendIconEnabled
|
||||
*/
|
||||
public void setLegendIconEnabled(boolean legendIconEnabled) {
|
||||
this.isLegendIconEnabled = legendIconEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
public class HorizontalPosition extends PositionMetric<HorizontalPositioning> {
|
||||
|
||||
public HorizontalPosition(float value, HorizontalPositioning layoutStyle) {
|
||||
super(value, layoutStyle);
|
||||
validatePair(value, layoutStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws IllegalArgumentException if there is a problem.
|
||||
* @param value
|
||||
*/
|
||||
protected void validatePair(float value, HorizontalPositioning layoutStyle) {
|
||||
switch(layoutStyle) {
|
||||
case ABSOLUTE_FROM_LEFT:
|
||||
case ABSOLUTE_FROM_RIGHT:
|
||||
case ABSOLUTE_FROM_CENTER:
|
||||
validateValue(value, PositionMetric.LayoutMode.ABSOLUTE);
|
||||
break;
|
||||
case RELATIVE_TO_LEFT:
|
||||
case RELATIVE_TO_RIGHT:
|
||||
case RELATIVE_TO_CENTER:
|
||||
validateValue(value, PositionMetric.LayoutMode.RELATIVE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPixelValue(float size) {
|
||||
switch(getLayoutType()) {
|
||||
case ABSOLUTE_FROM_LEFT:
|
||||
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_BEGINING);
|
||||
case ABSOLUTE_FROM_RIGHT:
|
||||
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_END);
|
||||
case ABSOLUTE_FROM_CENTER:
|
||||
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_CENTER);
|
||||
case RELATIVE_TO_LEFT:
|
||||
return this.getRelativePosition(size, PositionMetric.Origin.FROM_BEGINING);
|
||||
case RELATIVE_TO_RIGHT:
|
||||
return this.getRelativePosition(size, PositionMetric.Origin.FROM_END);
|
||||
case RELATIVE_TO_CENTER:
|
||||
return this.getRelativePosition(size, PositionMetric.Origin.FROM_CENTER);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported LayoutType: " + this.getLayoutType());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLayoutType(HorizontalPositioning layoutType) {
|
||||
super.setLayoutType(layoutType);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
public enum HorizontalPositioning {
|
||||
ABSOLUTE_FROM_LEFT,
|
||||
ABSOLUTE_FROM_RIGHT,
|
||||
ABSOLUTE_FROM_CENTER,
|
||||
RELATIVE_TO_LEFT,
|
||||
RELATIVE_TO_RIGHT,
|
||||
RELATIVE_TO_CENTER
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
/**
|
||||
* A set of insets for a rect space.
|
||||
*/
|
||||
public class Insets {
|
||||
|
||||
private float top;
|
||||
private float bottom;
|
||||
private float left;
|
||||
private float right;
|
||||
|
||||
public Insets() {}
|
||||
|
||||
public Insets(float top, float bottom, float left, float right) {
|
||||
this.top = top;
|
||||
this.bottom = bottom;
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
public float getTop() {
|
||||
return top;
|
||||
}
|
||||
|
||||
public void setTop(float top) {
|
||||
this.top = top;
|
||||
}
|
||||
|
||||
public float getBottom() {
|
||||
return bottom;
|
||||
}
|
||||
|
||||
public void setBottom(float bottom) {
|
||||
this.bottom = bottom;
|
||||
}
|
||||
|
||||
public float getLeft() {
|
||||
return left;
|
||||
}
|
||||
|
||||
public void setLeft(float left) {
|
||||
this.left = left;
|
||||
}
|
||||
|
||||
public float getRight() {
|
||||
return right;
|
||||
}
|
||||
|
||||
public void setRight(float right) {
|
||||
this.right = right;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import com.androidplot.ui.widget.Widget;
|
||||
import com.androidplot.util.DisplayDimensions;
|
||||
import com.androidplot.util.LinkedLayerList;
|
||||
|
||||
public class LayoutManager extends LinkedLayerList<Widget>
|
||||
implements View.OnTouchListener, Resizable {
|
||||
private boolean drawAnchorsEnabled = false;
|
||||
private Paint anchorPaint;
|
||||
private boolean drawOutlinesEnabled = false;
|
||||
private Paint outlinePaint;
|
||||
private boolean drawOutlineShadowsEnabled = false;
|
||||
private Paint outlineShadowPaint;
|
||||
private boolean drawMarginsEnabled = false;
|
||||
private Paint marginPaint;
|
||||
private boolean drawPaddingEnabled = false;
|
||||
private Paint paddingPaint;
|
||||
private DisplayDimensions displayDims = new DisplayDimensions();
|
||||
|
||||
{
|
||||
anchorPaint = new Paint();
|
||||
anchorPaint.setStyle(Paint.Style.FILL);
|
||||
anchorPaint.setColor(Color.GREEN);
|
||||
outlinePaint = new Paint();
|
||||
outlinePaint.setColor(Color.GREEN);
|
||||
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||
outlinePaint.setAntiAlias(true);
|
||||
outlinePaint.setStrokeWidth(2);
|
||||
marginPaint = new Paint();
|
||||
marginPaint.setColor(Color.YELLOW);
|
||||
marginPaint.setStyle(Paint.Style.FILL);
|
||||
marginPaint.setAlpha(200);
|
||||
paddingPaint= new Paint();
|
||||
paddingPaint.setColor(Color.BLUE);
|
||||
paddingPaint.setStyle(Paint.Style.FILL);
|
||||
paddingPaint.setAlpha(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked immediately following XML configuration.
|
||||
*/
|
||||
public synchronized void onPostInit() {
|
||||
for(Widget w : elements()) {
|
||||
w.onPostInit();
|
||||
}
|
||||
}
|
||||
|
||||
public LayoutManager() {
|
||||
}
|
||||
|
||||
public void setMarkupEnabled(boolean enabled) {
|
||||
setDrawOutlinesEnabled(enabled);
|
||||
setDrawAnchorsEnabled(enabled);
|
||||
setDrawMarginsEnabled(enabled);
|
||||
setDrawPaddingEnabled(enabled);
|
||||
setDrawOutlineShadowsEnabled(enabled);
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
if(isDrawMarginsEnabled()) {
|
||||
drawSpacing(canvas, displayDims.canvasRect, displayDims.marginatedRect, marginPaint);
|
||||
}
|
||||
if (isDrawPaddingEnabled()) {
|
||||
drawSpacing(canvas, displayDims.marginatedRect, displayDims.paddedRect, paddingPaint);
|
||||
}
|
||||
for (Widget widget : elements()) {
|
||||
try {
|
||||
canvas.save();
|
||||
PositionMetrics metrics = widget.getPositionMetrics();
|
||||
float elementWidth = widget.getWidthPix(displayDims.paddedRect.width());
|
||||
float elementHeight = widget.getHeightPix(displayDims.paddedRect.height());
|
||||
PointF coords = Widget.calculateCoordinates(elementHeight,
|
||||
elementWidth, displayDims.paddedRect, metrics);
|
||||
|
||||
DisplayDimensions dims = widget.getWidgetDimensions();
|
||||
|
||||
if (drawOutlineShadowsEnabled) {
|
||||
canvas.drawRect(dims.canvasRect, outlineShadowPaint);
|
||||
}
|
||||
|
||||
// not positive why this is, but the rect clipped by clipRect is 1 less than the one drawn by drawRect.
|
||||
// so this is necessary to avoid clipping borders. I suspect that its a floating point
|
||||
// jitter issue.
|
||||
if (widget.isClippingEnabled()) {
|
||||
canvas.clipRect(dims.canvasRect, Region.Op.INTERSECT);
|
||||
}
|
||||
widget.draw(canvas);
|
||||
|
||||
if (drawMarginsEnabled) {
|
||||
drawSpacing(canvas, dims.canvasRect, dims.marginatedRect, getMarginPaint());
|
||||
}
|
||||
|
||||
if (drawPaddingEnabled) {
|
||||
drawSpacing(canvas, dims.marginatedRect, dims.paddedRect, getPaddingPaint());
|
||||
}
|
||||
|
||||
if (drawAnchorsEnabled) {
|
||||
PointF anchorCoords =
|
||||
Widget.getAnchorCoordinates(coords.x, coords.y, elementWidth,
|
||||
elementHeight, metrics.getAnchor());
|
||||
drawAnchor(canvas, anchorCoords);
|
||||
}
|
||||
|
||||
|
||||
if (drawOutlinesEnabled) {
|
||||
canvas.drawRect(dims.canvasRect, outlinePaint);
|
||||
}
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawSpacing(Canvas canvas, RectF outer, RectF inner, Paint paint) {
|
||||
try {
|
||||
canvas.save();
|
||||
canvas.clipRect(inner, Region.Op.DIFFERENCE);
|
||||
canvas.drawRect(outer, paint);
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawAnchor(Canvas canvas, PointF coords) {
|
||||
float anchorSize = 4;
|
||||
canvas.drawRect(coords.x-anchorSize, coords.y-anchorSize, coords.x+anchorSize, coords.y+anchorSize, anchorPaint);
|
||||
|
||||
}
|
||||
|
||||
public boolean isDrawOutlinesEnabled() {
|
||||
return drawOutlinesEnabled;
|
||||
}
|
||||
|
||||
public void setDrawOutlinesEnabled(boolean drawOutlinesEnabled) {
|
||||
this.drawOutlinesEnabled = drawOutlinesEnabled;
|
||||
}
|
||||
|
||||
public Paint getOutlinePaint() {
|
||||
return outlinePaint;
|
||||
}
|
||||
|
||||
public void setOutlinePaint(Paint outlinePaint) {
|
||||
this.outlinePaint = outlinePaint;
|
||||
}
|
||||
|
||||
public boolean isDrawAnchorsEnabled() {
|
||||
return drawAnchorsEnabled;
|
||||
}
|
||||
|
||||
public void setDrawAnchorsEnabled(boolean drawAnchorsEnabled) {
|
||||
this.drawAnchorsEnabled = drawAnchorsEnabled;
|
||||
}
|
||||
|
||||
public boolean isDrawMarginsEnabled() {
|
||||
return drawMarginsEnabled;
|
||||
}
|
||||
|
||||
public void setDrawMarginsEnabled(boolean drawMarginsEnabled) {
|
||||
this.drawMarginsEnabled = drawMarginsEnabled;
|
||||
}
|
||||
|
||||
public Paint getMarginPaint() {
|
||||
return marginPaint;
|
||||
}
|
||||
|
||||
public void setMarginPaint(Paint marginPaint) {
|
||||
this.marginPaint = marginPaint;
|
||||
}
|
||||
|
||||
public boolean isDrawPaddingEnabled() {
|
||||
return drawPaddingEnabled;
|
||||
}
|
||||
|
||||
public void setDrawPaddingEnabled(boolean drawPaddingEnabled) {
|
||||
this.drawPaddingEnabled = drawPaddingEnabled;
|
||||
}
|
||||
|
||||
public Paint getPaddingPaint() {
|
||||
return paddingPaint;
|
||||
}
|
||||
|
||||
public void setPaddingPaint(Paint paddingPaint) {
|
||||
this.paddingPaint = paddingPaint;
|
||||
}
|
||||
|
||||
public boolean isDrawOutlineShadowsEnabled() {
|
||||
return drawOutlineShadowsEnabled;
|
||||
}
|
||||
|
||||
public void setDrawOutlineShadowsEnabled(boolean drawOutlineShadowsEnabled) {
|
||||
this.drawOutlineShadowsEnabled = drawOutlineShadowsEnabled;
|
||||
if(drawOutlineShadowsEnabled && outlineShadowPaint == null) {
|
||||
// use a default shadow effect in the case where none has been set:
|
||||
outlineShadowPaint = new Paint();
|
||||
outlineShadowPaint.setColor(Color.DKGRAY);
|
||||
outlineShadowPaint.setStyle(Paint.Style.FILL);
|
||||
outlineShadowPaint.setShadowLayer(3, 5, 5, Color.BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
public Paint getOutlineShadowPaint() {
|
||||
return outlineShadowPaint;
|
||||
}
|
||||
|
||||
public void setOutlineShadowPaint(Paint outlineShadowPaint) {
|
||||
this.outlineShadowPaint = outlineShadowPaint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates layouts for all widgets using last set
|
||||
* DisplayDimensions. Care should be excersized when choosing when
|
||||
* to call this method as it is a relatively slow operation.
|
||||
*/
|
||||
public void refreshLayout() {
|
||||
for (Widget widget : elements()) {
|
||||
widget.layout(displayDims);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void layout(final DisplayDimensions dims) {
|
||||
this.displayDims = dims;
|
||||
|
||||
refreshLayout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
|
||||
|
||||
abstract class LayoutMetric<LayoutType extends Enum> {
|
||||
|
||||
private LayoutType layoutType;
|
||||
|
||||
//private LayoutType layoutType;
|
||||
private float value;
|
||||
//private float lastRow;
|
||||
|
||||
public LayoutMetric(float value, LayoutType layoutType) {
|
||||
validatePair(value, layoutType);
|
||||
set(value, layoutType);
|
||||
//setLayoutType(layoutType);
|
||||
//setValue(value);
|
||||
//setLayoutType(layoutType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the values passed in are valid for the layout algorithm being used.
|
||||
* @param value
|
||||
* @param layoutType
|
||||
*/
|
||||
protected abstract void validatePair(float value, LayoutType layoutType);
|
||||
|
||||
public void set(float value, LayoutType layoutType) {
|
||||
validatePair(value, layoutType);
|
||||
this.value = value;
|
||||
this.layoutType = layoutType;
|
||||
}
|
||||
|
||||
public float getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(float value) {
|
||||
validatePair(value, layoutType);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public abstract float getPixelValue(float size);
|
||||
|
||||
public LayoutType getLayoutType() {
|
||||
return layoutType;
|
||||
}
|
||||
|
||||
public void setLayoutType(LayoutType layoutType) {
|
||||
validatePair(value, layoutType);
|
||||
this.layoutType = layoutType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
public abstract class PositionMetric<LayoutType extends Enum> extends LayoutMetric<LayoutType> {
|
||||
|
||||
protected enum Origin {
|
||||
FROM_BEGINING,
|
||||
FROM_CENTER,
|
||||
FROM_END
|
||||
}
|
||||
|
||||
protected enum LayoutMode {
|
||||
ABSOLUTE,
|
||||
RELATIVE
|
||||
}
|
||||
|
||||
public PositionMetric(float value, LayoutType layoutType) {
|
||||
super(value, layoutType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws IllegalArgumentException if there is a problem.
|
||||
* @param value
|
||||
* @param layoutMode
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
protected static void validateValue(float value, LayoutMode layoutMode) throws IllegalArgumentException {
|
||||
switch(layoutMode) {
|
||||
case ABSOLUTE:
|
||||
break;
|
||||
case RELATIVE:
|
||||
if(value < -1 || value > 1) {
|
||||
throw new IllegalArgumentException("Relative layout values must be within the range of -1 to 1.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown LayoutMode: " + layoutMode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected float getAbsolutePosition(float size, Origin origin) {
|
||||
switch(origin) {
|
||||
case FROM_BEGINING:
|
||||
return getValue();
|
||||
case FROM_CENTER:
|
||||
return (size/2f) + getValue();
|
||||
case FROM_END:
|
||||
return size - getValue();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported Origin: " + origin);
|
||||
}
|
||||
}
|
||||
|
||||
protected float getRelativePosition(float size, Origin origin) {
|
||||
//throw new UnsupportedOperationException("Not yet implemented.");
|
||||
|
||||
switch(origin) {
|
||||
case FROM_BEGINING:
|
||||
return size * getValue();
|
||||
case FROM_CENTER:
|
||||
return (size/2f) + ((size/2f) * getValue());
|
||||
case FROM_END:
|
||||
return size + (size*getValue());
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported Origin: " + origin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class PositionMetrics implements Comparable<PositionMetrics> {
|
||||
|
||||
private HorizontalPosition horizontalPosition;
|
||||
private VerticalPosition verticalPosition;
|
||||
private Anchor anchor;
|
||||
private float layerDepth;
|
||||
|
||||
public PositionMetrics(float x, HorizontalPositioning horizontalPositioning, float y, VerticalPositioning verticalPositioning, Anchor anchor) {
|
||||
setXPositionMetric(new HorizontalPosition(x, horizontalPositioning));
|
||||
setYPositionMetric(new VerticalPosition(y, verticalPositioning));
|
||||
setAnchor(anchor);
|
||||
|
||||
}
|
||||
|
||||
public VerticalPosition getYPositionMetric() {
|
||||
return verticalPosition;
|
||||
}
|
||||
|
||||
public void setYPositionMetric(VerticalPosition verticalPosition) {
|
||||
this.verticalPosition = verticalPosition;
|
||||
}
|
||||
|
||||
public Anchor getAnchor() {
|
||||
return anchor;
|
||||
}
|
||||
|
||||
public void setAnchor(Anchor anchor) {
|
||||
this.anchor = anchor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull PositionMetrics o) {
|
||||
if(this.layerDepth < o.layerDepth) {
|
||||
return -1;
|
||||
} else if(this.layerDepth == o.layerDepth) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public HorizontalPosition getXPositionMetric() {
|
||||
return horizontalPosition;
|
||||
}
|
||||
|
||||
public void setXPositionMetric(HorizontalPosition horizontalPosition) {
|
||||
this.horizontalPosition = horizontalPosition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import com.androidplot.Series;
|
||||
import com.androidplot.xy.XYSeriesFormatter;
|
||||
|
||||
public abstract class RenderBundle<RenderBundleType extends RenderBundle, SeriesType extends Series, SeriesFormatterType extends XYSeriesFormatter> {
|
||||
//private XYDataset series;
|
||||
private Series series;
|
||||
private SeriesFormatterType formatter;
|
||||
|
||||
public RenderBundle(SeriesType series, SeriesFormatterType formatter) {
|
||||
this.formatter = formatter;
|
||||
this.series = series;
|
||||
}
|
||||
|
||||
public Series getSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
public void setSeries(Series series) {
|
||||
this.series = series;
|
||||
}
|
||||
|
||||
public SeriesFormatterType getFormatter() {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
public void setFormatter(SeriesFormatterType formatter) {
|
||||
this.formatter = formatter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import com.androidplot.Plot;
|
||||
import com.androidplot.Series;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A stack of series to be rendered. The stack order is immutable but individual elements may be
|
||||
* manipulated via the public methods of {@link RenderStack.StackElement}.
|
||||
*/
|
||||
public class RenderStack<SeriesType extends Series, FormatterType extends Formatter> {
|
||||
|
||||
private final Plot plot;
|
||||
|
||||
private final ArrayList<StackElement<SeriesType, FormatterType>> elements;
|
||||
|
||||
public ArrayList<StackElement<SeriesType, FormatterType>> getElements() {
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* An element on the render stack.
|
||||
*/
|
||||
public class StackElement<SeriesType extends Series, FormatterType extends Formatter> {
|
||||
private SeriesBundle<SeriesType, FormatterType> seriesBundle;
|
||||
private boolean isEnabled = true;
|
||||
|
||||
public StackElement(SeriesBundle<SeriesType, FormatterType> seriesBundle) {
|
||||
set(seriesBundle);
|
||||
}
|
||||
|
||||
|
||||
public SeriesBundle<SeriesType, FormatterType> get() {
|
||||
return seriesBundle;
|
||||
}
|
||||
|
||||
public void set(SeriesBundle<SeriesType, FormatterType> seriesBundle) {
|
||||
this.seriesBundle = seriesBundle;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable a stack element for rendering. Has no effect on StackElements that
|
||||
* have already been rendered.
|
||||
* @param isEnabled
|
||||
*/
|
||||
public void setEnabled(boolean isEnabled) {
|
||||
this.isEnabled = isEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
public RenderStack(Plot plot) {
|
||||
this.plot = plot;
|
||||
elements = new ArrayList<>(plot.getRegistry().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs the stack structure with plot's current state. Should be called before
|
||||
* rendering series data to an XYGraphWidget.
|
||||
*/
|
||||
public void sync() {
|
||||
getElements().clear();
|
||||
List<SeriesBundle<SeriesType, FormatterType>> pairList
|
||||
= plot.getRegistry().getSeriesAndFormatterList();
|
||||
for(SeriesBundle<SeriesType, FormatterType> thisPair: pairList) {
|
||||
getElements().add(new StackElement<>(thisPair));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link RenderStack.StackElement#setEnabled(boolean)} with a value
|
||||
* of false on all stack elements associated with the specified renderer.
|
||||
* @param rendererClass
|
||||
*/
|
||||
public void disable(Class<? extends SeriesRenderer> rendererClass) {
|
||||
for(RenderStack.StackElement element : getElements()) {
|
||||
if(element.get().getFormatter().getRendererClass() == rendererClass) {
|
||||
element.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import com.androidplot.util.DisplayDimensions;
|
||||
|
||||
/**
|
||||
* Used by classes that depend on dimensional values to lay themselves out and draw.
|
||||
* Consideration should be given to synchronizing with any draw routines that also
|
||||
* exist within the class.
|
||||
*/
|
||||
public interface Resizable {
|
||||
|
||||
/**
|
||||
* Called when a change to the class' dimensions is made. This method is responsible
|
||||
* for cascading calls to update for any logical children of this class, for example
|
||||
* the Plot class is responsible for updating the LayoutManager. Note that while dims
|
||||
* is marked final in this interface, the compiler will not enforce it. Implementors of
|
||||
* this method should take care not to make changes to dims as this will affect parent
|
||||
* Resizables in likely undesired ways.
|
||||
* @param dims
|
||||
*/
|
||||
void layout(final DisplayDimensions dims);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import com.androidplot.Series;
|
||||
|
||||
/**
|
||||
* Defines a relationship between a Series instance and other elements needed to unique render that instance
|
||||
* such as a Formatter etc.
|
||||
*/
|
||||
public class SeriesBundle<SeriesType extends Series, FormatterType extends Formatter> {
|
||||
|
||||
private final SeriesType series;
|
||||
private final FormatterType formatter;
|
||||
|
||||
public SeriesBundle(SeriesType series, FormatterType formatter) {
|
||||
this.series = series;
|
||||
this.formatter = formatter;
|
||||
}
|
||||
|
||||
public SeriesType getSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
public FormatterType getFormatter() {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
public boolean rendersWith(SeriesRenderer renderer) {
|
||||
return getFormatter().getRendererClass() == renderer.getClass();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import com.androidplot.Series;
|
||||
import com.androidplot.Plot;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class SeriesRenderer
|
||||
<PlotType extends Plot, SeriesType extends Series, SeriesFormatterType extends Formatter> {
|
||||
|
||||
private PlotType plot;
|
||||
|
||||
public SeriesRenderer(PlotType plot) {
|
||||
this.plot = plot;
|
||||
}
|
||||
|
||||
public PlotType getPlot() {
|
||||
return plot;
|
||||
}
|
||||
|
||||
public void setPlot(PlotType plot) {
|
||||
this.plot = plot;
|
||||
}
|
||||
|
||||
public SeriesFormatterType getFormatter(SeriesType series) {
|
||||
return (SeriesFormatterType) plot.getFormatter(series, getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param canvas
|
||||
* @param plotArea
|
||||
* @param sfPair The series / formatter pair to be rendered
|
||||
*/
|
||||
public void render(Canvas canvas, RectF plotArea, SeriesBundle<SeriesType,
|
||||
SeriesFormatterType> sfPair, RenderStack stack) {
|
||||
onRender(canvas, plotArea, sfPair.getSeries(), sfPair.getFormatter(), stack);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param canvas
|
||||
* @param plotArea
|
||||
* @param series The series to be rendered
|
||||
* @param formatter The getFormatter that should be used to render the series
|
||||
* @param stack Ordered list of all series being renderered. May be manipulated by the Renderer
|
||||
* to gain effect.
|
||||
*/
|
||||
protected abstract void onRender(Canvas canvas, RectF plotArea, SeriesType series,
|
||||
SeriesFormatterType formatter, RenderStack stack);
|
||||
|
||||
/**
|
||||
* Draw the legend icon in the rect passed in.
|
||||
* @param canvas
|
||||
* @param rect
|
||||
*/
|
||||
protected abstract void doDrawLegendIcon(Canvas canvas, RectF rect, SeriesFormatterType formatter);
|
||||
|
||||
public void drawSeriesLegendIcon(Canvas canvas, RectF rect, SeriesFormatterType formatter) {
|
||||
try {
|
||||
canvas.save();
|
||||
canvas.clipRect(rect, Region.Op.INTERSECT);
|
||||
doDrawLegendIcon(canvas, rect, formatter);
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return A List of all {@link SeriesBundle} instances currently associated
|
||||
* with this Renderer.
|
||||
*/
|
||||
public List<SeriesBundle<SeriesType, ? extends SeriesFormatterType>> getSeriesAndFormatterList() {
|
||||
List<SeriesBundle<SeriesType, ? extends SeriesFormatterType>> results = new ArrayList<>();
|
||||
List<SeriesBundle> sfList = getPlot().getRegistry().getSeriesAndFormatterList();
|
||||
for(SeriesBundle<SeriesType, ? extends SeriesFormatterType> thisPair : sfList) {
|
||||
if(thisPair.rendersWith(this)) {
|
||||
results.add(thisPair);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public List<SeriesType> getSeriesList() {
|
||||
List<SeriesType> results = new ArrayList<>();
|
||||
List<SeriesBundle> sfList = getPlot().getRegistry().getSeriesAndFormatterList();
|
||||
|
||||
for(SeriesBundle<SeriesType, ? extends SeriesFormatterType> thisPair : sfList) {
|
||||
if(thisPair.rendersWith(this)) {
|
||||
results.add(thisPair.getSeries());
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.graphics.RectF;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
/**
|
||||
* Defines physical dimensions & scaling characteristics
|
||||
*/
|
||||
public class Size {
|
||||
|
||||
// convenience value; sets size to 100% width and height of the widget container.
|
||||
public static Size FILL = new Size(0, SizeMode.FILL, 0, SizeMode.FILL);
|
||||
|
||||
private SizeMetric height;
|
||||
private SizeMetric width;
|
||||
|
||||
/**
|
||||
* Convenience constructor. Wraps {@link #Size(SizeMetric, SizeMetric)}.
|
||||
* @param height Height value used algorithm to calculate the height of the associated widget(s).
|
||||
* @param heightLayoutType Algorithm used to calculate the height of the associated widget(s).
|
||||
* @param width Width value used algorithm to calculate the width of the associated widget(s).
|
||||
* @param widthLayoutType Algorithm used to calculate the width of the associated widget(s).
|
||||
*/
|
||||
public Size(float height, SizeMode heightLayoutType, float width, SizeMode widthLayoutType) {
|
||||
this.height = new SizeMetric(height, heightLayoutType);
|
||||
this.width = new SizeMetric(width, widthLayoutType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SizeMetrics instance using the specified size layout algorithm and value.
|
||||
* See {@link SizeMetric} for details on what can be passed in.
|
||||
* @param height
|
||||
* @param width
|
||||
*/
|
||||
public Size(SizeMetric height, SizeMetric width) {
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public SizeMetric getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(SizeMetric height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public SizeMetric getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a RectF with calculated width and height. The top-left corner is set to 0,0.
|
||||
* @param canvasRect
|
||||
* @return
|
||||
*/
|
||||
public RectF getRectF(RectF canvasRect) {
|
||||
return new RectF(
|
||||
0,
|
||||
0,
|
||||
width.getPixelValue(canvasRect.width()),
|
||||
height.getPixelValue(canvasRect.height()));
|
||||
}
|
||||
|
||||
public void setWidth(SizeMetric width) {
|
||||
this.width = width;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
/**
|
||||
* Encapsulates a sizing algorithm and an associated value.
|
||||
*
|
||||
* The available algorithms list are stored in the {@link SizeMode} enumeration.
|
||||
*
|
||||
*/
|
||||
public class SizeMetric extends LayoutMetric<SizeMode> {
|
||||
|
||||
public SizeMetric(float value, SizeMode layoutType) {
|
||||
super(value, layoutType);
|
||||
}
|
||||
|
||||
protected void validatePair(float value, SizeMode layoutType) {
|
||||
switch(layoutType) {
|
||||
case RELATIVE:
|
||||
if(value < 0 || value > 1) {
|
||||
throw new IllegalArgumentException("SizeMetric Relative and Hybrid layout values must be within the range of 0 to 1.");
|
||||
}
|
||||
case ABSOLUTE:
|
||||
case FILL:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPixelValue(float size) {
|
||||
switch(getLayoutType()) {
|
||||
case ABSOLUTE:
|
||||
return getValue();
|
||||
case RELATIVE:
|
||||
return getValue() * size;
|
||||
case FILL:
|
||||
return size - getValue();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported LayoutType: " + this.getLayoutType());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLayoutType(SizeMode layoutType) {
|
||||
super.setLayoutType(layoutType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
/**
|
||||
* Algorithms available for calculating an arbitrary dimension of a widget.
|
||||
* Each algorithm also takes a single value called "val" in this doc.
|
||||
* ABSOLUTE - Val is treated as absolute. If val is 5 then the size of the widget along the associated axis is 5 pixels.
|
||||
*
|
||||
* RELATIVE - Val represents the percentage of the display that the widget should fill along the associated axis. For example,
|
||||
* if the total size of the owning plot is 120 pixels and val is set to 50 then the size of the widget along the associated axis
|
||||
* is 60; 50% of 120 = 60.
|
||||
*
|
||||
* FILL - Widget completely fills along the associated axis, minus the input size value
|
||||
*/
|
||||
public enum SizeMode {
|
||||
ABSOLUTE,
|
||||
RELATIVE,
|
||||
FILL
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
public abstract class TableModel {
|
||||
private TableOrder order;
|
||||
|
||||
protected TableModel(TableOrder order) {
|
||||
setOrder(order);
|
||||
}
|
||||
|
||||
public abstract Iterator<RectF> getIterator(RectF tableRect, int totalElements);
|
||||
|
||||
//public abstract RectF getCellRect(RectF tableRect, int numElements);
|
||||
|
||||
public TableOrder getOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public void setOrder(TableOrder order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public enum Axis {
|
||||
ROW,
|
||||
COLUMN
|
||||
}
|
||||
|
||||
public enum CellSizingMethod {
|
||||
FIXED,
|
||||
FILL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
public enum TableOrder {
|
||||
ROW_MAJOR, // standard c-style
|
||||
COLUMN_MAJOR
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
/**
|
||||
* The sizing methods available to a table.
|
||||
* AUTO: The table is divided evenly int tableSize/numElements sections.
|
||||
* FIXED: Each element in the table has a predefined number of pixels
|
||||
* regardless of what the table's dimensions actually are.
|
||||
*/
|
||||
public enum TableSizingMethod {
|
||||
AUTO,
|
||||
FIXED
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
public enum TextOrientation {
|
||||
HORIZONTAL,
|
||||
VERTICAL_ASCENDING,
|
||||
VERTICAL_DESCENDING
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
public class VerticalPosition extends PositionMetric<VerticalPositioning> {
|
||||
|
||||
public VerticalPosition(float value, VerticalPositioning layoutStyle) {
|
||||
super(value, layoutStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws IllegalArgumentException if there is a problem.
|
||||
* @param value
|
||||
*/
|
||||
protected void validatePair(float value, VerticalPositioning layoutStyle) {
|
||||
switch(layoutStyle) {
|
||||
case ABSOLUTE_FROM_TOP:
|
||||
case ABSOLUTE_FROM_BOTTOM:
|
||||
case ABSOLUTE_FROM_CENTER:
|
||||
validateValue(value, PositionMetric.LayoutMode.ABSOLUTE);
|
||||
break;
|
||||
case RELATIVE_TO_TOP:
|
||||
case RELATIVE_TO_BOTTOM:
|
||||
case RELATIVE_TO_CENTER:
|
||||
validateValue(value, PositionMetric.LayoutMode.RELATIVE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPixelValue(float size) {
|
||||
switch(getLayoutType()) {
|
||||
case ABSOLUTE_FROM_TOP:
|
||||
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_BEGINING);
|
||||
case ABSOLUTE_FROM_BOTTOM:
|
||||
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_END);
|
||||
case ABSOLUTE_FROM_CENTER:
|
||||
return this.getAbsolutePosition(size, PositionMetric.Origin.FROM_CENTER);
|
||||
case RELATIVE_TO_TOP:
|
||||
return this.getRelativePosition(size, PositionMetric.Origin.FROM_BEGINING);
|
||||
case RELATIVE_TO_BOTTOM:
|
||||
return this.getRelativePosition(size, PositionMetric.Origin.FROM_END);
|
||||
case RELATIVE_TO_CENTER:
|
||||
return this.getRelativePosition(size, PositionMetric.Origin.FROM_CENTER);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported LayoutType: " + this.getLayoutType());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLayoutType(VerticalPositioning layoutType) {
|
||||
super.setLayoutType(layoutType);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui;
|
||||
|
||||
public enum VerticalPositioning {
|
||||
ABSOLUTE_FROM_TOP,
|
||||
ABSOLUTE_FROM_BOTTOM,
|
||||
ABSOLUTE_FROM_CENTER,
|
||||
RELATIVE_TO_TOP,
|
||||
RELATIVE_TO_BOTTOM,
|
||||
RELATIVE_TO_CENTER
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.androidplot.ui.widget;
|
||||
|
||||
/**
|
||||
* An item to be displayed by {@link LegendWidget}.
|
||||
*/
|
||||
public interface LegendItem {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The user facing label for this item.
|
||||
*/
|
||||
String getTitle();
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package com.androidplot.ui.widget;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.androidplot.ui.LayoutManager;
|
||||
import com.androidplot.ui.Size;
|
||||
import com.androidplot.ui.TableModel;
|
||||
import com.androidplot.util.FontUtils;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides core functionality for displaying a legend widget within a {@link com.androidplot.Plot}.
|
||||
* @param <ItemT>
|
||||
*/
|
||||
public abstract class LegendWidget<ItemT extends LegendItem> extends Widget {
|
||||
|
||||
private static final float DEFAULT_TEXT_SIZE_DP = 20;
|
||||
|
||||
private TableModel tableModel;
|
||||
private Size iconSize;
|
||||
|
||||
private Paint textPaint;
|
||||
private Paint iconBackgroundPaint;
|
||||
private Paint iconBorderPaint;
|
||||
|
||||
private boolean drawIconBackgroundEnabled = true;
|
||||
private boolean drawIconBorderEnabled = true;
|
||||
|
||||
private Comparator<ItemT> legendItemComparator;
|
||||
|
||||
{
|
||||
textPaint = new Paint();
|
||||
textPaint.setColor(Color.LTGRAY);
|
||||
textPaint.setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_DP));
|
||||
textPaint.setAntiAlias(true);
|
||||
|
||||
iconBackgroundPaint = new Paint();
|
||||
iconBackgroundPaint.setColor(Color.BLACK);
|
||||
|
||||
iconBorderPaint = new Paint();
|
||||
iconBorderPaint.setColor(Color.TRANSPARENT);
|
||||
iconBorderPaint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
|
||||
|
||||
public LegendWidget(@NonNull TableModel tableModel, @NonNull LayoutManager layoutManager,
|
||||
@NonNull Size size, @NonNull Size iconSize) {
|
||||
super(layoutManager, size);
|
||||
setTableModel(tableModel);
|
||||
this.iconSize = iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doOnDraw(Canvas canvas, RectF widgetRect) {
|
||||
final List<ItemT> items = getLegendItems();
|
||||
if(legendItemComparator != null) {
|
||||
Collections.sort(items, legendItemComparator);
|
||||
}
|
||||
final Iterator<RectF> cellRectIterator = tableModel.getIterator(widgetRect, items.size());
|
||||
for(ItemT item : items) {
|
||||
final RectF cellRect = cellRectIterator.next();
|
||||
final RectF iconRect = getIconRect(cellRect);
|
||||
beginDrawingCell(canvas, iconRect);
|
||||
drawItem(canvas, iconRect, item);
|
||||
finishDrawingCell(canvas, cellRect, iconRect, item);
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawItem(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item) {
|
||||
drawIcon(canvas, iconRect, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the icon representing the legend item
|
||||
* @param canvas
|
||||
* @param iconRect The space to be occupied by the icon.
|
||||
* @param item
|
||||
*/
|
||||
protected abstract void drawIcon(@NonNull Canvas canvas, @NonNull RectF iconRect, @NonNull ItemT item);
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The list of legend items to be drawn. This is used to calculate table dimensions etc.
|
||||
*/
|
||||
protected abstract List<ItemT> getLegendItems();
|
||||
|
||||
private RectF getIconRect(RectF cellRect) {
|
||||
float cellRectCenterY = cellRect.top + (cellRect.height()/2);
|
||||
RectF iconRect = iconSize.getRectF(cellRect);
|
||||
|
||||
// center the icon rect vertically
|
||||
float centeredIconOriginY = cellRectCenterY - (iconRect.height()/2);
|
||||
iconRect.offsetTo(cellRect.left + 1, centeredIconOriginY);
|
||||
return iconRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Done at the start of rendering a new cell. Whatever is drawn here will be beneath the rest
|
||||
* of the cell content; typically used to draw backgrounds.
|
||||
* @param canvas
|
||||
* @param iconRect
|
||||
*/
|
||||
protected void beginDrawingCell(Canvas canvas, RectF iconRect) {
|
||||
|
||||
if(drawIconBackgroundEnabled && iconBackgroundPaint != null) {
|
||||
canvas.drawRect(iconRect, iconBackgroundPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Done at the end of rendering a new cell. Whatever is drawn here will be on top of
|
||||
* the rest of the cell content; typically used to draw borders and text.
|
||||
* @param canvas
|
||||
* @param cellRect
|
||||
* @param iconRect
|
||||
* @param legendItem
|
||||
*/
|
||||
protected void finishDrawingCell(Canvas canvas, RectF cellRect, RectF iconRect, LegendItem legendItem) {
|
||||
|
||||
if(drawIconBorderEnabled && iconBorderPaint != null) {
|
||||
canvas.drawRect(iconRect, iconBorderPaint);
|
||||
}
|
||||
|
||||
float centeredTextOriginY = getRectCenterY(cellRect) + (FontUtils.getFontHeight(textPaint)/2);
|
||||
|
||||
if (textPaint.getTextAlign().equals(Paint.Align.RIGHT)) {
|
||||
canvas.drawText(legendItem.getTitle(), iconRect.left - 2, centeredTextOriginY, textPaint);
|
||||
} else {
|
||||
canvas.drawText(legendItem.getTitle(), iconRect.right + 2, centeredTextOriginY, textPaint);
|
||||
}
|
||||
}
|
||||
|
||||
protected static float getRectCenterY(RectF cellRect) {
|
||||
return cellRect.top + (cellRect.height()/2);
|
||||
}
|
||||
|
||||
public synchronized void setTableModel(TableModel tableModel) {
|
||||
this.tableModel = tableModel;
|
||||
}
|
||||
|
||||
public Paint getTextPaint() {
|
||||
return textPaint;
|
||||
}
|
||||
|
||||
public void setTextPaint(Paint textPaint) {
|
||||
this.textPaint = textPaint;
|
||||
}
|
||||
|
||||
public boolean isDrawIconBackgroundEnabled() {
|
||||
return drawIconBackgroundEnabled;
|
||||
}
|
||||
|
||||
public void setDrawIconBackgroundEnabled(boolean drawIconBackgroundEnabled) {
|
||||
this.drawIconBackgroundEnabled = drawIconBackgroundEnabled;
|
||||
}
|
||||
|
||||
public boolean isDrawIconBorderEnabled() {
|
||||
return drawIconBorderEnabled;
|
||||
}
|
||||
|
||||
public void setDrawIconBorderEnabled(boolean drawIconBorderEnabled) {
|
||||
this.drawIconBorderEnabled = drawIconBorderEnabled;
|
||||
}
|
||||
|
||||
public Size getIconSize() {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
public void setIconSize(Size iconSize) {
|
||||
this.iconSize = iconSize;
|
||||
}
|
||||
|
||||
public Comparator<ItemT> getLegendItemComparator() {
|
||||
return legendItemComparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a scheme for sorting the display order or legend items. By default no sorting is applied
|
||||
* and {@link com.androidplot.Series} items typically appear in the order which the series was
|
||||
* added to the {@link com.androidplot.Plot}.
|
||||
* @param legendItemComparator
|
||||
*/
|
||||
public void setLegendItemComparator(Comparator<ItemT> legendItemComparator) {
|
||||
this.legendItemComparator = legendItemComparator;
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui.widget;
|
||||
|
||||
import android.graphics.*;
|
||||
import com.androidplot.ui.*;
|
||||
import com.androidplot.util.FontUtils;
|
||||
|
||||
public class TextLabelWidget extends Widget {
|
||||
private String text;
|
||||
private Paint labelPaint;
|
||||
private TextOrientation orientation;
|
||||
private boolean autoPackEnabled = true;
|
||||
|
||||
{
|
||||
labelPaint = new Paint();
|
||||
labelPaint.setColor(Color.WHITE);
|
||||
labelPaint.setAntiAlias(true);
|
||||
labelPaint.setTextAlign(Paint.Align.CENTER);
|
||||
setClippingEnabled(false);
|
||||
}
|
||||
|
||||
public TextLabelWidget(LayoutManager layoutManager, Size size) {
|
||||
this(layoutManager, size, TextOrientation.HORIZONTAL);
|
||||
}
|
||||
|
||||
public TextLabelWidget(LayoutManager layoutManager, String title, Size size, TextOrientation orientation) {
|
||||
this(layoutManager, size, orientation);
|
||||
setText(title);
|
||||
}
|
||||
|
||||
public TextLabelWidget(LayoutManager layoutManager, Size size, TextOrientation orientation) {
|
||||
super(layoutManager, new Size(0, SizeMode.ABSOLUTE, 0, SizeMode.ABSOLUTE));
|
||||
setSize(size);
|
||||
this.orientation = orientation;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMetricsChanged(Size olds, Size news) {
|
||||
if(autoPackEnabled) {
|
||||
pack();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostInit() {
|
||||
if(autoPackEnabled) {
|
||||
pack();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dimensions of the widget to exactly contain the text contents
|
||||
*/
|
||||
public void pack() {
|
||||
Rect size = FontUtils.getStringDimensions(text, getLabelPaint());
|
||||
if(size == null) {
|
||||
return;
|
||||
}
|
||||
switch(orientation) {
|
||||
case HORIZONTAL:
|
||||
setSize(new Size(size.height(), SizeMode.ABSOLUTE, size.width()+2, SizeMode.ABSOLUTE));
|
||||
break;
|
||||
case VERTICAL_ASCENDING:
|
||||
case VERTICAL_DESCENDING:
|
||||
setSize(new Size(size.width(), SizeMode.ABSOLUTE, size.height()+2, SizeMode.ABSOLUTE));
|
||||
break;
|
||||
}
|
||||
refreshLayout();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not call this method directly. It is indirectly invoked every time a plot is
|
||||
* redrawn.
|
||||
* @param canvas The Canvas to draw onto
|
||||
* @param widgetRect the size and coordinates of this widget
|
||||
*/
|
||||
@Override
|
||||
public void doOnDraw(Canvas canvas, RectF widgetRect) {
|
||||
if(text == null || text.length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
float vOffset = labelPaint.getFontMetrics().descent;
|
||||
PointF start = getAnchorCoordinates(widgetRect,
|
||||
Anchor.CENTER);
|
||||
|
||||
try {
|
||||
canvas.save();
|
||||
canvas.translate(start.x, start.y);
|
||||
switch (orientation) {
|
||||
case HORIZONTAL:
|
||||
break;
|
||||
case VERTICAL_ASCENDING:
|
||||
canvas.rotate(-90);
|
||||
break;
|
||||
case VERTICAL_DESCENDING:
|
||||
canvas.rotate(90);
|
||||
break;
|
||||
default:
|
||||
|
||||
throw new UnsupportedOperationException("Orientation " + orientation + " not yet implemented for TextLabelWidget.");
|
||||
}
|
||||
canvas.drawText(text, 0, vOffset, labelPaint);
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
public Paint getLabelPaint() {
|
||||
return labelPaint;
|
||||
}
|
||||
|
||||
public void setLabelPaint(Paint labelPaint) {
|
||||
this.labelPaint = labelPaint;
|
||||
|
||||
// when paint changes, packing params change too so run
|
||||
// to see if we need to resize:
|
||||
if(autoPackEnabled) {
|
||||
pack();
|
||||
}
|
||||
}
|
||||
|
||||
public TextOrientation getOrientation() {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
public void setOrientation(TextOrientation orientation) {
|
||||
this.orientation = orientation;
|
||||
if(autoPackEnabled) {
|
||||
pack();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAutoPackEnabled() {
|
||||
return autoPackEnabled;
|
||||
}
|
||||
|
||||
public void setAutoPackEnabled(boolean autoPackEnabled) {
|
||||
this.autoPackEnabled = autoPackEnabled;
|
||||
if(autoPackEnabled) {
|
||||
pack();
|
||||
}
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
if(autoPackEnabled) {
|
||||
pack();
|
||||
}
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.ui.widget;
|
||||
|
||||
import android.graphics.*;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.androidplot.ui.*;
|
||||
import com.androidplot.util.DisplayDimensions;
|
||||
import com.androidplot.ui.HorizontalPositioning;
|
||||
import com.androidplot.ui.VerticalPositioning;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
/**
|
||||
* A Widget is a graphical sub-element of a Plot that can be positioned relative
|
||||
* to the bounds of the Plot.
|
||||
*/
|
||||
public abstract class Widget implements BoxModelable, Resizable {
|
||||
|
||||
private Paint borderPaint;
|
||||
private Paint backgroundPaint;
|
||||
private boolean clippingEnabled = false;
|
||||
private BoxModel boxModel = new BoxModel();
|
||||
private Size size;
|
||||
private DisplayDimensions plotDimensions = new DisplayDimensions();
|
||||
private DisplayDimensions widgetDimensions = new DisplayDimensions();
|
||||
private boolean isVisible = true;
|
||||
private PositionMetrics positionMetrics;
|
||||
private LayoutManager layoutManager;
|
||||
|
||||
private Rotation rotation = Rotation.NONE;
|
||||
private RectF lastWidgetRect = null;
|
||||
|
||||
public enum Rotation {
|
||||
NINETY_DEGREES,
|
||||
NEGATIVE_NINETY_DEGREES,
|
||||
ONE_HUNDRED_EIGHTY_DEGREES,
|
||||
NONE,
|
||||
}
|
||||
|
||||
public Widget(LayoutManager layoutManager, SizeMetric heightMetric, SizeMetric widthMetric) {
|
||||
this(layoutManager, new Size(heightMetric, widthMetric));
|
||||
}
|
||||
|
||||
public Widget(LayoutManager layoutManager, Size size) {
|
||||
this.layoutManager = layoutManager;
|
||||
Size oldSize = this.size;
|
||||
setSize(size);
|
||||
onMetricsChanged(oldSize, size);
|
||||
}
|
||||
|
||||
public DisplayDimensions getWidgetDimensions() {
|
||||
return widgetDimensions;
|
||||
}
|
||||
|
||||
public Anchor getAnchor() {
|
||||
return getPositionMetrics().getAnchor();
|
||||
}
|
||||
|
||||
public void setAnchor(Anchor anchor) {
|
||||
getPositionMetrics().setAnchor(anchor);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Same as {@link #position(float, HorizontalPositioning, float, VerticalPositioning, Anchor)}
|
||||
* but with the anchor parameter defaulted to the upper left corner.
|
||||
*
|
||||
* @param x
|
||||
* @param horizontalPositioning
|
||||
* @param y
|
||||
* @param verticalPositioning
|
||||
*/
|
||||
public void position(float x, HorizontalPositioning horizontalPositioning, float y, VerticalPositioning verticalPositioning) {
|
||||
position(x, horizontalPositioning, y, verticalPositioning, Anchor.LEFT_TOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x X-Coordinate of the top left corner of element. When using RELATIVE, must be a value between 0 and 1.
|
||||
* @param horizontalPositioning LayoutType to use when orienting this element's X-Coordinate.
|
||||
* @param y Y_VALS_ONLY-Coordinate of the top-left corner of element. When using RELATIVE, must be a value between 0 and 1.
|
||||
* @param verticalPositioning LayoutType to use when orienting this element's Y_VALS_ONLY-Coordinate.
|
||||
* @param anchor The point of reference used by this positioning call.
|
||||
*/
|
||||
public void position(float x, HorizontalPositioning horizontalPositioning, float y,
|
||||
VerticalPositioning verticalPositioning, Anchor anchor) {
|
||||
setPositionMetrics(new PositionMetrics(x, horizontalPositioning, y, verticalPositioning, anchor));
|
||||
layoutManager.addToTop(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overridden by subclasses to respond to resizing events.
|
||||
*
|
||||
* @param oldSize
|
||||
* @param newSize
|
||||
*/
|
||||
protected void onMetricsChanged(Size oldSize, Size newSize) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overridden by subclasses to handle any final resizing etc. that
|
||||
* can only be done after XML configuration etc. has completed.
|
||||
*/
|
||||
public void onPostInit() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not point lies within this Widget.
|
||||
*
|
||||
* @param point
|
||||
* @return
|
||||
*/
|
||||
public boolean containsPoint(PointF point) {
|
||||
return widgetDimensions.canvasRect.contains(point.x, point.y);
|
||||
}
|
||||
|
||||
public void setSize(Size size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public Size getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
|
||||
public void setWidth(float width) {
|
||||
size.getWidth().setValue(width);
|
||||
}
|
||||
|
||||
public void setWidth(float width, SizeMode layoutType) {
|
||||
size.getWidth().set(width, layoutType);
|
||||
}
|
||||
|
||||
public void setHeight(float height) {
|
||||
size.getHeight().setValue(height);
|
||||
}
|
||||
|
||||
public void setHeight(float height, SizeMode layoutType) {
|
||||
size.getHeight().set(height, layoutType);
|
||||
}
|
||||
|
||||
public SizeMetric getWidthMetric() {
|
||||
return size.getWidth();
|
||||
}
|
||||
|
||||
public SizeMetric getHeightMetric() {
|
||||
return size.getHeight();
|
||||
}
|
||||
|
||||
public float getWidthPix(float size) {
|
||||
return this.size.getWidth().getPixelValue(size);
|
||||
}
|
||||
|
||||
public float getHeightPix(float size) {
|
||||
return this.size.getHeight().getPixelValue(size);
|
||||
}
|
||||
|
||||
public RectF getMarginatedRect(RectF widgetRect) {
|
||||
return boxModel.getMarginatedRect(widgetRect);
|
||||
}
|
||||
|
||||
public RectF getPaddedRect(RectF widgetMarginRect) {
|
||||
return boxModel.getPaddedRect(widgetMarginRect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarginRight(float marginRight) {
|
||||
boxModel.setMarginRight(marginRight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMargins(float left, float top, float right, float bottom) {
|
||||
boxModel.setMargins(left, top, right, bottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPadding(float left, float top, float right, float bottom) {
|
||||
boxModel.setPadding(left, top, right, bottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMarginTop() {
|
||||
return boxModel.getMarginTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarginTop(float marginTop) {
|
||||
boxModel.setMarginTop(marginTop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMarginBottom() {
|
||||
return boxModel.getMarginBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPaddingLeft() {
|
||||
return boxModel.getPaddingLeft();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPaddingLeft(float paddingLeft) {
|
||||
boxModel.setPaddingLeft(paddingLeft);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPaddingTop() {
|
||||
return boxModel.getPaddingTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPaddingTop(float paddingTop) {
|
||||
boxModel.setPaddingTop(paddingTop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPaddingRight() {
|
||||
return boxModel.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPaddingRight(float paddingRight) {
|
||||
boxModel.setPaddingRight(paddingRight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPaddingBottom() {
|
||||
return boxModel.getPaddingBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPaddingBottom(float paddingBottom) {
|
||||
boxModel.setPaddingBottom(paddingBottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
public void setMarginBottom(float marginBottom) {
|
||||
boxModel.setMarginBottom(marginBottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMarginLeft() {
|
||||
return boxModel.getMarginLeft();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarginLeft(float marginLeft) {
|
||||
boxModel.setMarginLeft(marginLeft);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMarginRight() {
|
||||
return boxModel.getMarginRight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the pixel dimensions used for rendering this Widget
|
||||
* to be recalculated. Should be called any time a parameter that factors
|
||||
* into this Widget's size or position is altered.
|
||||
*/
|
||||
public synchronized void refreshLayout() {
|
||||
if (positionMetrics == null) {
|
||||
// make sure positionMetrics have been set. this method can be
|
||||
// automatically called during xml configuration of certain params
|
||||
// before the widget is fully configured.
|
||||
return;
|
||||
}
|
||||
float elementWidth = getWidthPix(plotDimensions.paddedRect.width());
|
||||
float elementHeight = getHeightPix(plotDimensions.paddedRect.height());
|
||||
PointF coords = calculateCoordinates(elementHeight,
|
||||
elementWidth, plotDimensions.paddedRect, positionMetrics);
|
||||
|
||||
RectF widgetRect = new RectF(coords.x, coords.y,
|
||||
coords.x + elementWidth, coords.y + elementHeight);
|
||||
RectF marginatedWidgetRect = getMarginatedRect(widgetRect);
|
||||
RectF paddedWidgetRect = getPaddedRect(marginatedWidgetRect);
|
||||
widgetDimensions = new DisplayDimensions(widgetRect,
|
||||
marginatedWidgetRect, paddedWidgetRect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void layout(final DisplayDimensions plotDimensions) {
|
||||
this.plotDimensions = plotDimensions;
|
||||
refreshLayout();
|
||||
}
|
||||
|
||||
|
||||
public static PointF calculateCoordinates(float height, float width, RectF viewRect, PositionMetrics metrics) {
|
||||
float x = metrics.getXPositionMetric().getPixelValue(viewRect.width()) + viewRect.left;
|
||||
float y = metrics.getYPositionMetric().getPixelValue(viewRect.height()) + viewRect.top;
|
||||
PointF point = new PointF(x, y);
|
||||
return PixelUtils.sub(point, getAnchorOffset(width, height, metrics.getAnchor()));
|
||||
}
|
||||
|
||||
public static PointF getAnchorOffset(float width, float height, Anchor anchor) {
|
||||
PointF point = new PointF();
|
||||
switch (anchor) {
|
||||
case LEFT_TOP:
|
||||
break;
|
||||
case LEFT_MIDDLE:
|
||||
point.set(0, height / 2);
|
||||
break;
|
||||
case LEFT_BOTTOM:
|
||||
point.set(0, height);
|
||||
break;
|
||||
case RIGHT_TOP:
|
||||
point.set(width, 0);
|
||||
break;
|
||||
case RIGHT_BOTTOM:
|
||||
point.set(width, height);
|
||||
break;
|
||||
case RIGHT_MIDDLE:
|
||||
point.set(width, height / 2);
|
||||
break;
|
||||
case TOP_MIDDLE:
|
||||
point.set(width / 2, 0);
|
||||
break;
|
||||
case BOTTOM_MIDDLE:
|
||||
point.set(width / 2, height);
|
||||
break;
|
||||
case CENTER:
|
||||
point.set(width / 2, height / 2);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported anchor location: " + anchor);
|
||||
}
|
||||
return point;
|
||||
}
|
||||
|
||||
public static PointF getAnchorCoordinates(RectF widgetRect, Anchor anchor) {
|
||||
return PixelUtils.add(new PointF(widgetRect.left, widgetRect.top),
|
||||
getAnchorOffset(widgetRect.width(), widgetRect.height(), anchor));
|
||||
}
|
||||
|
||||
public static PointF getAnchorCoordinates(float x, float y, float width, float height, Anchor anchor) {
|
||||
return getAnchorCoordinates(new RectF(x, y, x + width, y + height), anchor);
|
||||
}
|
||||
|
||||
private void checkSize(@NonNull RectF widgetRect) {
|
||||
if (lastWidgetRect == null || !lastWidgetRect.equals(widgetRect)) {
|
||||
onResize(lastWidgetRect, widgetRect);
|
||||
}
|
||||
lastWidgetRect = widgetRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever the height or width of the Widget's reserved space has changed,
|
||||
* immediately before {@link #doOnDraw(Canvas, RectF)}.
|
||||
* May be used to efficiently carry out expensive operations only when necessary.
|
||||
*
|
||||
* @param oldRect
|
||||
* @param newRect
|
||||
*/
|
||||
protected void onResize(@Nullable RectF oldRect, @NonNull RectF newRect) {
|
||||
// do nothing by default
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
if (isVisible()) {
|
||||
if (backgroundPaint != null) {
|
||||
drawBackground(canvas, widgetDimensions.canvasRect);
|
||||
}
|
||||
canvas.save();
|
||||
final RectF widgetRect = applyRotation(canvas, widgetDimensions.paddedRect);
|
||||
checkSize(widgetRect);
|
||||
doOnDraw(canvas, widgetRect);
|
||||
canvas.restore();
|
||||
|
||||
if (borderPaint != null) {
|
||||
drawBorder(canvas, widgetRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected RectF applyRotation(Canvas canvas, RectF rect) {
|
||||
float rotationDegs = 0;
|
||||
final float cx = widgetDimensions.paddedRect.centerX();
|
||||
final float cy = widgetDimensions.paddedRect.centerY();
|
||||
final float halfHeight = widgetDimensions.paddedRect.height() / 2;
|
||||
final float halfWidth = widgetDimensions.paddedRect.width() / 2;
|
||||
switch (rotation) {
|
||||
case NINETY_DEGREES:
|
||||
rotationDegs = 90;
|
||||
rect = new RectF(
|
||||
cx - halfHeight,
|
||||
cy - halfWidth,
|
||||
cx + halfHeight,
|
||||
cy + halfWidth);
|
||||
break;
|
||||
case NEGATIVE_NINETY_DEGREES:
|
||||
rotationDegs = -90;
|
||||
rect = new RectF(
|
||||
cx - halfHeight,
|
||||
cy - halfWidth,
|
||||
cx + halfHeight,
|
||||
cy + halfWidth);
|
||||
break;
|
||||
case ONE_HUNDRED_EIGHTY_DEGREES:
|
||||
rotationDegs = 180;
|
||||
// fall through
|
||||
case NONE:
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Not yet implemented.");
|
||||
|
||||
}
|
||||
if (rotation != Rotation.NONE) {
|
||||
canvas.rotate(rotationDegs, cx, cy);
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
protected void drawBorder(Canvas canvas, RectF paddedRect) {
|
||||
canvas.drawRect(paddedRect, borderPaint);
|
||||
}
|
||||
|
||||
protected void drawBackground(Canvas canvas, RectF widgetRect) {
|
||||
canvas.drawRect(widgetRect, backgroundPaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param canvas The Canvas to draw onto
|
||||
* @param widgetRect the size and coordinates of this widget
|
||||
*/
|
||||
protected abstract void doOnDraw(Canvas canvas, RectF widgetRect);
|
||||
|
||||
public Paint getBorderPaint() {
|
||||
return borderPaint;
|
||||
}
|
||||
|
||||
public void setBorderPaint(Paint borderPaint) {
|
||||
this.borderPaint = borderPaint;
|
||||
}
|
||||
|
||||
public Paint getBackgroundPaint() {
|
||||
return backgroundPaint;
|
||||
}
|
||||
|
||||
public void setBackgroundPaint(Paint backgroundPaint) {
|
||||
this.backgroundPaint = backgroundPaint;
|
||||
}
|
||||
|
||||
public boolean isClippingEnabled() {
|
||||
return clippingEnabled;
|
||||
}
|
||||
|
||||
public void setClippingEnabled(boolean clippingEnabled) {
|
||||
this.clippingEnabled = clippingEnabled;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
isVisible = visible;
|
||||
}
|
||||
|
||||
public PositionMetrics getPositionMetrics() {
|
||||
return positionMetrics;
|
||||
}
|
||||
|
||||
public void setPositionMetrics(PositionMetrics positionMetrics) {
|
||||
this.positionMetrics = positionMetrics;
|
||||
}
|
||||
|
||||
public Rotation getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public void setRotation(Rotation rotation) {
|
||||
this.rotation = rotation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.androidplot.util;
|
||||
|
||||
import android.os.*;
|
||||
|
||||
/**
|
||||
* Wraps {@link Trace} to provide API-safe methods as well as an easy target for runtime removal
|
||||
* via obfuscation.
|
||||
*/
|
||||
public abstract class APTrace {
|
||||
|
||||
public static void begin(final String sectionName) {
|
||||
if(Build.VERSION.SDK_INT >= 18) {
|
||||
Trace.beginSection(sectionName);
|
||||
} else {
|
||||
// TODO: alternate impl?
|
||||
}
|
||||
}
|
||||
|
||||
public static void end() {
|
||||
if(Build.VERSION.SDK_INT >= 18) {
|
||||
Trace.endSection();
|
||||
} else {
|
||||
// TODO: alternate impl?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Paint;
|
||||
import android.util.*;
|
||||
|
||||
import com.androidplot.ui.*;
|
||||
import com.androidplot.ui.Size;
|
||||
import com.androidplot.ui.widget.Widget;
|
||||
import com.androidplot.xy.StepMode;
|
||||
import com.androidplot.xy.StepModel;
|
||||
|
||||
/**
|
||||
* Methods for applying styleable attributes.
|
||||
*
|
||||
*/
|
||||
public class AttrUtils {
|
||||
|
||||
private static final String TAG = AttrUtils.class.getName();
|
||||
|
||||
public static void configureInsets(TypedArray attrs, Insets insets,
|
||||
int topAttr, int bottomAttr, int leftAttr, int rightAttr) {
|
||||
insets.setTop(attrs.getDimension(topAttr, insets.getTop()));
|
||||
insets.setBottom(attrs.getDimension(bottomAttr, insets.getBottom()));
|
||||
insets.setLeft(attrs.getDimension(leftAttr, insets.getLeft()));
|
||||
insets.setRight(attrs.getDimension(rightAttr, insets.getRight()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link Paint} instance used for drawing text from xml attrs.
|
||||
* @param attrs
|
||||
* @param paint
|
||||
* @param colorAttr
|
||||
* @param textSizeAttr
|
||||
*/
|
||||
public static void configureTextPaint(TypedArray attrs, Paint paint,
|
||||
int colorAttr, int textSizeAttr) {
|
||||
configureTextPaint(attrs, paint, colorAttr, textSizeAttr, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link Paint} instance used for drawing text from xml attrs.
|
||||
* @param attrs
|
||||
* @param paint
|
||||
* @param colorAttr
|
||||
* @param textSizeAttr
|
||||
* @param alignAttr
|
||||
*/
|
||||
public static void configureTextPaint(TypedArray attrs, Paint paint, int colorAttr,
|
||||
int textSizeAttr, Integer alignAttr) {
|
||||
if(attrs != null) {
|
||||
setColor(attrs, paint, colorAttr);
|
||||
setTextSize(attrs, paint, textSizeAttr);
|
||||
|
||||
if(alignAttr != null && attrs.hasValue(alignAttr)) {
|
||||
configureTextAlign(attrs, paint, alignAttr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure {@link Paint} text alignment from xml attrs.
|
||||
* @param attrs
|
||||
* @param paint
|
||||
* @param alignAttr
|
||||
*/
|
||||
public static void configureTextAlign(TypedArray attrs, Paint paint, int alignAttr) {
|
||||
if (attrs != null) {
|
||||
//if(attrs.hasValue(alignAttr)) {
|
||||
final Paint.Align alignment = Paint.Align.values()
|
||||
[attrs.getInt(alignAttr, paint.getTextAlign().ordinal())];
|
||||
paint.setTextAlign(alignment);
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link Paint} instance used for drawing lines from xml attrs.
|
||||
* @param attrs
|
||||
* @param paint
|
||||
* @param colorAttr
|
||||
* @param strokeWidthAttr
|
||||
*/
|
||||
public static void configureLinePaint(TypedArray attrs, Paint paint, int colorAttr, int strokeWidthAttr) {
|
||||
if(attrs != null) {
|
||||
setColor(attrs, paint, colorAttr);
|
||||
paint.setStrokeWidth(attrs.getDimension(strokeWidthAttr, paint.getStrokeWidth()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void setColor(TypedArray attrs, Paint paint, int attrId) {
|
||||
if(paint == null) {
|
||||
Log.w(TAG, "Attempt to configure null Paint property for attrId: " + attrId);
|
||||
} else {
|
||||
paint.setColor(attrs.getColor(attrId, paint.getColor()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void setTextSize(TypedArray attrs, Paint paint, int attrId) {
|
||||
paint.setTextSize(attrs.getDimension(attrId, paint.getTextSize()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link BoxModelable} instance from xml attrs.
|
||||
* @param attrs
|
||||
* @param model
|
||||
* @param marginTop
|
||||
* @param marginBottom
|
||||
* @param marginLeft
|
||||
* @param marginRight
|
||||
* @param paddingTop
|
||||
* @param paddingBottom
|
||||
* @param paddingLeft
|
||||
* @param paddingRight
|
||||
*/
|
||||
public static void configureBoxModelable(TypedArray attrs, BoxModelable model, int marginTop, int marginBottom,
|
||||
int marginLeft, int marginRight, int paddingTop, int paddingBottom,
|
||||
int paddingLeft, int paddingRight) {
|
||||
if(attrs != null) {
|
||||
model.setMargins(attrs.getDimension(marginLeft, model.getMarginLeft()),
|
||||
attrs.getDimension(marginTop, model.getMarginTop()),
|
||||
attrs.getDimension(marginRight, model.getMarginRight()),
|
||||
attrs.getDimension(marginBottom, model.getMarginBottom()));
|
||||
|
||||
model.setPadding(attrs.getDimension(paddingLeft, model.getPaddingLeft()),
|
||||
attrs.getDimension(paddingTop, model.getPaddingTop()),
|
||||
attrs.getDimension(paddingRight, model.getPaddingRight()),
|
||||
attrs.getDimension(paddingBottom, model.getPaddingBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link Size} instance from xml attrs.
|
||||
* @param attrs
|
||||
* @param model
|
||||
* @param heightSizeLayoutTypeAttr
|
||||
* @param heightAttr
|
||||
* @param widthSizeLayoutTypeAttr
|
||||
* @param widthAttr
|
||||
*/
|
||||
public static void configureSize(TypedArray attrs, Size model, int heightSizeLayoutTypeAttr, int heightAttr,
|
||||
int widthSizeLayoutTypeAttr, int widthAttr) {
|
||||
if(attrs != null) {
|
||||
configureSizeMetric(attrs, model.getHeight(), heightSizeLayoutTypeAttr, heightAttr);
|
||||
configureSizeMetric(attrs, model.getWidth(), widthSizeLayoutTypeAttr, widthAttr);
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureSizeMetric(TypedArray attrs, SizeMetric model, int typeAttr, int valueAttr) {
|
||||
|
||||
final float value = getIntFloatDimenValue(attrs, valueAttr, model.getValue()).floatValue();
|
||||
final SizeMode sizeMode =
|
||||
getSizeLayoutType(attrs, typeAttr, model.getLayoutType());
|
||||
|
||||
model.set(value, sizeMode);
|
||||
}
|
||||
|
||||
private static SizeMode getSizeLayoutType(TypedArray attrs, int attr, SizeMode defaultValue) {
|
||||
return SizeMode.values()[attrs.getInt(attr, defaultValue.ordinal())];
|
||||
}
|
||||
|
||||
public static void configureWidget(TypedArray attrs, Widget widget, int heightSizeLayoutTypeAttr, int heightAttr,
|
||||
int widthSizeLayoutTypeAttr, int widthAttr, int xLayoutStyleAttr,
|
||||
int xLayoutValueAttr, int yLayoutStyleAttr, int yLayoutValueAttr,
|
||||
int anchorPositionAttr, int visibilityAttr) {
|
||||
if(attrs != null) {
|
||||
configureSize(attrs, widget.getSize(), heightSizeLayoutTypeAttr,
|
||||
heightAttr, widthSizeLayoutTypeAttr, widthAttr);
|
||||
configurePositionMetrics(attrs, widget.getPositionMetrics(), xLayoutStyleAttr, xLayoutValueAttr,
|
||||
yLayoutStyleAttr, yLayoutValueAttr, anchorPositionAttr);
|
||||
widget.setVisible(attrs.getBoolean(visibilityAttr, widget.isVisible()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void configureWidgetRotation(TypedArray attrs, Widget widget, int rotationAttr) {
|
||||
if(attrs != null) {
|
||||
widget.setRotation(getWidgetRotation(attrs, rotationAttr, Widget.Rotation.NONE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link Widget} from xml attrs.
|
||||
* @param attrs
|
||||
* @param metrics
|
||||
* @param xLayoutStyleAttr
|
||||
* @param xLayoutValueAttr
|
||||
* @param yLayoutStyleAttr
|
||||
* @param yLayoutValueAttr
|
||||
* @param anchorPositionAttr
|
||||
*/
|
||||
public static void configurePositionMetrics(TypedArray attrs, PositionMetrics metrics, int xLayoutStyleAttr,
|
||||
int xLayoutValueAttr, int yLayoutStyleAttr, int yLayoutValueAttr,
|
||||
int anchorPositionAttr) {
|
||||
if(attrs != null && metrics != null) {
|
||||
metrics.getXPositionMetric().set(
|
||||
getIntFloatDimenValue(attrs, xLayoutValueAttr, metrics.getXPositionMetric().getValue()).floatValue(),
|
||||
getXLayoutStyle(attrs, xLayoutStyleAttr, metrics.getXPositionMetric().getLayoutType()));
|
||||
|
||||
metrics.getYPositionMetric().set(
|
||||
getIntFloatDimenValue(attrs, yLayoutValueAttr, metrics.getYPositionMetric().getValue()).floatValue(),
|
||||
getYLayoutStyle(attrs, yLayoutStyleAttr, metrics.getYPositionMetric().getLayoutType()));
|
||||
metrics.setAnchor(getAnchorPosition(attrs, anchorPositionAttr, metrics.getAnchor()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to retrieve values from xml that can be entered as a int, float or dimen.
|
||||
* @param attrs
|
||||
* @param valueAttr
|
||||
* @param defaultValue
|
||||
* @return
|
||||
*/
|
||||
private static Number getIntFloatDimenValue(TypedArray attrs, int valueAttr, Number defaultValue) {
|
||||
Number result = defaultValue;
|
||||
if(attrs != null && attrs.hasValue(valueAttr)) {
|
||||
final int valueType = attrs.peekValue(valueAttr).type;
|
||||
if (valueType == TypedValue.TYPE_DIMENSION) {
|
||||
result = attrs.getDimension(valueAttr, defaultValue.floatValue());
|
||||
} else if(valueType == TypedValue.TYPE_INT_DEC) {
|
||||
result = attrs.getInt(valueAttr, defaultValue.intValue());
|
||||
} else if (valueType == TypedValue.TYPE_FLOAT) {
|
||||
result = attrs.getFloat(valueAttr, defaultValue.floatValue());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid value type - must be int, float or dimension.");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static HorizontalPositioning getXLayoutStyle(TypedArray attrs, int attr, HorizontalPositioning defaultValue) {
|
||||
return HorizontalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())];
|
||||
}
|
||||
|
||||
private static VerticalPositioning getYLayoutStyle(TypedArray attrs, int attr, VerticalPositioning defaultValue) {
|
||||
return VerticalPositioning.values()[attrs.getInt(attr, defaultValue.ordinal())];
|
||||
}
|
||||
|
||||
private static Widget.Rotation getWidgetRotation(TypedArray attrs, int attr, Widget.Rotation defaultValue) {
|
||||
return Widget.Rotation.values()[attrs.getInt(attr, defaultValue.ordinal())];
|
||||
}
|
||||
|
||||
private static Anchor getAnchorPosition(TypedArray attrs, int attr, Anchor defaultValue) {
|
||||
return Anchor.values()[attrs.getInt(attr, defaultValue.ordinal())];
|
||||
}
|
||||
|
||||
public static void configureStep(TypedArray attrs, StepModel model, int stepModeAttr, int stepValueAttr) {
|
||||
if(attrs != null) {
|
||||
model.setMode(StepMode.values()[attrs.getInt(stepModeAttr, model.getMode().ordinal())]);
|
||||
model.setValue(getIntFloatDimenValue(attrs, stepValueAttr, model.getValue()).doubleValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* Convenience class for managing {@link com.androidplot.ui.BoxModel} data
|
||||
*/
|
||||
public class DisplayDimensions {
|
||||
|
||||
private static final int ONE = 1;
|
||||
|
||||
public final RectF canvasRect;
|
||||
public final RectF marginatedRect;
|
||||
public final RectF paddedRect;
|
||||
|
||||
// init to 1 to avoid potential divide by zero errors (yet to be observed in practice)
|
||||
private static final RectF initRect;
|
||||
|
||||
static {
|
||||
initRect = new RectF(ONE, ONE, ONE, ONE);
|
||||
}
|
||||
|
||||
public DisplayDimensions() {
|
||||
this(initRect, initRect, initRect);
|
||||
}
|
||||
public DisplayDimensions(RectF canvasRect, RectF marginatedRect, RectF paddedRect) {
|
||||
this.canvasRect = canvasRect;
|
||||
this.marginatedRect = marginatedRect;
|
||||
this.paddedRect = paddedRect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.androidplot.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* An extension of {@link Number} optimized for speed at the cost of memory.
|
||||
*/
|
||||
public class FastNumber extends Number {
|
||||
|
||||
@NonNull private final Number number;
|
||||
private boolean hasDoublePrimitive;
|
||||
private boolean hasFloatPrimitive;
|
||||
private boolean hasIntPrimitive;
|
||||
|
||||
private double doublePrimitive;
|
||||
private float floatPrimitive;
|
||||
private int intPrimitive;
|
||||
|
||||
/**
|
||||
* Safe-instantiator of FastNumber; returns a null result if the input Number is also null.
|
||||
* @param number
|
||||
* @return
|
||||
*/
|
||||
public static FastNumber orNull(@NonNull Number number) {
|
||||
if(number == null) {
|
||||
return null;
|
||||
} else {
|
||||
return new FastNumber(number);
|
||||
}
|
||||
}
|
||||
|
||||
private FastNumber(@NonNull Number number) {
|
||||
|
||||
//noinspection ConstantConditions //in case someone ignores the @NonNull annotation
|
||||
if (number == null) {
|
||||
throw new IllegalArgumentException("number parameter cannot be null");
|
||||
}
|
||||
|
||||
// avoid nested instances of FastNumber :
|
||||
if(number instanceof FastNumber) {
|
||||
FastNumber rhs = (FastNumber) number;
|
||||
this.number = rhs.number;
|
||||
this.hasDoublePrimitive = rhs.hasDoublePrimitive;
|
||||
this.hasFloatPrimitive = rhs.hasFloatPrimitive;
|
||||
this.hasIntPrimitive = rhs.hasIntPrimitive;
|
||||
this.doublePrimitive = rhs.doublePrimitive;
|
||||
this.floatPrimitive = rhs.floatPrimitive;
|
||||
this.intPrimitive = rhs.intPrimitive;
|
||||
} else {
|
||||
this.number = number;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
if(!hasIntPrimitive) {
|
||||
intPrimitive = number.intValue();
|
||||
hasIntPrimitive = true;
|
||||
}
|
||||
return intPrimitive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long longValue() {
|
||||
// TODO: optimize me!
|
||||
return number.longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float floatValue() {
|
||||
if(!hasFloatPrimitive) {
|
||||
floatPrimitive = number.floatValue();
|
||||
hasFloatPrimitive = true;
|
||||
}
|
||||
return floatPrimitive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double doubleValue() {
|
||||
if(!hasDoublePrimitive) {
|
||||
doublePrimitive = number.doubleValue();
|
||||
hasDoublePrimitive = true;
|
||||
}
|
||||
return doublePrimitive;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be equal, two instances must both be instances of {@link FastNumber}. The inner {@link
|
||||
* #number} field must also be a common type. Numbers which are mathematically equal are not
|
||||
* necessarily equal. This keeps with the java implementation of common Number classes where for
|
||||
* instance {@code new Integer(0).equals(new Double(0))} returns {@code false}
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FastNumber that = (FastNumber) o;
|
||||
|
||||
return number.equals(that.number);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return number.hashCode();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.valueOf(doubleValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import android.graphics.*;
|
||||
|
||||
public class FontUtils {
|
||||
|
||||
private static final int ZERO = 0;
|
||||
|
||||
/**
|
||||
* Determines the height of the tallest character that can be drawn by paint.
|
||||
* @param paint
|
||||
* @return
|
||||
*/
|
||||
public static float getFontHeight(Paint paint) {
|
||||
Paint.FontMetrics metrics = paint.getFontMetrics();
|
||||
return (-metrics.ascent) + metrics.descent;
|
||||
//return (-metrics.top) + metrics.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the smallest rect that ecompasses the text to be drawn using paint.
|
||||
* @param text
|
||||
* @param paint
|
||||
* @return
|
||||
*/
|
||||
public static Rect getPackedStringDimensions(String text, Paint paint) {
|
||||
Rect size = new Rect();
|
||||
paint.getTextBounds(text, ZERO, text.length(), size);
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like getPackedStringDimensions except adds extra space to accommodate all
|
||||
* characters that can be drawn regardless of whether or not they exist in text.
|
||||
* This ensures a more uniform appearance for things that have dynamic text.
|
||||
* @param text
|
||||
* @param paint
|
||||
* @return
|
||||
*/
|
||||
public static Rect getStringDimensions(String text, Paint paint) {
|
||||
Rect size = new Rect();
|
||||
if(text == null || text.length() == ZERO) {
|
||||
return null;
|
||||
}
|
||||
paint.getTextBounds(text, ZERO, text.length(), size);
|
||||
size.bottom = size.top + (int) getFontHeight(paint);
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws text vertically centered on the specified coordinates
|
||||
* @param canvas
|
||||
* @param paint
|
||||
* @param text
|
||||
* @param cx
|
||||
* @param cy
|
||||
*/
|
||||
public static void drawTextVerticallyCentered(Canvas canvas, String text, float cx, float cy, Paint paint) {
|
||||
Rect textBounds = new Rect();
|
||||
paint.getTextBounds(text, 0, text.length(), textBounds);
|
||||
canvas.drawText(text, cx, cy - textBounds.exactCenterY(), paint);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An implementation of {@link Layerable}. Provides fast element retrieval via hash key in addition to
|
||||
* mutable ordering (z indexing) of elements.
|
||||
*/
|
||||
public class LayerHash<KeyType, ValueType> implements Layerable<KeyType> {
|
||||
|
||||
private HashMap<KeyType, ValueType> hash;
|
||||
private LinkedLayerList<KeyType> zlist;
|
||||
|
||||
{
|
||||
hash = new HashMap<>();
|
||||
zlist = new LinkedLayerList<>();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return zlist.size();
|
||||
}
|
||||
|
||||
|
||||
public ValueType get(KeyType key) {
|
||||
return hash.get(key);
|
||||
}
|
||||
|
||||
public List<KeyType> getKeysAsList() {
|
||||
return zlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* If key already exists within the structure, it's value is replaced with the new value and
|
||||
* it's existing order is maintained.
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
public synchronized void addToTop(KeyType key, ValueType value) {
|
||||
if(hash.containsKey(key)) {
|
||||
hash.put(key, value);
|
||||
} else {
|
||||
hash.put(key, value);
|
||||
zlist.addToTop(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If key already exists within the structure, it's value is replaced with the new value and
|
||||
* it's existing order is maintained.
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
public synchronized void addToBottom(KeyType key, ValueType value) {
|
||||
if(hash.containsKey(key)) {
|
||||
hash.put(key, value);
|
||||
} else {
|
||||
hash.put(key, value);
|
||||
zlist.addToBottom(key);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean moveToTop(KeyType element) {
|
||||
if(!hash.containsKey(element)) {
|
||||
return false;
|
||||
} else {
|
||||
return zlist.moveToTop(element);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean moveAbove(KeyType objectToMove, KeyType reference) {
|
||||
if(objectToMove == reference) {
|
||||
throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B.");
|
||||
}
|
||||
if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) {
|
||||
return false;
|
||||
} else {
|
||||
return zlist.moveAbove(objectToMove, reference);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean moveBeneath(KeyType objectToMove, KeyType reference) {
|
||||
if(objectToMove == reference) {
|
||||
throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B.");
|
||||
}
|
||||
if(!hash.containsKey(reference) || !hash.containsKey(objectToMove)) {
|
||||
return false;
|
||||
} else {
|
||||
return zlist.moveBeneath(objectToMove, reference);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean moveToBottom(KeyType key) {
|
||||
if(!hash.containsKey(key)) {
|
||||
return false;
|
||||
} else {
|
||||
return zlist.moveToBottom(key);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean moveUp(KeyType key) {
|
||||
if (!hash.containsKey(key)) {
|
||||
return false;
|
||||
} else {
|
||||
return zlist.moveUp(key);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean moveDown(KeyType key) {
|
||||
if (!hash.containsKey(key)) {
|
||||
return false;
|
||||
} else {
|
||||
return zlist.moveDown(key);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyType> elements() {
|
||||
return zlist;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return Ordered list of keys.
|
||||
*/
|
||||
public List<KeyType> keys() {
|
||||
return elements();
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean remove(KeyType key) {
|
||||
if(hash.containsKey(key)) {
|
||||
hash.remove(key);
|
||||
zlist.remove(key);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public ValueType getTop() {
|
||||
return hash.get(zlist.getLast());
|
||||
}
|
||||
|
||||
public ValueType getBottom() {
|
||||
return hash.get(zlist.getFirst());
|
||||
}
|
||||
|
||||
public ValueType getAbove(KeyType key) {
|
||||
final int index = zlist.indexOf(key);
|
||||
if(index >= 0 && index < size() - 1) {
|
||||
return hash.get(zlist.get(index + 1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ValueType getBeneath(KeyType key) {
|
||||
final int index = zlist.indexOf(key);
|
||||
if(index > 0) {
|
||||
return hash.get(zlist.get(index - 1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility class providing additional element organization operations.
|
||||
* @param <ElementType>
|
||||
*/
|
||||
public class LayerListOrganizer<ElementType> implements Layerable<ElementType> {
|
||||
|
||||
private static final int ZERO = 0;
|
||||
private static final int ONE = 1;
|
||||
|
||||
private List<ElementType> list;
|
||||
|
||||
public LayerListOrganizer(List<ElementType> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
|
||||
public boolean moveToTop(ElementType element) {
|
||||
if(list.remove(element)) {
|
||||
list.add(list.size(), element);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean moveAbove(ElementType objectToMove, ElementType reference) {
|
||||
if(objectToMove == reference) {
|
||||
throw new IllegalArgumentException("Illegal argument to moveAbove(A, B); A cannot be equal to B.");
|
||||
}
|
||||
|
||||
|
||||
list.remove(objectToMove);
|
||||
int refIndex = list.indexOf(reference);
|
||||
list.add(refIndex + ONE, objectToMove);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean moveBeneath(ElementType objectToMove, ElementType reference) {
|
||||
if (objectToMove == reference) {
|
||||
throw new IllegalArgumentException("Illegal argument to moveBeaneath(A, B); A cannot be equal to B.");
|
||||
}
|
||||
|
||||
list.remove(objectToMove);
|
||||
int refIndex = list.indexOf(reference);
|
||||
list.add(refIndex, objectToMove);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public boolean moveToBottom(ElementType key) {
|
||||
list.remove(key);
|
||||
list.add(ZERO, key);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean moveUp(ElementType key) {
|
||||
int widgetIndex = list.indexOf(key);
|
||||
if(widgetIndex == - ONE) {
|
||||
// key not found:
|
||||
return false;
|
||||
}
|
||||
if(widgetIndex >= list.size() - ONE) {
|
||||
// already at the top:
|
||||
return true;
|
||||
}
|
||||
|
||||
ElementType widgetAbove = list.get(widgetIndex + ONE);
|
||||
return moveAbove(key, widgetAbove);
|
||||
}
|
||||
|
||||
public boolean moveDown(ElementType key) {
|
||||
int widgetIndex = list.indexOf(key);
|
||||
if(widgetIndex == - ONE) {
|
||||
// key not found:
|
||||
return false;
|
||||
}
|
||||
if(widgetIndex <= ZERO) {
|
||||
// already at the bottom:
|
||||
return true;
|
||||
}
|
||||
|
||||
ElementType widgetBeneath = list.get(widgetIndex - ONE);
|
||||
return moveBeneath(key, widgetBeneath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ElementType> elements() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public void addToBottom(ElementType element) {
|
||||
list.add(ZERO, element);
|
||||
}
|
||||
|
||||
public void addToTop(ElementType element) {
|
||||
list.add(list.size(), element);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Encapsulates the concept of "layerable" objects; Each object is stored above or below each other object and may
|
||||
* be moved up and down in the queue relative to other elements in the hash or absolutely to the front or back of the queue.
|
||||
*
|
||||
* Note that the method names correspond to the order of items drawn directly on top of one another using an iterator;
|
||||
* the first element drawn (lowest layer) is effectively the "bottom" element.
|
||||
* @param <ElementType>
|
||||
*/
|
||||
public interface Layerable<ElementType> {
|
||||
|
||||
/**
|
||||
* Move above all other elements
|
||||
* @param element
|
||||
* @return
|
||||
*/
|
||||
boolean moveToTop(ElementType element);
|
||||
|
||||
|
||||
/**
|
||||
* Move above the specified element
|
||||
* @param objectToMove
|
||||
* @param reference
|
||||
* @return
|
||||
*/
|
||||
boolean moveAbove(ElementType objectToMove, ElementType reference);
|
||||
|
||||
|
||||
/**
|
||||
* Move beneath the specified element
|
||||
*
|
||||
* @param objectToMove
|
||||
* @param reference
|
||||
* @return
|
||||
*/
|
||||
boolean moveBeneath(ElementType objectToMove, ElementType reference);
|
||||
|
||||
/**
|
||||
* Move beneath all other elements
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
boolean moveToBottom(ElementType key);
|
||||
|
||||
|
||||
/**
|
||||
* Move up by one element
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
boolean moveUp(ElementType key);
|
||||
|
||||
/**
|
||||
* Move down by one element
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
boolean moveDown(ElementType key);
|
||||
|
||||
List<ElementType> elements();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A implementation of {@link Layerable} backed by a {@link LinkedList}.
|
||||
* @param <Type>
|
||||
*/
|
||||
public class LinkedLayerList<Type> extends LinkedList<Type> implements Layerable<Type> {
|
||||
|
||||
private LayerListOrganizer<Type> organizer = new LayerListOrganizer<>(this);
|
||||
|
||||
@Override
|
||||
public boolean moveToTop(Type element) {
|
||||
return organizer.moveToTop(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveAbove(Type objectToMove, Type reference) {
|
||||
return organizer.moveAbove(objectToMove, reference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveBeneath(Type objectToMove, Type reference) {
|
||||
return organizer.moveBeneath(objectToMove, reference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToBottom(Type key) {
|
||||
return organizer.moveToBottom(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveUp(Type key) {
|
||||
return organizer.moveUp(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveDown(Type key) {
|
||||
return organizer.moveDown(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Type> elements() {
|
||||
return organizer.elements();
|
||||
}
|
||||
|
||||
public void addToBottom(Type element) {
|
||||
organizer.addToBottom(element);
|
||||
}
|
||||
|
||||
public void addToTop(Type element) {
|
||||
organizer.addToTop(element);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
/**
|
||||
* Essentially just a version of the Map interface used to associate
|
||||
* a key of a given type with a value of a given type, does impose a 1:1
|
||||
* relationship between keys and values and defines no method for insertion or deletion.
|
||||
*/
|
||||
public interface Mapping<Key, Value> {
|
||||
|
||||
/**
|
||||
* @param value
|
||||
* @return The Key associated with the specified value.
|
||||
*/
|
||||
Key get(Value value);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PixelUtils {
|
||||
private static DisplayMetrics metrics;
|
||||
|
||||
/**
|
||||
* Recalculates scale value etc. Should be called when an application starts or
|
||||
* whenever the screen is rotated.
|
||||
*/
|
||||
public static void init(Context ctx) {
|
||||
metrics = ctx.getResources().getDisplayMetrics();
|
||||
}
|
||||
|
||||
public static PointF add(PointF lhs, PointF rhs) {
|
||||
return new PointF(lhs.x + rhs.x, lhs.y + rhs.y);
|
||||
}
|
||||
|
||||
public static PointF sub(PointF lhs, PointF rhs) {
|
||||
return new PointF(lhs.x - rhs.x, lhs.y - rhs.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a dp value to pixels.
|
||||
* @param dp
|
||||
* @return Pixel value of dp.
|
||||
*/
|
||||
public static float dpToPix(float dp) {
|
||||
checkMetrics();
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an sp value to pixels.
|
||||
* @param sp
|
||||
* @return Pixel value of sp.
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
public static float spToPix(float sp) {
|
||||
checkMetrics();
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* CODE BELOW IS ADAPTED IN PART FROM MINDRIOT'S SAMPLE CODE HERE:
|
||||
* http://stackoverflow.com/questions/8343971/how-to-parse-a-dimension-string-and-convert-it-to-a-dimension-value
|
||||
*/
|
||||
// Initialize dimension string to constant lookup.
|
||||
public static final Map<String, Integer> dimensionConstantLookup = initDimensionConstantLookup();
|
||||
|
||||
private static Map<String, Integer> initDimensionConstantLookup() {
|
||||
Map<String, Integer> m = new HashMap<String, Integer>();
|
||||
m.put("px", TypedValue.COMPLEX_UNIT_PX);
|
||||
m.put("dip", TypedValue.COMPLEX_UNIT_DIP);
|
||||
m.put("dp", TypedValue.COMPLEX_UNIT_DIP);
|
||||
m.put("sp", TypedValue.COMPLEX_UNIT_SP);
|
||||
m.put("pt", TypedValue.COMPLEX_UNIT_PT);
|
||||
m.put("in", TypedValue.COMPLEX_UNIT_IN);
|
||||
m.put("mm", TypedValue.COMPLEX_UNIT_MM);
|
||||
return Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
// Initialize pattern for dimension string.
|
||||
protected static final String DIMENSION_REGEX = "^\\-?\\s*(\\d+(\\.\\d+)*)\\s*([a-zA-Z]+)\\s*$";
|
||||
protected static final Pattern DIMENSION_PATTERN = Pattern.compile(DIMENSION_REGEX);
|
||||
|
||||
public static float stringToDimension(String dimension) {
|
||||
// Mimics TypedValue.complexToDimension(int data, DisplayMetrics metrics).
|
||||
InternalDimension internalDimension = stringToInternalDimension(dimension);
|
||||
return TypedValue.applyDimension(internalDimension.unit, internalDimension.value, metrics);
|
||||
}
|
||||
|
||||
private static InternalDimension stringToInternalDimension(String dimension) {
|
||||
// Match target against pattern.
|
||||
Matcher matcher = DIMENSION_PATTERN.matcher(dimension);
|
||||
if (matcher.matches()) {
|
||||
// Match found.
|
||||
// Extract value.
|
||||
float value = Float.valueOf(matcher.group(1));
|
||||
// Extract dimension units.
|
||||
String unit = matcher.group(3).toLowerCase();
|
||||
// Get Android dimension constant.
|
||||
Integer dimensionUnit = dimensionConstantLookup.get(unit);
|
||||
if (dimensionUnit == null) {
|
||||
// Invalid format.
|
||||
throw new NumberFormatException();
|
||||
} else {
|
||||
// Return valid dimension.
|
||||
return new InternalDimension(value, dimensionUnit);
|
||||
}
|
||||
} else {
|
||||
// Invalid format.
|
||||
throw new NumberFormatException();
|
||||
}
|
||||
}
|
||||
|
||||
private static class InternalDimension {
|
||||
float value;
|
||||
int unit;
|
||||
|
||||
public InternalDimension(float value, int unit) {
|
||||
this.value = value;
|
||||
this.unit = unit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safety run to hopefully help clarify what could otherwise be a confusing NPE.
|
||||
*/
|
||||
private static void checkMetrics() {
|
||||
if(metrics == null) {
|
||||
throw new RuntimeException("PixelUtils not initialized; call PixelUtils.init(Context) before using.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import com.androidplot.Plot;
|
||||
import com.androidplot.PlotListener;
|
||||
|
||||
/**
|
||||
* !!! THIS CLASS IS STILL UNDER DEVELOPMENT AND MAY CONTAIN BUGS !!!
|
||||
* Gathers performance statistics from a Plot. Instances of PlotStatistics
|
||||
* should never be added to more than one Plot, otherwise the statiscs will
|
||||
* be invalid.
|
||||
*/
|
||||
public class PlotStatistics implements PlotListener {
|
||||
long updateDelayMs;
|
||||
long longestRenderMs = 0;
|
||||
long shortestRenderMs = 0;
|
||||
long lastStart = 0;
|
||||
long lastLatency = 0;
|
||||
long lastAnnotation;
|
||||
long latencySamples = 0;
|
||||
long latencySum = 0;
|
||||
String annotationString = "";
|
||||
private boolean annotatePlotEnabled;
|
||||
|
||||
private Paint paint;
|
||||
{
|
||||
paint = new Paint();
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setTextSize(30);
|
||||
resetCounters();
|
||||
}
|
||||
|
||||
public PlotStatistics(long updateDelayMs, boolean annotatePlotEnabled) {
|
||||
this.updateDelayMs = updateDelayMs;
|
||||
this.annotatePlotEnabled = annotatePlotEnabled;
|
||||
}
|
||||
|
||||
public void setAnnotatePlotEnabled(boolean enabled) {
|
||||
this.annotatePlotEnabled = enabled;
|
||||
}
|
||||
|
||||
private void resetCounters() {
|
||||
longestRenderMs = 0;
|
||||
shortestRenderMs = 999999999;
|
||||
latencySamples = 0;
|
||||
latencySum = 0;
|
||||
}
|
||||
|
||||
private void annotatePlot(Plot source, Canvas canvas) {
|
||||
long nowMs = System.currentTimeMillis();
|
||||
// throttle the update frequency:
|
||||
long msSinceUpdate = (nowMs - lastAnnotation);
|
||||
if(msSinceUpdate >= updateDelayMs) {
|
||||
|
||||
float avgLatency = latencySamples > 0 ? latencySum/latencySamples : 0;
|
||||
String overallFPS = String.format("%.2f", latencySamples > 0 ? (1000f/msSinceUpdate) * latencySamples : 0);
|
||||
String potentialFPS = String.format("%.2f", latencySamples > 0 ? 1000f/avgLatency : 0);
|
||||
annotationString = "FPS (potential): " + potentialFPS + " FPS (actual): " + overallFPS + " Latency (ms) Avg: " + lastLatency + " \nMin: " + shortestRenderMs +
|
||||
" Max: " + longestRenderMs;
|
||||
lastAnnotation = nowMs;
|
||||
resetCounters();
|
||||
}
|
||||
RectF r = source.getDisplayDimensions().canvasRect;
|
||||
if(annotatePlotEnabled) {
|
||||
canvas.drawText(annotationString, r.centerX(), r.centerY(), paint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeDraw(Plot source, Canvas canvas) {
|
||||
lastStart = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAfterDraw(Plot source, Canvas canvas) {
|
||||
lastLatency = System.currentTimeMillis() - lastStart;
|
||||
if(lastLatency < shortestRenderMs) {
|
||||
shortestRenderMs = lastLatency;
|
||||
}
|
||||
|
||||
if(lastLatency > longestRenderMs) {
|
||||
longestRenderMs = lastLatency;
|
||||
}
|
||||
latencySum += lastLatency;
|
||||
latencySamples++;
|
||||
annotatePlot(source, canvas);
|
||||
}
|
||||
|
||||
public void setEnabled(boolean isEnabled) {
|
||||
this.annotatePlotEnabled = isEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
import com.androidplot.ui.*;
|
||||
|
||||
/**
|
||||
* Convenience methods for dealing with {@link android.graphics.RectF}
|
||||
*/
|
||||
public abstract class RectFUtils {
|
||||
|
||||
|
||||
/**
|
||||
* Determine if two {@link RectF} instances are equal. Must be used in place
|
||||
* of default equality operation due to a bug that exists in older versions of Android:
|
||||
* http://stackoverflow.com/questions/13517852/rectf-equals-fails-on-android-versions-below-jelly-bean
|
||||
* @param r1 May not be null
|
||||
* @param r2 May not be null
|
||||
* @return True if r1 and r2 are identical, false otherwise.
|
||||
*/
|
||||
public static boolean areIdentical(RectF r1, RectF r2) {
|
||||
return r1.left == r2.left &&
|
||||
r1.top == r2.top &&
|
||||
r1.right == r2.right &&
|
||||
r1.bottom == r2.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a new {@link RectF} by applying insets to rect.
|
||||
* @param rect
|
||||
* @param insets
|
||||
* @return The {@link RectF} created as a result of applying insets, or the passed in
|
||||
* instance, if the insets were null.
|
||||
*/
|
||||
public static RectF applyInsets(RectF rect, Insets insets) {
|
||||
if (insets != null) {
|
||||
return new RectF(
|
||||
rect.left + insets.getLeft(),
|
||||
rect.top + insets.getTop(),
|
||||
rect.right - insets.getRight(),
|
||||
rect.bottom - insets.getBottom());
|
||||
} else {
|
||||
return rect;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a RectF from two height and two width values; the h and w values will
|
||||
* be passed into the RectF constructor such that RectF.left <= RectF.right and
|
||||
* RectF.top <= RectF.bottom.
|
||||
* @param w1 width1
|
||||
* @param h1 height1
|
||||
* @param w2 width2
|
||||
* @param h2 height2
|
||||
* @return
|
||||
*/
|
||||
public static RectF createFromEdges(float w1, float h1, float w2, float h2) {
|
||||
final boolean w1IsLeft = w1 <= w2;
|
||||
final boolean h1IsTop = h1 <= h2;
|
||||
return new RectF(
|
||||
w1IsLeft ? w1 : w2,
|
||||
h1IsTop ? h1 : h2,
|
||||
w1IsLeft ? w2 : w1,
|
||||
h1IsTop ? h2 : h1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.androidplot.Plot;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility class for invoking Plot.redraw() on a background thread
|
||||
* at a set frequency.
|
||||
*/
|
||||
public class Redrawer implements Runnable {
|
||||
|
||||
private static final int ONE_SECOND_MS = 1000;
|
||||
|
||||
private static final String TAG = Redrawer.class.getName();
|
||||
|
||||
private List<WeakReference<Plot>> plots;
|
||||
private long sleepTime;
|
||||
|
||||
// used to temporarily pause rendering without disposing of the run thread
|
||||
private boolean keepRunning;
|
||||
|
||||
// when set to false, run thread will be allowed to exit the main run loop
|
||||
private boolean keepAlive;
|
||||
|
||||
private Thread thread;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param plots List of Plot instances to be redrawn
|
||||
* @param maxRefreshRate Desired frequency at which to redraw plots.
|
||||
* @param startImmediately If true, invokes run() immediately after construction.
|
||||
*/
|
||||
public Redrawer(List<Plot> plots, float maxRefreshRate, boolean startImmediately) {
|
||||
this.plots = new ArrayList<>(plots.size());
|
||||
for(Plot plot : plots) {
|
||||
this.plots.add(new WeakReference<>(plot));
|
||||
}
|
||||
setMaxRefreshRate(maxRefreshRate);
|
||||
thread = new Thread(this, "Androidplot Redrawer");
|
||||
thread.start();
|
||||
if(startImmediately) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
public Redrawer(Plot plot, float maxRefreshRate, boolean startImmediately) {
|
||||
this(Collections.singletonList(plot), maxRefreshRate, startImmediately);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily stop redrawing the plot.
|
||||
*/
|
||||
public synchronized void pause() {
|
||||
keepRunning = false;
|
||||
notify();
|
||||
Log.d(TAG, "Redrawer paused.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start/resume redrawing the plot.
|
||||
*/
|
||||
public synchronized void start() {
|
||||
keepRunning = true;
|
||||
notify();
|
||||
Log.d(TAG, "Redrawer started.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Internally, this causes
|
||||
* the refresh thread to exit. Should always be called
|
||||
* before exiting the application.
|
||||
*/
|
||||
public synchronized void finish() {
|
||||
keepRunning = false;
|
||||
keepAlive = false;
|
||||
notify();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
keepAlive = true;
|
||||
try {
|
||||
while(keepAlive) {
|
||||
if(keepRunning) {
|
||||
// redraw plot(s) and sleep in an interruptible state for a
|
||||
// max of sleepTime ms.
|
||||
// TODO: record start and end timestamps and
|
||||
// TODO: calculate sleepTime from that, in order to more accurately
|
||||
// TODO: meet desired refresh rate.
|
||||
for(WeakReference<Plot> plotRef : plots) {
|
||||
plotRef.get().redraw();
|
||||
}
|
||||
synchronized (this) {
|
||||
wait(sleepTime);
|
||||
}
|
||||
} else {
|
||||
// sleep until notified
|
||||
synchronized (this) {
|
||||
wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
|
||||
} finally {
|
||||
Log.d(TAG, "Redrawer thread exited.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum refresh rate that Redrawer should use. Actual
|
||||
* refresh rate could be slower.
|
||||
* @param refreshRate Refresh rate in Hz.
|
||||
*/
|
||||
public void setMaxRefreshRate(float refreshRate) {
|
||||
sleepTime = (long)(ONE_SECOND_MS / refreshRate);
|
||||
Log.d(TAG, "Set Redrawer refresh rate to " +
|
||||
refreshRate + "( " + sleepTime + " ms)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.util;
|
||||
|
||||
import com.androidplot.Region;
|
||||
import com.androidplot.xy.FastXYSeries;
|
||||
import com.androidplot.xy.OrderedXYSeries;
|
||||
import com.androidplot.xy.RectRegion;
|
||||
import com.androidplot.xy.XYConstraints;
|
||||
import com.androidplot.xy.XYSeries;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with Series data.
|
||||
*/
|
||||
public class SeriesUtils {
|
||||
|
||||
public static RectRegion minMax(List<XYSeries> seriesList) {
|
||||
return minMax(null, seriesList);
|
||||
}
|
||||
|
||||
public static RectRegion minMax(XYSeries... seriesList) {
|
||||
return minMax(null, seriesList);
|
||||
}
|
||||
|
||||
public static Region minMaxX(XYSeries... seriesList) {
|
||||
final Region bounds = new Region();
|
||||
for (XYSeries series : seriesList) {
|
||||
for (int i = 0; i < series.size(); i++) {
|
||||
bounds.union(series.getX(i));
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public static Region minMaxY(XYSeries... seriesList) {
|
||||
final Region bounds = new Region();
|
||||
for (XYSeries series : seriesList) {
|
||||
for (int i = 0; i < series.size(); i++) {
|
||||
bounds.union(series.getY(i));
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param constraints may be null.
|
||||
* @param seriesList
|
||||
* @return
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public static RectRegion minMax(XYConstraints constraints, List<XYSeries> seriesList) {
|
||||
return minMax(constraints, seriesList.toArray(new XYSeries[seriesList.size()]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param constraints May be null.
|
||||
* @param seriesArray
|
||||
* @return
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public static RectRegion minMax(XYConstraints constraints, XYSeries... seriesArray) {
|
||||
|
||||
final RectRegion bounds = new RectRegion();
|
||||
|
||||
// make sure there is series data to iterate over:
|
||||
if (seriesArray != null && seriesArray.length > 0) {
|
||||
|
||||
// iterate over each series
|
||||
for (XYSeries series : seriesArray) {
|
||||
|
||||
// if this is an advanced xy series then minMax have already been calculated for us:
|
||||
if (series instanceof FastXYSeries) {
|
||||
final RectRegion b = ((FastXYSeries) series).minMax();
|
||||
if(b == null) {
|
||||
//this series doesn't currently have min or max region (might be empty)
|
||||
continue;
|
||||
}
|
||||
if(constraints == null || constraints.contains(b)) {
|
||||
bounds.union(b);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < series.size(); i++) {
|
||||
final Number xi = series.getX(i);
|
||||
final Number yi = series.getY(i);
|
||||
|
||||
// if constraints have been set, make sure this xy coordinate exists within them:
|
||||
if (constraints == null || constraints.contains(xi, yi)) {
|
||||
bounds.union(xi, yi);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param bounds Starting minMax values to work from; only lists values that are greater than or less
|
||||
* than those in bounds will be be used.
|
||||
* @param lists lists to be evaluated for min/max values.
|
||||
* @return the original bounds instance passed in
|
||||
*/
|
||||
public static Region minMax(Region bounds, List<Number>... lists) {
|
||||
for (final List<Number> list : lists) {
|
||||
for (final Number i : list) {
|
||||
bounds.union(i);
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the range of visible i-vals in the specified series. Assumes that x-vals are
|
||||
* in strict ascending order; behavior is undefined otherwise.
|
||||
* @param series
|
||||
* @param visibleBounds The visible constraints of the plot
|
||||
* @return
|
||||
*/
|
||||
public static Region iBounds(XYSeries series, RectRegion visibleBounds) {
|
||||
final float step = series.size() >= 200 ? 50 : 1;
|
||||
final int iBoundsMin = iBoundsMin(series, visibleBounds.getMinX().doubleValue(), step);
|
||||
final int iBoundsMax = iBoundsMax(series, visibleBounds.getMaxX().doubleValue(), step);
|
||||
return new Region(iBoundsMin, iBoundsMax);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing
|
||||
* TODO a binary search algorithm that also supports nulls is not trivial and would not likely
|
||||
* TODO result in any noticeable performance increase here. It's a task for another day!
|
||||
* @param series
|
||||
* @param visibleMax
|
||||
* @param step
|
||||
* @return The index of the smallest non-null value that is greater than visibleMax, or the index
|
||||
* of the last element if no such value exists.
|
||||
*/
|
||||
protected static int iBoundsMax(XYSeries series, double visibleMax, float step) {
|
||||
int max = series.size() - 1;
|
||||
final int seriesSize = series.size();
|
||||
final int steps = (int) Math.ceil(seriesSize / step);
|
||||
for (int stepIndex = steps; stepIndex >= 0; stepIndex--) {
|
||||
final int i = stepIndex * (int) step;
|
||||
for (int ii = 0; ii < step; ii++) {
|
||||
final int iii = i + ii;
|
||||
if(iii < seriesSize) {
|
||||
final Number thisX = series.getX(iii);
|
||||
if (thisX != null) {
|
||||
final double thisDouble = thisX.doubleValue();
|
||||
if (thisDouble > visibleMax) {
|
||||
// this is the smallest non-null value in this block, so skip
|
||||
// to the next block:
|
||||
max = iii;
|
||||
break;
|
||||
} else if (thisDouble == visibleMax) {
|
||||
return iii;
|
||||
} else {
|
||||
return max;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: This is a poor alternative to a true binary search implementation. Unfortunately writing
|
||||
* TODO a binary search algorithm that also supports nulls is not trivial and would not likely
|
||||
* TODO result in any noticeable performance increase here. It's a task for another day!
|
||||
* @param series
|
||||
* @param visibleMin
|
||||
* @param step
|
||||
* @return The index of the largest non-null value that is less than visible, or 0
|
||||
* (the first element index) if no such value exists.
|
||||
*/
|
||||
protected static int iBoundsMin(XYSeries series, double visibleMin, float step) {
|
||||
int min = 0;
|
||||
final int steps = (int) Math.ceil(series.size() / step);
|
||||
for (int stepIndex = 1; stepIndex <= steps; stepIndex++) {
|
||||
final int i = stepIndex * (int) step;
|
||||
for (int ii = 1; ii <= step; ii++) {
|
||||
final int iii = i - ii;
|
||||
if(iii < 0) {
|
||||
break;
|
||||
}
|
||||
if(iii < series.size()) {
|
||||
final Number thisX = series.getX(iii);
|
||||
if (thisX != null) {
|
||||
if (thisX.doubleValue() < visibleMin) {
|
||||
// this is the largest non-null value in this block, so skip
|
||||
// to the next block:
|
||||
min = iii;
|
||||
break;
|
||||
} else if (thisX.doubleValue() == visibleMin) {
|
||||
return iii;
|
||||
} else {
|
||||
return min;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the minMax iVals of the xVals surrounding a range of one or more null values.
|
||||
* @param series
|
||||
* @param index index of the null value in question
|
||||
* @return The iVals of the non-null values surrounding the null range. If the null range is unbounded on
|
||||
* either side then either or both min and max values will also be null.
|
||||
*/
|
||||
protected static Region getNullRegion(XYSeries series, int index) {
|
||||
Region region = new Region();
|
||||
if(series.getX(index) != null) {
|
||||
throw new IllegalArgumentException("Attempt to find null region for non null index: " + index);
|
||||
}
|
||||
for(int i = index - 1; i >= 0; i--) {
|
||||
Number val = series.getX(i);
|
||||
if(val != null) {
|
||||
region.setMin(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = index + 1; i < series.size(); i++) {
|
||||
Number val = series.getX(i);
|
||||
if(val != null) {
|
||||
region.setMax(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param lists
|
||||
* @return
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public static Region minMax(List<Number>... lists) {
|
||||
return minMax(new Region(), lists);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the XVal order of an XYSeries. If series does not implement {@link OrderedXYSeries}
|
||||
* then {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE} is assumed.
|
||||
* @param series
|
||||
* @return The {@link com.androidplot.xy.OrderedXYSeries.XOrder} of the series.
|
||||
*/
|
||||
public static OrderedXYSeries.XOrder getXYOrder(XYSeries series) {
|
||||
return series instanceof OrderedXYSeries ?
|
||||
((OrderedXYSeries) series).getXOrder() : OrderedXYSeries.XOrder.NONE;
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import com.androidplot.ui.RenderStack;
|
||||
import com.androidplot.ui.SeriesRenderer;
|
||||
|
||||
/**
|
||||
* This is an experimental (but stable) implementation of an {@link XYSeriesRenderer} that provides instrumentation
|
||||
* allowing advanced behaviors like dynamically coloring / styling individual segments of a series, etc. This class
|
||||
* may be removed or renamed in future releases.
|
||||
* Currently has the following constraints:
|
||||
* - Interpolation is not supported
|
||||
* - Only draws lines; no points or fill
|
||||
* - Draws series lines using simple Canvas.drawLine(...) invocations.
|
||||
* @since 0.9.9
|
||||
*/
|
||||
public class AdvancedLineAndPointRenderer extends XYSeriesRenderer<XYSeries, AdvancedLineAndPointRenderer.Formatter> {
|
||||
|
||||
private int latestIndex;
|
||||
|
||||
public AdvancedLineAndPointRenderer(XYPlot plot) {
|
||||
super(plot);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) {
|
||||
PointF thisPoint;
|
||||
PointF lastPoint = null;
|
||||
for (int i = 0; i < series.size(); i++) {
|
||||
Number y = series.getY(i);
|
||||
Number x = series.getX(i);
|
||||
|
||||
if (y != null && x != null) {
|
||||
thisPoint = getPlot().getBounds()
|
||||
.transformScreen(x, y, plotArea);
|
||||
} else {
|
||||
thisPoint = null;
|
||||
}
|
||||
|
||||
// don't need to do any of this if the line isnt going to be drawn:
|
||||
if(formatter.getLinePaint() != null) {
|
||||
if (thisPoint != null && lastPoint != null) {
|
||||
canvas.drawLine(lastPoint.x, lastPoint.y, thisPoint.x, thisPoint.y, formatter.getLinePaint(i, latestIndex, series.size()));
|
||||
}
|
||||
}
|
||||
lastPoint = thisPoint;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDrawLegendIcon(Canvas canvas, RectF rect, Formatter formatter) {
|
||||
if(formatter.getLinePaint() != null) {
|
||||
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
|
||||
}
|
||||
}
|
||||
|
||||
public void setLatestIndex(int latestIndex) {
|
||||
this.latestIndex = latestIndex;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formatter designed to work in tandem with {@link AdvancedLineAndPointRenderer}.
|
||||
* @since 0.9.9
|
||||
*/
|
||||
public static class Formatter extends XYSeriesFormatter<XYRegionFormatter> {
|
||||
|
||||
private static final int DEFAULT_STROKE_WIDTH = 3;
|
||||
|
||||
private Paint linePaint;
|
||||
|
||||
public Formatter() {
|
||||
linePaint = new Paint();
|
||||
linePaint.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||
linePaint.setColor(Color.RED);
|
||||
}
|
||||
|
||||
public Formatter(Context context, int xmlConfigId) {
|
||||
this();
|
||||
configure(context, xmlConfigId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends SeriesRenderer> getRendererClass() {
|
||||
return AdvancedLineAndPointRenderer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdvancedLineAndPointRenderer doGetRendererInstance(XYPlot plot) {
|
||||
return new AdvancedLineAndPointRenderer(plot);
|
||||
}
|
||||
|
||||
public Paint getLinePaint() {
|
||||
return linePaint;
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, simply returns the line paint as-is. May be overridden to provide custom behavior based
|
||||
* on input params.
|
||||
* @param thisIndex
|
||||
* @param latestIndex
|
||||
* @param seriesSize
|
||||
* @return
|
||||
*/
|
||||
public Paint getLinePaint(int thisIndex, int latestIndex, int seriesSize) {
|
||||
return getLinePaint();
|
||||
}
|
||||
|
||||
public void setLinePaint(Paint linePaint) {
|
||||
this.linePaint = linePaint;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
public enum Axis {
|
||||
DOMAIN,
|
||||
RANGE
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
import android.content.*;
|
||||
import android.graphics.Paint;
|
||||
import com.androidplot.ui.SeriesRenderer;
|
||||
|
||||
public class BarFormatter extends LineAndPointFormatter {
|
||||
|
||||
public Paint getFillPaint() {
|
||||
return fillPaint;
|
||||
}
|
||||
|
||||
public void setFillPaint(Paint fillPaint) {
|
||||
this.fillPaint = fillPaint;
|
||||
}
|
||||
|
||||
public Paint getBorderPaint() {
|
||||
return borderPaint;
|
||||
}
|
||||
|
||||
public void setBorderPaint(Paint borderPaint) {
|
||||
this.borderPaint = borderPaint;
|
||||
}
|
||||
|
||||
private Paint fillPaint;
|
||||
private Paint borderPaint;
|
||||
|
||||
private float marginTop;
|
||||
private float marginBottom;
|
||||
private float marginLeft;
|
||||
private float marginRight;
|
||||
|
||||
/**
|
||||
* Should only be used in conjunction with calls to configure()...
|
||||
*/
|
||||
public BarFormatter() {
|
||||
fillPaint = new Paint();
|
||||
fillPaint.setStyle(Paint.Style.FILL);
|
||||
fillPaint.setAlpha(100);
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setAlpha(100);
|
||||
}
|
||||
|
||||
public BarFormatter(int fillColor, int borderColor) {
|
||||
this();
|
||||
fillPaint.setColor(fillColor);
|
||||
borderPaint.setColor(borderColor);
|
||||
}
|
||||
|
||||
public BarFormatter(Context context, int xmlCfgId) {
|
||||
this();
|
||||
configure(context, xmlCfgId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends SeriesRenderer> getRendererClass() {
|
||||
return BarRenderer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
|
||||
return new BarRenderer(plot);
|
||||
}
|
||||
|
||||
public float getMarginTop() {
|
||||
return marginTop;
|
||||
}
|
||||
|
||||
public void setMarginTop(float marginTop) {
|
||||
this.marginTop = marginTop;
|
||||
}
|
||||
|
||||
public float getMarginBottom() {
|
||||
return marginBottom;
|
||||
}
|
||||
|
||||
public void setMarginBottom(float marginBottom) {
|
||||
this.marginBottom = marginBottom;
|
||||
}
|
||||
|
||||
public float getMarginLeft() {
|
||||
return marginLeft;
|
||||
}
|
||||
|
||||
public void setMarginLeft(float marginLeft) {
|
||||
this.marginLeft = marginLeft;
|
||||
}
|
||||
|
||||
public float getMarginRight() {
|
||||
return marginRight;
|
||||
}
|
||||
|
||||
public void setMarginRight(float marginRight) {
|
||||
this.marginRight = marginRight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import com.androidplot.ui.RenderStack;
|
||||
import com.androidplot.ui.SeriesBundle;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
import com.androidplot.util.RectFUtils;
|
||||
|
||||
/**
|
||||
* Renders the points in an XYSeries as bars. See {@link BarOrientation} javadoc for details on supported
|
||||
* presentation styles.
|
||||
*
|
||||
*/
|
||||
public class BarRenderer<FormatterType extends BarFormatter> extends GroupRenderer<FormatterType> {
|
||||
|
||||
private BarOrientation barOrientation = BarOrientation.OVERLAID; // default Render Style
|
||||
private BarGroupWidthMode barGroupWidthMode = BarGroupWidthMode.FIXED_WIDTH; // default Width Style
|
||||
|
||||
/**
|
||||
* Represents the size in pixels of either bar width or bar gap width, depending on the current
|
||||
* value of barWidthMode.
|
||||
*/
|
||||
private float width = PixelUtils.dpToPix(3);
|
||||
|
||||
/**
|
||||
* How bars should be laid out when in a group of 2 or more series.
|
||||
*/
|
||||
public enum BarOrientation {
|
||||
|
||||
/**
|
||||
* Bars are drawn overlapping one another, in the order their respective series
|
||||
* was added to the plot.
|
||||
*/
|
||||
IN_ORDER,
|
||||
|
||||
/**
|
||||
* Bars are drawn overlapping one another, with taller bars being drawn behind
|
||||
* the shorter ones.
|
||||
*/
|
||||
OVERLAID, // bars are overlaid in descending y-val order (largest val in back)
|
||||
|
||||
/**
|
||||
* Bars are stacked on top of one another so that the sum of their yVals produces the final
|
||||
* height of that bar.
|
||||
*/
|
||||
STACKED, // bars are drawn stacked vertically on top of each other
|
||||
|
||||
/**
|
||||
* Bars are drawn next to one another, grouped by iVal
|
||||
*/
|
||||
SIDE_BY_SIDE // bars are drawn horizontally next to each-other
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode with which to calculate the width of each bar.
|
||||
*/
|
||||
public enum BarGroupWidthMode {
|
||||
FIXED_WIDTH, // bar width is always barWidth
|
||||
FIXED_GAP // bar width is calculated relative to a fixed gap width between each bar
|
||||
}
|
||||
|
||||
public BarRenderer(XYPlot plot) {
|
||||
super(plot);
|
||||
}
|
||||
|
||||
public void setBarOrientation(BarOrientation renderBarOrientation) {
|
||||
this.barOrientation = renderBarOrientation;
|
||||
}
|
||||
|
||||
public BarOrientation getBarOrientation() {
|
||||
return this.barOrientation;
|
||||
}
|
||||
|
||||
public BarGroupWidthMode getBarGroupWidthMode() {
|
||||
return this.barGroupWidthMode;
|
||||
}
|
||||
|
||||
public float getBarGroupWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
public void setBarGroupWidth(BarGroupWidthMode mode, float width) {
|
||||
this.barGroupWidthMode = mode;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
protected BarComparator getBarComparator(float rangeOriginPx) {
|
||||
return new BarComparator(getBarOrientation(), rangeOriginPx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doDrawLegendIcon(Canvas canvas, RectF rect, BarFormatter formatter) {
|
||||
if (formatter.hasFillPaint()) {
|
||||
canvas.drawRect(rect, formatter.getFillPaint());
|
||||
}
|
||||
canvas.drawRect(rect, formatter.getBorderPaint());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the BarFormatter instance that corresponds with the series passed in.
|
||||
* Can be overridden to return other BarFormatters as a result of touch events etc.
|
||||
* @param index index of the point being rendered.
|
||||
* @param series XYSeries to which the point being rendered belongs.
|
||||
* @return The desired getFormatter or null to use the default.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
public FormatterType getFormatter(int index, XYSeries series) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRender(Canvas canvas, RectF plotArea, List<SeriesBundle<XYSeries,
|
||||
? extends FormatterType>> sfList, int seriesSize, RenderStack stack) {
|
||||
|
||||
List<BarGroup> barGroups = new ArrayList<>();
|
||||
|
||||
/*
|
||||
* Build the axisMap (yVal,BarGroup)... a TreeMap of BarGroups
|
||||
* BarGroups represent a point on the X axis where a single or group of bars need to be drawn.
|
||||
*/
|
||||
for(int i = 0; i < seriesSize; i++) {
|
||||
final BarGroup group = new BarGroup(i, 0, plotArea);
|
||||
int seriesOrder = 0;
|
||||
for(SeriesBundle<XYSeries, ? extends FormatterType> bundle : sfList) {
|
||||
// TODO: is this null check really necessary?
|
||||
if(bundle.getSeries().getX(i) != null) {
|
||||
Bar bar = new Bar(getPlot(), bundle.getSeries(),
|
||||
bundle.getFormatter(), seriesOrder, i, plotArea);
|
||||
group.addBar(bar);
|
||||
group.centerPix = bar.xPix;
|
||||
}
|
||||
seriesOrder++;
|
||||
}
|
||||
barGroups.add(group);
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the dimensions of each barGroup and then draw each bar within it according to
|
||||
* the Render Style and Width Style.
|
||||
*/
|
||||
final int groupCount = barGroups.size();
|
||||
for(BarGroup barGroup : barGroups) {
|
||||
|
||||
// Determine the exact left and right X for the Bar Group
|
||||
switch (barGroupWidthMode) {
|
||||
case FIXED_WIDTH:
|
||||
barGroup.leftPix = barGroup.centerPix - (width / 2);
|
||||
barGroup.rightPix = barGroup.leftPix + width;
|
||||
break;
|
||||
case FIXED_GAP:
|
||||
float barWidth = plotArea.width();
|
||||
if(groupCount > 1) {
|
||||
barWidth = (barGroups.get(1).centerPix - barGroups.get(0).centerPix) - width;
|
||||
}
|
||||
|
||||
final float halfWidth = barWidth / 2;
|
||||
barGroup.leftPix = barGroup.centerPix - halfWidth;
|
||||
barGroup.rightPix = barGroup.centerPix + halfWidth;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* Draw the bars within the barGroup area.
|
||||
*/
|
||||
double rangeOrigin = getPlot().getRangeOrigin().doubleValue();
|
||||
float rangeOriginPx = (float) getPlot().getBounds().yRegion
|
||||
.transform(rangeOrigin, plotArea.top, plotArea.bottom, true);
|
||||
|
||||
final BarComparator comparator = getBarComparator(rangeOriginPx);
|
||||
switch (barOrientation) {
|
||||
case IN_ORDER:
|
||||
case OVERLAID:
|
||||
Collections.sort(barGroup.bars, comparator);
|
||||
for (Bar bar : barGroup.bars) {
|
||||
drawBar(canvas, bar, createBarRect(
|
||||
bar.barGroup.leftPix,
|
||||
bar.yPix,
|
||||
bar.barGroup.rightPix,
|
||||
rangeOriginPx, bar.formatter));
|
||||
}
|
||||
break;
|
||||
case SIDE_BY_SIDE:
|
||||
final float width = barGroup.getWidth() / barGroup.bars.size();
|
||||
float leftX = barGroup.leftPix;
|
||||
Collections.sort(barGroup.bars, comparator);
|
||||
for (Bar bar : barGroup.bars) {
|
||||
drawBar(canvas, bar, createBarRect(
|
||||
leftX, bar.yPix,
|
||||
leftX + width, rangeOriginPx,
|
||||
bar.formatter));
|
||||
leftX = leftX + width;
|
||||
}
|
||||
break;
|
||||
case STACKED:
|
||||
float bottom = (int) barGroup.plotArea.bottom;
|
||||
Collections.sort(barGroup.bars, comparator);
|
||||
for (Bar bar : barGroup.bars) {
|
||||
// TODO: handling sub range-origin values for the purpose of labeling
|
||||
final float height = (int) bar.barGroup.plotArea.bottom - bar.yPix;
|
||||
final float top = bottom - height;
|
||||
drawBar(canvas, bar, createBarRect(
|
||||
bar.barGroup.leftPix, top,
|
||||
bar.barGroup.rightPix, bottom,
|
||||
bar.formatter));
|
||||
bottom = top;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unexpected BarOrientation: " + barOrientation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected RectF createBarRect(float w1, float h1, float w2, float h2, BarFormatter formatter) {
|
||||
final RectF result = RectFUtils.createFromEdges(w1, h1,w2, h2);
|
||||
result.left += formatter.getMarginLeft();
|
||||
result.right -= formatter.getMarginRight();
|
||||
result.top += formatter.getMarginTop();
|
||||
result.bottom -= formatter.getMarginBottom();
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void drawBar(Canvas canvas, Bar<FormatterType> bar, RectF rect) {
|
||||
|
||||
// null yVals are skipped:
|
||||
if(bar.getY() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BarFormatter formatter = getFormatter(bar.i, bar.series);
|
||||
if(formatter == null) {
|
||||
formatter = bar.formatter;
|
||||
}
|
||||
if(rect.height() > 0 && rect.width() > 0) {
|
||||
if (formatter.hasFillPaint()) {
|
||||
canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom,
|
||||
formatter.getFillPaint());
|
||||
}
|
||||
|
||||
if (formatter.hasLinePaint()) {
|
||||
canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom,
|
||||
formatter.getBorderPaint());
|
||||
}
|
||||
}
|
||||
|
||||
PointLabelFormatter plf =
|
||||
formatter.hasPointLabelFormatter() ? formatter.getPointLabelFormatter() : null;
|
||||
|
||||
PointLabeler pointLabeler =
|
||||
formatter != null ? formatter.getPointLabeler() : null;
|
||||
|
||||
if (plf != null && plf.hasTextPaint() && pointLabeler != null) {
|
||||
canvas.drawText(pointLabeler.getLabel(bar.series, bar.i),
|
||||
rect.centerX() + plf.hOffset, bar.yPix + plf.vOffset,
|
||||
plf.getTextPaint());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param <FormatterType>
|
||||
*/
|
||||
public static class Bar<FormatterType extends BarFormatter> {
|
||||
|
||||
public final XYSeries series;
|
||||
public final FormatterType formatter;
|
||||
public final int i;
|
||||
public final int seriesOrder;
|
||||
public final float xPix;
|
||||
public final float yPix;
|
||||
protected BarGroup barGroup;
|
||||
|
||||
// TODO: factor out plot param
|
||||
public Bar(XYPlot plot, XYSeries series, FormatterType formatter, int seriesOrder, int i, RectF plotArea) {
|
||||
this.series = series;
|
||||
this.formatter = formatter;
|
||||
this.i = i;
|
||||
this.seriesOrder = seriesOrder;
|
||||
|
||||
final double xVal = series.getX(i).doubleValue();
|
||||
xPix = (float) plot.getBounds().getxRegion()
|
||||
.transform(xVal, plotArea.left, plotArea.right, false);
|
||||
|
||||
if (series.getY(i) != null) {
|
||||
final double yVal = series.getY(i).doubleValue();
|
||||
this.yPix = (float) plot.getBounds().yRegion
|
||||
.transform(yVal, plotArea.top, plotArea.bottom, true);
|
||||
} else {
|
||||
this.yPix = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Number getY() {
|
||||
return series.getY(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of one or more {@Bar} instances sharing a common iVal.
|
||||
*/
|
||||
private static class BarGroup {
|
||||
|
||||
public ArrayList<Bar> bars;
|
||||
public int i;
|
||||
public float centerPix;
|
||||
public float leftPix;
|
||||
public float rightPix;
|
||||
public RectF plotArea;
|
||||
|
||||
public BarGroup(int i, float centerPix, RectF plotArea) {
|
||||
// Setup the TreeMap with the required comparator
|
||||
this.bars = new ArrayList<>(); // create a comparator that compares series title given the index.
|
||||
this.centerPix = centerPix;
|
||||
this.plotArea = plotArea;
|
||||
this.i = i;
|
||||
}
|
||||
|
||||
public void addBar(Bar bar) {
|
||||
bar.barGroup = this;
|
||||
this.bars.add(bar);
|
||||
}
|
||||
|
||||
protected float getWidth() {
|
||||
return rightPix - leftPix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to determine the order in which bars of the same group will be drawn.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static class BarComparator implements Comparator<Bar> {
|
||||
|
||||
private final BarOrientation barOrientation;
|
||||
private final float rangeOriginPx;
|
||||
|
||||
public BarComparator(BarOrientation barOrientation, float rangeOriginPx) {
|
||||
this.rangeOriginPx = rangeOriginPx;
|
||||
this.barOrientation = barOrientation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(Bar bar1, Bar bar2) {
|
||||
switch (barOrientation) {
|
||||
case OVERLAID:
|
||||
if(bar1.yPix > rangeOriginPx && bar2.yPix > rangeOriginPx) {
|
||||
return Float.valueOf(bar2.yPix).compareTo(bar1.yPix);
|
||||
} else {
|
||||
return Float.valueOf(bar1.yPix).compareTo(bar2.yPix);
|
||||
}
|
||||
case IN_ORDER:
|
||||
case SIDE_BY_SIDE:
|
||||
case STACKED:
|
||||
default:
|
||||
return Integer.valueOf(bar1.seriesOrder).compareTo(bar2.seriesOrder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
public enum BoundaryMode {
|
||||
FIXED,
|
||||
AUTO,
|
||||
GROW,
|
||||
SHRINK
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import com.androidplot.ui.SeriesRenderer;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
/**
|
||||
* Format for drawing a value using {@link BubbleRenderer}.
|
||||
* @since 1.2.2
|
||||
*/
|
||||
public class BubbleFormatter extends XYSeriesFormatter<XYRegionFormatter> {
|
||||
|
||||
private static final float DEFAULT_STROKE_PIX = 1;
|
||||
private static final int DEFAULT_STROKE_COLOR = Color.BLACK;
|
||||
private static final int DEFAULT_FILL_COLOR = Color.YELLOW;
|
||||
|
||||
|
||||
private Paint strokePaint;
|
||||
private Paint fillPaint;
|
||||
|
||||
{
|
||||
strokePaint = new Paint();
|
||||
strokePaint.setAntiAlias(true);
|
||||
strokePaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_STROKE_PIX));
|
||||
strokePaint.setStyle(Paint.Style.STROKE);
|
||||
strokePaint.setColor(DEFAULT_STROKE_COLOR);
|
||||
|
||||
fillPaint = new Paint();
|
||||
fillPaint.setAntiAlias(true);
|
||||
fillPaint.setColor(DEFAULT_FILL_COLOR);
|
||||
|
||||
// default point labeler should draw z for bubbles:
|
||||
setPointLabeler(new PointLabeler<BubbleSeries>() {
|
||||
@Override
|
||||
public String getLabel(BubbleSeries series, int index) {
|
||||
return String.valueOf(series.getZ(index));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public BubbleFormatter() {}
|
||||
|
||||
public BubbleFormatter(Context context, int xmlCfgId) {
|
||||
this();
|
||||
configure(context, xmlCfgId);
|
||||
}
|
||||
|
||||
public BubbleFormatter(int fillColor, int strokeColor) {
|
||||
fillPaint.setColor(fillColor);
|
||||
strokePaint.setColor(strokeColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends SeriesRenderer> getRendererClass() {
|
||||
return BubbleRenderer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BubbleRenderer doGetRendererInstance(XYPlot plot) {
|
||||
return new BubbleRenderer(plot);
|
||||
}
|
||||
|
||||
public Paint getStrokePaint() {
|
||||
return strokePaint;
|
||||
}
|
||||
|
||||
public void setStrokePaint(Paint strokePaint) {
|
||||
this.strokePaint = strokePaint;
|
||||
}
|
||||
|
||||
public Paint getFillPaint() {
|
||||
return fillPaint;
|
||||
}
|
||||
|
||||
public void setFillPaint(Paint fillPaint) {
|
||||
this.fillPaint = fillPaint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.*;
|
||||
|
||||
import com.androidplot.Region;
|
||||
import com.androidplot.ui.*;
|
||||
import com.androidplot.util.*;
|
||||
|
||||
/**
|
||||
* Renders three dimensional data onto an {@link XYPlot} as bubbles; the x/y values define the position
|
||||
* of the bubble and z is uses as a scaling value for the bubble's radius.
|
||||
* @since 1.2.2
|
||||
*/
|
||||
public class BubbleRenderer<FormatterType extends BubbleFormatter> extends XYSeriesRenderer<BubbleSeries, FormatterType> {
|
||||
|
||||
protected static final float MIN_BUBBLE_RADIUS_DEFAULT_DP = 9;
|
||||
protected static final float MAX_BUBBLE_RADIUS_DEFAULT_DP = 25;
|
||||
|
||||
private Region bubbleBounds;
|
||||
|
||||
private BubbleScaleMode bubbleScaleMode = BubbleScaleMode.SQUARE_ROOT;
|
||||
|
||||
public enum BubbleScaleMode {
|
||||
|
||||
/**
|
||||
* Bubble radius is scaled directly by {@link BubbleSeries} z-vals
|
||||
*/
|
||||
LINEAR,
|
||||
|
||||
/**
|
||||
* Bubble radius is scaled by the square root of {@link BubbleSeries} z-vals.
|
||||
* This is the default scaling used.
|
||||
*/
|
||||
SQUARE_ROOT
|
||||
}
|
||||
|
||||
public BubbleRenderer(XYPlot plot) {
|
||||
super(plot);
|
||||
|
||||
bubbleBounds = new Region(
|
||||
PixelUtils.dpToPix(MIN_BUBBLE_RADIUS_DEFAULT_DP),
|
||||
PixelUtils.dpToPix(MAX_BUBBLE_RADIUS_DEFAULT_DP));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRender(Canvas canvas, RectF plotArea, BubbleSeries series,
|
||||
FormatterType formatter, RenderStack stack) {
|
||||
|
||||
Region magnitudeBounds = calculateBounds();
|
||||
for(int i = 0; i < series.size(); i++) {
|
||||
|
||||
// only render non-null values greater than zero:
|
||||
if(series.getY(i) != null && series.getZ(i).doubleValue() > 0) {
|
||||
|
||||
final PointF centerPoint = getPlot().getBounds().
|
||||
transform(series.getX(i), series.getY(i), plotArea, false, true);
|
||||
|
||||
// calculate bubble radius:
|
||||
float bubbleRadius = magnitudeBounds.
|
||||
transform(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT ?
|
||||
Math.sqrt(series.getZ(i).doubleValue()) :
|
||||
series.getZ(i).doubleValue(), bubbleBounds).floatValue();
|
||||
drawBubble(canvas, formatter, series, i, centerPoint, bubbleRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a bubble onto the canvas
|
||||
* @param canvas
|
||||
* @param formatter
|
||||
* @param series
|
||||
* @param index
|
||||
* @param centerPoint the x/y coords of the center of the bubble
|
||||
* @param radius size of the bubble
|
||||
*/
|
||||
protected void drawBubble(Canvas canvas, FormatterType formatter, BubbleSeries series,
|
||||
int index, PointF centerPoint, float radius) {
|
||||
canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getFillPaint());
|
||||
canvas.drawCircle(centerPoint.x, centerPoint.y, radius, formatter.getStrokePaint());
|
||||
if(series != null && formatter.hasPointLabelFormatter() && formatter.getPointLabeler() != null) {
|
||||
FontUtils.drawTextVerticallyCentered(
|
||||
canvas,
|
||||
formatter.getPointLabeler().getLabel(series, index),
|
||||
centerPoint.x,
|
||||
centerPoint.y,
|
||||
formatter.getPointLabelFormatter().getTextPaint());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) {
|
||||
drawBubble(canvas, formatter, null, 0,
|
||||
new PointF(rect.centerX(), rect.centerY()), (rect.width()/2.5f));
|
||||
}
|
||||
|
||||
public float getMinBubbleRadius() {
|
||||
return bubbleBounds.getMin().floatValue();
|
||||
}
|
||||
|
||||
public void setMinBubbleRadius(float minBubbleRadius) {
|
||||
bubbleBounds.setMin(minBubbleRadius);
|
||||
}
|
||||
|
||||
public float getMaxBubbleRadius() {
|
||||
return bubbleBounds.getMax().floatValue();
|
||||
}
|
||||
|
||||
public void setMaxBubbleRadius(float maxBubbleRadius) {
|
||||
bubbleBounds.setMax(maxBubbleRadius);
|
||||
}
|
||||
|
||||
public BubbleScaleMode getBubbleScaleMode() {
|
||||
return bubbleScaleMode;
|
||||
}
|
||||
|
||||
public void setBubbleScaleMode(BubbleScaleMode bubbleScaleMode) {
|
||||
this.bubbleScaleMode = bubbleScaleMode;
|
||||
}
|
||||
|
||||
protected Region calculateBounds() {
|
||||
Region bounds = new Region();
|
||||
for(SeriesBundle<BubbleSeries, ? extends FormatterType> f : getSeriesAndFormatterList()) {
|
||||
SeriesUtils.minMax(bounds, f.getSeries().getZVals());
|
||||
}
|
||||
|
||||
if(bounds.getMax() != null && bounds.getMax().doubleValue() > 0) {
|
||||
if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) {
|
||||
// scale for easier visual interpretation. see:
|
||||
// https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly
|
||||
bounds.setMax(Math.sqrt(bounds.getMax().doubleValue()));
|
||||
}
|
||||
} else {
|
||||
// no non-null, greater than zero vals so bounds are undefined
|
||||
return null;
|
||||
}
|
||||
|
||||
if(bounds.getMin().doubleValue() > 0) {
|
||||
|
||||
if(bubbleScaleMode == BubbleScaleMode.SQUARE_ROOT) {
|
||||
// scale for easier visual interpretation. see:
|
||||
// https://en.wikipedia.org/wiki/Bubble_chart#Choosing_bubble_sizes_correctly
|
||||
bounds.setMin(Math.sqrt(bounds.getMin().doubleValue()));
|
||||
}
|
||||
} else {
|
||||
// if the smallest value is negative, use zero instead since those vals arent visible:
|
||||
bounds.setMax(0);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Created by halfhp on 9/17/16.
|
||||
*/
|
||||
public class BubbleSeries implements XYSeries {
|
||||
|
||||
private List<Number> xVals;
|
||||
private List<Number> yVals;
|
||||
private List<Number> zVals;
|
||||
private String title;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param interleavedValues Interleaved values ordered as x, y, z; total size must be a multiple of 3.
|
||||
*/
|
||||
public BubbleSeries(Number... interleavedValues) {
|
||||
if(interleavedValues == null || interleavedValues.length % 3 > 0) {
|
||||
throw new RuntimeException("BubbleSeries interleave array length must be a non-zero multiple of 3.");
|
||||
}
|
||||
|
||||
xVals = new ArrayList<>();
|
||||
yVals = new ArrayList<>();
|
||||
zVals = new ArrayList<>();
|
||||
for(int i = 0; i < interleavedValues.length; i+=3) {
|
||||
xVals.add(interleavedValues[i]);
|
||||
yVals.add(interleavedValues[i+1]);
|
||||
zVals.add(interleavedValues[i+2]);
|
||||
}
|
||||
}
|
||||
|
||||
public BubbleSeries(List<Number> yVals, List<Number> zVals, String title) {
|
||||
this.yVals = yVals;
|
||||
this.zVals = zVals;
|
||||
this.title = title;
|
||||
// populate x with iVals:
|
||||
this.xVals = new ArrayList<>(zVals.size());
|
||||
for(int i = 0; i < zVals.size(); i++) {
|
||||
this.xVals.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
public BubbleSeries(List<Number> xVals, List<Number> yVals, List<Number> zVals, String title) {
|
||||
this.xVals = xVals;
|
||||
this.yVals = yVals;
|
||||
this.zVals = zVals;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return xVals.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getX(int index) {
|
||||
return xVals.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getY(int index) {
|
||||
return yVals.get(index);
|
||||
}
|
||||
|
||||
public Number getZ(int index) {
|
||||
return zVals.get(index);
|
||||
}
|
||||
|
||||
public List<Number> getZVals() {
|
||||
return zVals;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import com.androidplot.ui.SeriesRenderer;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
/**
|
||||
* Format for drawing a value using {@link CandlestickRenderer}.
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public class CandlestickFormatter extends XYSeriesFormatter<XYRegionFormatter> {
|
||||
|
||||
private static final float DEFAULT_WIDTH_PIX = PixelUtils.dpToPix(10);
|
||||
private static final float DEFAULT_STROKE_PIX = PixelUtils.dpToPix(4);
|
||||
|
||||
private Paint wickPaint;
|
||||
private Paint risingBodyFillPaint;
|
||||
private Paint fallingBodyFillPaint;
|
||||
private Paint risingBodyStrokePaint;
|
||||
private Paint fallingBodyStrokePaint;
|
||||
private Paint upperCapPaint;
|
||||
private Paint lowerCapPaint;
|
||||
|
||||
private float bodyWidth = DEFAULT_WIDTH_PIX;
|
||||
private float upperCapWidth = DEFAULT_WIDTH_PIX;
|
||||
private float lowerCapWidth = DEFAULT_WIDTH_PIX;
|
||||
|
||||
private BodyStyle bodyStyle;
|
||||
|
||||
public enum BodyStyle {
|
||||
SQUARE,
|
||||
TRIANGULAR
|
||||
}
|
||||
|
||||
protected static Paint getDefaultFillPaint(int color) {
|
||||
Paint p = new Paint();
|
||||
p.setStyle(Paint.Style.FILL);
|
||||
p.setColor(color);
|
||||
return p;
|
||||
}
|
||||
|
||||
protected static Paint getDefaultStrokePaint(int color) {
|
||||
Paint p = new Paint();
|
||||
p.setStyle(Paint.Style.STROKE);
|
||||
p.setStrokeWidth(DEFAULT_STROKE_PIX);
|
||||
p.setColor(color);
|
||||
p.setAntiAlias(true);
|
||||
return p;
|
||||
}
|
||||
|
||||
public CandlestickFormatter(Context context, int xmlCfgId) {
|
||||
this();
|
||||
configure(context, xmlCfgId);
|
||||
}
|
||||
|
||||
public CandlestickFormatter() {
|
||||
this(getDefaultStrokePaint(Color.YELLOW),
|
||||
getDefaultFillPaint(Color.GREEN),
|
||||
getDefaultFillPaint(Color.RED),
|
||||
getDefaultStrokePaint(Color.GREEN),
|
||||
getDefaultStrokePaint(Color.RED),
|
||||
getDefaultStrokePaint(Color.YELLOW),
|
||||
getDefaultStrokePaint(Color.YELLOW),
|
||||
BodyStyle.SQUARE);
|
||||
}
|
||||
|
||||
public CandlestickFormatter(Paint wickPaint, Paint risingBodyFillPaint, Paint fallingBodyFillPaint,
|
||||
Paint risingBodyStrokePaint, Paint fallingBodyStrokePaint,
|
||||
Paint upperCapPaint, Paint lowerCapPaint, BodyStyle bodyStyle) {
|
||||
setWickPaint(wickPaint);
|
||||
setRisingBodyFillPaint(risingBodyFillPaint);
|
||||
setFallingBodyFillPaint(fallingBodyFillPaint);
|
||||
setRisingBodyStrokePaint(risingBodyStrokePaint);
|
||||
setFallingBodyStrokePaint(fallingBodyStrokePaint);
|
||||
setUpperCapPaint(upperCapPaint);
|
||||
setLowerCapPaint(lowerCapPaint);
|
||||
setBodyStyle(bodyStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends SeriesRenderer> getRendererClass() {
|
||||
return CandlestickRenderer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
|
||||
return new CandlestickRenderer(plot);
|
||||
}
|
||||
|
||||
public Paint getWickPaint() {
|
||||
return wickPaint;
|
||||
}
|
||||
|
||||
public void setWickPaint(Paint wickPaint) {
|
||||
this.wickPaint = wickPaint;
|
||||
}
|
||||
|
||||
public Paint getRisingBodyFillPaint() {
|
||||
return risingBodyFillPaint;
|
||||
}
|
||||
|
||||
public void setRisingBodyFillPaint(Paint risingBodyFillPaint) {
|
||||
this.risingBodyFillPaint = risingBodyFillPaint;
|
||||
}
|
||||
|
||||
public Paint getRisingBodyStrokePaint() {
|
||||
return risingBodyStrokePaint;
|
||||
}
|
||||
|
||||
public void setRisingBodyStrokePaint(Paint risingBodyStrokePaint) {
|
||||
this.risingBodyStrokePaint = risingBodyStrokePaint;
|
||||
}
|
||||
|
||||
public Paint getUpperCapPaint() {
|
||||
return upperCapPaint;
|
||||
}
|
||||
|
||||
public void setUpperCapPaint(Paint upperCapPaint) {
|
||||
this.upperCapPaint = upperCapPaint;
|
||||
}
|
||||
|
||||
public Paint getLowerCapPaint() {
|
||||
return lowerCapPaint;
|
||||
}
|
||||
|
||||
public void setLowerCapPaint(Paint lowerCapPaint) {
|
||||
this.lowerCapPaint = lowerCapPaint;
|
||||
}
|
||||
|
||||
public float getBodyWidth() {
|
||||
return bodyWidth;
|
||||
}
|
||||
|
||||
public void setBodyWidth(float bodyWidth) {
|
||||
this.bodyWidth = bodyWidth;
|
||||
}
|
||||
|
||||
public float getLowerCapWidth() {
|
||||
return lowerCapWidth;
|
||||
}
|
||||
|
||||
public void setLowerCapWidth(float lowerCapWidth) {
|
||||
this.lowerCapWidth = lowerCapWidth;
|
||||
}
|
||||
|
||||
public float getUpperCapWidth() {
|
||||
return upperCapWidth;
|
||||
}
|
||||
|
||||
public void setUpperCapWidth(float upperCapWidth) {
|
||||
this.upperCapWidth = upperCapWidth;
|
||||
}
|
||||
|
||||
public Paint getFallingBodyFillPaint() {
|
||||
return fallingBodyFillPaint;
|
||||
}
|
||||
|
||||
public void setFallingBodyFillPaint(Paint fallingBodyFillPaint) {
|
||||
this.fallingBodyFillPaint = fallingBodyFillPaint;
|
||||
}
|
||||
|
||||
public Paint getFallingBodyStrokePaint() {
|
||||
return fallingBodyStrokePaint;
|
||||
}
|
||||
|
||||
public void setFallingBodyStrokePaint(Paint fallingBodyStrokePaint) {
|
||||
this.fallingBodyStrokePaint = fallingBodyStrokePaint;
|
||||
}
|
||||
|
||||
public BodyStyle getBodyStyle() {
|
||||
return bodyStyle;
|
||||
}
|
||||
|
||||
public void setBodyStyle(BodyStyle bodyStyle) {
|
||||
this.bodyStyle = bodyStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to set caps and wick to a single color in one call.
|
||||
* @param paint
|
||||
*/
|
||||
public void setCapAndWickPaint(Paint paint) {
|
||||
setUpperCapPaint(paint);
|
||||
setLowerCapPaint(paint);
|
||||
setWickPaint(paint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* Helper utility to simplify the creation of of candlestick charts
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public abstract class CandlestickMaker {
|
||||
|
||||
/**
|
||||
* Adds a candlestick chart to the specified plot using the specified
|
||||
* high, low, open and close values.
|
||||
* @param plot
|
||||
* @param formatter
|
||||
* @param openVals
|
||||
* @param closeVals
|
||||
* @param highVals
|
||||
* @param lowVals
|
||||
*/
|
||||
public static void make(XYPlot plot, CandlestickFormatter formatter,
|
||||
XYSeries openVals, XYSeries closeVals, XYSeries highVals, XYSeries lowVals) {
|
||||
plot.addSeries(formatter, highVals, lowVals, openVals, closeVals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a candlestick chart to the specified plot using the specified {@link CandlestickSeries}.
|
||||
* @param plot
|
||||
* @param formatter
|
||||
* @param series
|
||||
* @since 0.9.8
|
||||
*/
|
||||
public static void make(XYPlot plot, CandlestickFormatter formatter, CandlestickSeries series) {
|
||||
make(plot, formatter, series.getOpenSeries(), series.getCloseSeries(),
|
||||
series.getHighSeries(), series.getLowSeries());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the validity of series data comprising a {@link CandlestickSeries}.
|
||||
* This is a development aid; be sure to remove any usage of this method in production code.
|
||||
* @param series
|
||||
* @since 0.9.8
|
||||
*/
|
||||
public static void check(CandlestickSeries series) {
|
||||
check(series.getOpenSeries(), series.getCloseSeries(), series.getHighSeries(), series.getLowSeries());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the validity of series data comprising a candlestick chart.
|
||||
* This is a development aid; be sure to remove any usage of this method in production code.
|
||||
* @param openVals
|
||||
* @param closeVals
|
||||
* @param highVals
|
||||
* @param lowVals
|
||||
* @since 0.9.8
|
||||
*/
|
||||
public static void check(XYSeries openVals, XYSeries closeVals, XYSeries highVals, XYSeries lowVals) {
|
||||
final int size = openVals.size();
|
||||
assert closeVals.size() == size : "closeVals has irregular size.";
|
||||
assert highVals.size() == size : "highVals has irregular size.";
|
||||
assert lowVals.size() == size : "lowVals has irregular size.";
|
||||
|
||||
for(int i = 0; i < size; i++) {
|
||||
|
||||
final double highVal = highVals.getY(i).doubleValue();
|
||||
final double lowVal = lowVals.getY(i).doubleValue();
|
||||
final double openVal = openVals.getY(i).doubleValue();
|
||||
final double closeVal = closeVals.getY(i).doubleValue();
|
||||
|
||||
assert openVal <= highVal : "Detected openVal > highVal at index " + i;
|
||||
assert openVal >= lowVal : "Detected openVal < lowVal at index " + i;
|
||||
assert closeVal <= highVal : "Detected closeVal > highVal at index " + i;
|
||||
assert closeVal >= lowVal : "Detected closeVal < lowVal at index " + i;
|
||||
assert lowVal <= highVal : "Detected lowVal > highVal at index " + i;
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.*;
|
||||
import com.androidplot.ui.RenderStack;
|
||||
import com.androidplot.ui.SeriesBundle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Renders a group of {@link com.androidplot.xy.XYSeries} as a candlestick chart
|
||||
* into an {@link com.androidplot.xy.XYPlot}.
|
||||
*
|
||||
* Constraints:
|
||||
* - Exactly four series must be added using the same {@link CandlestickFormatter}.
|
||||
* - Each of the four series has the same x(i) value.
|
||||
* - Expects that series are added in the order of:
|
||||
* high, low, open, close
|
||||
*
|
||||
* {@link CandlestickSeries} and {@link CandlestickMaker} provide simplified classes and methods
|
||||
* for setting up a candlestick chart.
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public class CandlestickRenderer<FormatterType extends CandlestickFormatter> extends GroupRenderer<FormatterType> {
|
||||
|
||||
protected static final int HIGH_INDEX = 0;
|
||||
protected static final int LOW_INDEX = 1;
|
||||
protected static final int OPEN_INDEX = 2;
|
||||
protected static final int CLOSE_INDEX = 3;
|
||||
|
||||
public CandlestickRenderer(XYPlot plot) {
|
||||
super(plot);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRender(Canvas canvas, RectF plotArea, List<SeriesBundle<XYSeries,
|
||||
? extends FormatterType>> sfList, int seriesSize, RenderStack stack) {
|
||||
|
||||
final FormatterType formatter = sfList.get(0).getFormatter();
|
||||
for(int i = 0; i < seriesSize; i++) {
|
||||
|
||||
final XYSeries highSeries = sfList.get(HIGH_INDEX).getSeries();
|
||||
final XYSeries lowSeries = sfList.get(LOW_INDEX).getSeries();
|
||||
final XYSeries openSeries = sfList.get(OPEN_INDEX).getSeries();
|
||||
final XYSeries closeSeries = sfList.get(CLOSE_INDEX).getSeries();
|
||||
|
||||
// x-val for all series should be identical so just grab x from the first series:
|
||||
Number x = highSeries.getX(i);
|
||||
|
||||
Number high = highSeries.getY(i);
|
||||
Number low = lowSeries.getY(i);
|
||||
Number open = openSeries.getY(i);
|
||||
Number close = closeSeries.getY(i);
|
||||
|
||||
// draw the candlestick:
|
||||
final PointF highPix = getPlot().getBounds().transformScreen(x, high, plotArea);
|
||||
final PointF lowPix = getPlot().getBounds().transformScreen(x, low, plotArea);
|
||||
final PointF openPix = getPlot().getBounds().transformScreen(x, open, plotArea);
|
||||
final PointF closePix = getPlot().getBounds().transformScreen(x, close, plotArea);
|
||||
|
||||
drawWick(canvas, highPix, lowPix, formatter);
|
||||
drawBody(canvas, openPix, closePix, formatter);
|
||||
drawUpperCap(canvas, highPix, formatter);
|
||||
drawLowerCap(canvas, lowPix, formatter);
|
||||
|
||||
// draw labels, if any:
|
||||
final PointLabelFormatter plf = formatter.hasPointLabelFormatter()
|
||||
? formatter.getPointLabelFormatter() : null;
|
||||
final PointLabeler pointLabeler = formatter.getPointLabeler();
|
||||
if(plf != null && pointLabeler != null) {
|
||||
drawTextLabel(canvas, highPix, pointLabeler.getLabel(highSeries, i), plf);
|
||||
drawTextLabel(canvas, lowPix, pointLabeler.getLabel(lowSeries, i), plf);
|
||||
drawTextLabel(canvas, openPix, pointLabeler.getLabel(openSeries, i), plf);
|
||||
drawTextLabel(canvas, closePix, pointLabeler.getLabel(closeSeries, i), plf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawTextLabel(Canvas canvas, PointF coords, String text, PointLabelFormatter plf) {
|
||||
if(text != null) {
|
||||
canvas.drawText(text, coords.x + plf.hOffset, coords.y + plf.vOffset, plf.getTextPaint());
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawWick(Canvas canvas, PointF min, PointF max, FormatterType formatter) {
|
||||
canvas.drawLine(min.x, min.y, max.x, max.y, formatter.getWickPaint());
|
||||
}
|
||||
|
||||
protected void drawBody(Canvas canvas, PointF open, PointF close, FormatterType formatter) {
|
||||
final float halfWidth = formatter.getBodyWidth() / 2;
|
||||
final RectF rect = new RectF(open.x - halfWidth, open.y, close.x + halfWidth, close.y);
|
||||
|
||||
Paint bodyFillPaint = open.y >= close.y ?
|
||||
formatter.getRisingBodyFillPaint() : formatter.getFallingBodyFillPaint();
|
||||
|
||||
Paint bodyStrokePaint = open.y >= close.y ?
|
||||
formatter.getRisingBodyStrokePaint() : formatter.getFallingBodyStrokePaint();
|
||||
|
||||
switch(formatter.getBodyStyle()) {
|
||||
case SQUARE:
|
||||
canvas.drawRect(rect, bodyFillPaint);
|
||||
canvas.drawRect(rect, bodyStrokePaint);
|
||||
break;
|
||||
case TRIANGULAR:
|
||||
drawTriangle(canvas, rect, bodyFillPaint, bodyStrokePaint);
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawUpperCap(Canvas canvas, PointF val, FormatterType formatter) {
|
||||
final float halfWidth = formatter.getUpperCapWidth();
|
||||
canvas.drawLine(val.x - halfWidth, val.y, val.x + halfWidth, val.y, formatter.getUpperCapPaint());
|
||||
}
|
||||
|
||||
protected void drawLowerCap(Canvas canvas, PointF val, FormatterType formatter) {
|
||||
final float halfWidth = formatter.getLowerCapWidth();
|
||||
canvas.drawLine(val.x - halfWidth, val.y, val.x + halfWidth, val.y, formatter.getLowerCapPaint());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDrawLegendIcon(Canvas canvas, RectF rect, FormatterType formatter) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
protected void drawTriangle(Canvas canvas, RectF rect,
|
||||
Paint fillPaint, Paint strokePaint) {
|
||||
Path path = new Path();
|
||||
path.moveTo(rect.centerX(), rect.bottom);
|
||||
path.lineTo(rect.left,rect.top);
|
||||
path.lineTo(rect.right, rect.top);
|
||||
path.close();
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.drawPath(path, strokePaint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import com.androidplot.xy.SimpleXYSeries;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Convenience class for representing a series of candlestick values;
|
||||
* is NOT a descendant of {@link com.androidplot.xy.XYSeries} and therefore
|
||||
* cannot be directly added to an {@link com.androidplot.xy.XYPlot}.
|
||||
*
|
||||
* This class is NOT threadsafe.
|
||||
*
|
||||
* @since 0.9.8
|
||||
*/
|
||||
public class CandlestickSeries {
|
||||
|
||||
private SimpleXYSeries highSeries = new SimpleXYSeries(null);
|
||||
private SimpleXYSeries lowSeries = new SimpleXYSeries(null);
|
||||
private SimpleXYSeries openSeries = new SimpleXYSeries(null);
|
||||
private SimpleXYSeries closeSeries = new SimpleXYSeries(null);
|
||||
|
||||
protected static List<Number> generateRange(int start, int end) {
|
||||
List<Number> range = new ArrayList<>(end - start);
|
||||
for(int i = start; i < end; i++) {
|
||||
range.add(i);
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
public CandlestickSeries(Item... items) {
|
||||
this(Arrays.asList(items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new CandlestickSeries.
|
||||
* Calls {@link #CandlestickSeries(List, List)} with a list of xVals
|
||||
* generated using the formula x=i.
|
||||
* @param items
|
||||
*/
|
||||
public CandlestickSeries(List<Item> items) {
|
||||
this(generateRange(0, items.size()), items);
|
||||
}
|
||||
|
||||
public CandlestickSeries(List<Number> xVals, List<Item> items) {
|
||||
if(xVals.size() != items.size()) {
|
||||
throw new IllegalArgumentException("xVals and yVals length must be identical.");
|
||||
}
|
||||
for(int i = 0; i < xVals.size(); i++) {
|
||||
Number x = xVals.get(i);
|
||||
highSeries.addLast(x, items.get(i).getHigh());
|
||||
lowSeries.addLast(x, items.get(i).getLow());
|
||||
openSeries.addLast(x, items.get(i).getOpen());
|
||||
closeSeries.addLast(x, items.get(i).getClose());
|
||||
}
|
||||
}
|
||||
|
||||
public SimpleXYSeries getHighSeries() {
|
||||
return highSeries;
|
||||
}
|
||||
|
||||
public void setHighSeries(SimpleXYSeries highSeries) {
|
||||
this.highSeries = highSeries;
|
||||
}
|
||||
|
||||
public SimpleXYSeries getLowSeries() {
|
||||
return lowSeries;
|
||||
}
|
||||
|
||||
public void setLowSeries(SimpleXYSeries lowSeries) {
|
||||
this.lowSeries = lowSeries;
|
||||
}
|
||||
|
||||
public SimpleXYSeries getOpenSeries() {
|
||||
return openSeries;
|
||||
}
|
||||
|
||||
public void setOpenSeries(SimpleXYSeries openSeries) {
|
||||
this.openSeries = openSeries;
|
||||
}
|
||||
|
||||
public SimpleXYSeries getCloseSeries() {
|
||||
return closeSeries;
|
||||
}
|
||||
|
||||
public void setCloseSeries(SimpleXYSeries closeSeries) {
|
||||
this.closeSeries = closeSeries;
|
||||
}
|
||||
|
||||
public static class Item {
|
||||
private double low;
|
||||
private double high;
|
||||
private double open;
|
||||
private double close;
|
||||
|
||||
/**
|
||||
* An individual candlestick value. Since it is illegal to include
|
||||
* null values for any member of a candlestick, this class is modeled with
|
||||
* double values instead of {@link Number} instances.
|
||||
* @param low
|
||||
* @param high
|
||||
* @param open
|
||||
* @param close
|
||||
*/
|
||||
public Item(double low, double high, double open, double close) {
|
||||
this.low = low;
|
||||
this.high = high;
|
||||
this.open = open;
|
||||
this.close = close;
|
||||
}
|
||||
|
||||
public double getLow() {
|
||||
return low;
|
||||
}
|
||||
|
||||
public void setLow(double low) {
|
||||
this.low = low;
|
||||
}
|
||||
|
||||
public double getHigh() {
|
||||
return high;
|
||||
}
|
||||
|
||||
public void setHigh(double high) {
|
||||
this.high = high;
|
||||
}
|
||||
|
||||
public double getOpen() {
|
||||
return open;
|
||||
}
|
||||
|
||||
public void setOpen(double open) {
|
||||
this.open = open;
|
||||
}
|
||||
|
||||
public double getClose() {
|
||||
return close;
|
||||
}
|
||||
|
||||
public void setClose(double close) {
|
||||
this.close = close;
|
||||
}
|
||||
}
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An implementation of Catmull-Rom interpolation, based on the information found at:
|
||||
* http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/19283471#19283471
|
||||
*/
|
||||
public class CatmullRomInterpolator implements Interpolator<CatmullRomInterpolator.Params> {
|
||||
|
||||
public enum Type {
|
||||
Uniform,
|
||||
Centripetal
|
||||
}
|
||||
|
||||
public static class Params implements InterpolationParams {
|
||||
|
||||
private int pointPerSegment;
|
||||
private Type type;
|
||||
|
||||
public Params(int pointPerSegment, Type type) {
|
||||
this.pointPerSegment = pointPerSegment;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<CatmullRomInterpolator> getInterpolatorClass() {
|
||||
return CatmullRomInterpolator.class;
|
||||
}
|
||||
|
||||
public int getPointPerSegment() {
|
||||
return pointPerSegment;
|
||||
}
|
||||
|
||||
public void setPointPerSegment(int pointPerSegment) {
|
||||
this.pointPerSegment = pointPerSegment;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a normal XYSeries, inserting a new point at the beginning and end of the series.
|
||||
*/
|
||||
static class ExtrapolatedXYSeries implements XYSeries {
|
||||
private final XYCoords first;
|
||||
private final XYCoords last;
|
||||
private final XYSeries series;
|
||||
|
||||
public ExtrapolatedXYSeries(XYSeries series, XYCoords first, XYCoords last) {
|
||||
this.series = series;
|
||||
this.first = first;
|
||||
this.last = last;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getX(int i) {
|
||||
if(i == 0) {
|
||||
return first.x;
|
||||
} else if(i == series.size() + 1) {
|
||||
return last.x;
|
||||
} else {
|
||||
return series.getX(i-1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getY(int i) {
|
||||
if(i == 0) {
|
||||
return first.y;
|
||||
} else if(i == series.size() + 1) {
|
||||
return last.y;
|
||||
} else {
|
||||
return series.getY(i-1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return series.size() + 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return series.getTitle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will calculate the Catmull-Rom interpolation curve, returning it as a list of Coord coordinate
|
||||
* objects. This method in particular adds the first and last control points which are not visible, but required
|
||||
* for calculating the spline.
|
||||
*
|
||||
* @param series The list of original straight line points to calculate an interpolation from.
|
||||
* @return The list of interpolated coordinates.
|
||||
* @throws java.lang.IllegalArgumentException if pointsPerSegment is less than 2.
|
||||
*/
|
||||
@Override
|
||||
public List<XYCoords> interpolate(XYSeries series, Params params) {
|
||||
if (params.getPointPerSegment() < 2) {
|
||||
throw new IllegalArgumentException(
|
||||
"pointsPerSegment must be greater than 2, since 2 points is just the linear segment.");
|
||||
}
|
||||
|
||||
// Cannot interpolate curves given only two points. Two points
|
||||
// is best represented as a simple line segment.
|
||||
if (series.size() < 3) {
|
||||
throw new IllegalArgumentException("Cannot interpolate a series with fewer than 3 vertices.");
|
||||
}
|
||||
|
||||
// Get the change in x and y between the first and second coordinates.
|
||||
double dx = series.getX(1).doubleValue() - series.getX(0).doubleValue();
|
||||
double dy = series.getY(1).doubleValue() - series.getY(0).doubleValue();
|
||||
|
||||
// Then using the change, extrapolate backwards to find a control point.
|
||||
double x1 = series.getX(0).doubleValue() - dx;
|
||||
double y1 = series.getY(0).doubleValue() - dy;
|
||||
|
||||
// Actually create the start point from the extrapolated values.
|
||||
XYCoords start = new XYCoords(x1, y1);
|
||||
|
||||
// Repeat for the end control point.
|
||||
int n = series.size() -1;
|
||||
dx = series.getX(n).doubleValue() - series.getX(n-1).doubleValue();
|
||||
dy = series.getY(n).doubleValue() - series.getY(n - 1).doubleValue();
|
||||
double xn = series.getX(n).doubleValue() + dx;
|
||||
double yn = series.getY(n).doubleValue() + dy;
|
||||
XYCoords end = new XYCoords(xn, yn);
|
||||
|
||||
// TODO: figure out whether this extra control-point synthesis is
|
||||
// TODO: really necessary and either remove the above or fix the below.
|
||||
// insert the start control point at the start of the vertices list.
|
||||
// TODO vertices.add(0, start);
|
||||
|
||||
// append the end control ponit to the end of the vertices list.
|
||||
// TODO vertices.add(end);
|
||||
//}
|
||||
|
||||
ExtrapolatedXYSeries extrapolatedXYSeries = new ExtrapolatedXYSeries(series, start, end);
|
||||
|
||||
// Dimension a result list of coordinates.
|
||||
List<XYCoords> result = new ArrayList<>();
|
||||
|
||||
// When looping, remember that each cycle requires 4 points, starting
|
||||
// with i and ending with i+3. So we don't loop through all the points.
|
||||
for (int i = 0; i < extrapolatedXYSeries.size() - 3; i++) {
|
||||
|
||||
// Actually calculate the Catmull-Rom curve for one segment.
|
||||
List<XYCoords> points = interpolate(extrapolatedXYSeries, i, params);
|
||||
|
||||
// Since the middle points are added twice, once for each bordering
|
||||
// segment, we only add the 0 index result point for the first
|
||||
// segment. Otherwise we will have duplicate points.
|
||||
if (result.size() > 0) {
|
||||
points.remove(0);
|
||||
}
|
||||
|
||||
// Add the coordinates for the segment to the result list.
|
||||
result.addAll(points);
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of control points, this will create a list of pointsPerSegment
|
||||
* points spaced uniformly along the resulting Catmull-Rom curve.
|
||||
*
|
||||
* @param series The list of control points, leading and ending with a
|
||||
* coordinate that is only used for controling the spline and is not visualized.
|
||||
* @param index The index of control point p0, where p0, p1, p2, and p3 are
|
||||
* used in order to create a curve between p1 and p2.
|
||||
* @return the list of coordinates that define the CatmullRom curve
|
||||
* between the points defined by index+1 and index+2.
|
||||
*/
|
||||
protected List<XYCoords> interpolate(XYSeries series, int index, Params params) {
|
||||
List<XYCoords> result = new ArrayList<>();
|
||||
double[] x = new double[4];
|
||||
double[] y = new double[4];
|
||||
double[] time = new double[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
x[i] = series.getX(index + i).doubleValue();
|
||||
y[i] = series.getY(index + i).doubleValue();
|
||||
time[i] = i;
|
||||
}
|
||||
|
||||
double tstart = 1;
|
||||
double tend = 2;
|
||||
if (params.getType() != Type.Uniform) {
|
||||
double total = 0;
|
||||
for (int i = 1; i < 4; i++) {
|
||||
double dx = x[i] - x[i - 1];
|
||||
double dy = y[i] - y[i - 1];
|
||||
if (params.getType() == Type.Centripetal) {
|
||||
total += Math.pow(dx * dx + dy * dy, .25);
|
||||
} else {
|
||||
total += Math.pow(dx * dx + dy * dy, .5);
|
||||
}
|
||||
time[i] = total;
|
||||
}
|
||||
tstart = time[1];
|
||||
tend = time[2];
|
||||
}
|
||||
|
||||
int segments = params.getPointPerSegment() - 1;
|
||||
result.add(new XYCoords(series.getX(index + 1), series.getY(index + 1)));
|
||||
for (int i = 1; i < segments; i++) {
|
||||
double xi = interpolate(x, time, tstart + (i * (tend - tstart)) / segments);
|
||||
double yi = interpolate(y, time, tstart + (i * (tend - tstart)) / segments);
|
||||
result.add(new XYCoords(xi, yi));
|
||||
}
|
||||
result.add(new XYCoords(series.getX(index + 2), series.getY(index + 2)));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlike the other implementation here, which uses the default "uniform"
|
||||
* treatment of t, this computation is used to calculate the same values but
|
||||
* introduces the ability to "parameterize" the t values used in the
|
||||
* calculation. This is based on Figure 3 from
|
||||
* http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
|
||||
*
|
||||
* @param p An array of double values of length 4, where interpolation
|
||||
* occurs from p1 to p2.
|
||||
* @param time An array of time measures of length 4, corresponding to each
|
||||
* p value.
|
||||
* @param t the actual interpolation ratio from 0 to 1 representing the
|
||||
* position between p1 and p2 to interpolate the value.
|
||||
* @return
|
||||
*/
|
||||
protected static double interpolate(double[] p, double[] time, double t) {
|
||||
double L01 = p[0] * (time[1] - t) / (time[1] - time[0]) + p[1] * (t - time[0]) / (time[1] - time[0]);
|
||||
double L12 = p[1] * (time[2] - t) / (time[2] - time[1]) + p[2] * (t - time[1]) / (time[2] - time[1]);
|
||||
double L23 = p[2] * (time[3] - t) / (time[3] - time[2]) + p[3] * (t - time[2]) / (time[3] - time[2]);
|
||||
double L012 = L01 * (time[2] - t) / (time[2] - time[0]) + L12 * (t - time[0]) / (time[2] - time[0]);
|
||||
double L123 = L12 * (time[3] - t) / (time[3] - time[1]) + L23 * (t - time[1]) / (time[3] - time[1]);
|
||||
double C12 = L012 * (time[2] - t) / (time[2] - time[1]) + L123 * (t - time[1]) / (time[2] - time[1]);
|
||||
return C12;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* An {@link XYSeries} that exposes methods to set values and resize
|
||||
*/
|
||||
public interface EditableXYSeries extends XYSeries {
|
||||
|
||||
void setX(Number x, int index);
|
||||
void setY(Number y, int index);
|
||||
|
||||
/**
|
||||
* Resize to accommodate the specified number of x/y pairs. If elements must be droped, those
|
||||
* at the highest iVal should be removed first.
|
||||
* @param size
|
||||
*/
|
||||
void resize(int size);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* Base for all estimation management schemes.
|
||||
*/
|
||||
public abstract class Estimator {
|
||||
|
||||
public abstract void run(XYPlot plot, XYSeriesBundle sf);
|
||||
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.androidplot.ui.RenderStack;
|
||||
import com.androidplot.ui.SeriesRenderer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A faster implementation of of {@link LineAndPointRenderer}. For performance reasons, has these constraints:
|
||||
* - Interpolation is not supported
|
||||
* - Does not draw fill
|
||||
* - Does not support null values
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public class FastLineAndPointRenderer extends XYSeriesRenderer<XYSeries, FastLineAndPointRenderer.Formatter> {
|
||||
|
||||
/**
|
||||
* A line drawn by {@link Canvas#drawLines(float[], int, int, Paint)} must be defined by at
|
||||
* least four points {@code x0, y0, x1, y1}
|
||||
*/
|
||||
private static final int MINIMUM_NUMBER_OF_POINTS_TO_DEFINE_A_LINE = 4;
|
||||
private float[] points;
|
||||
List<Integer> segmentOffsets = new ArrayList<>();
|
||||
List<Integer> segmentLengths = new ArrayList<>();
|
||||
public FastLineAndPointRenderer(XYPlot plot) {
|
||||
super(plot);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRender(Canvas canvas, RectF plotArea, XYSeries series, Formatter formatter, RenderStack stack) {
|
||||
|
||||
segmentOffsets.clear();
|
||||
segmentLengths.clear();
|
||||
|
||||
final int numPoints = series.size() * 2;
|
||||
if(points == null || points.length != numPoints) {
|
||||
// only allocate when necessary:
|
||||
points = new float[series.size()*2];
|
||||
}
|
||||
|
||||
int segmentLen = 0;
|
||||
boolean isLastPointNull = true;
|
||||
PointF resultPoint = new PointF();
|
||||
for (int i = 0, j = 0; i < series.size(); i++, j+=2) {
|
||||
Number y = series.getY(i);
|
||||
Number x = series.getX(i);
|
||||
|
||||
if (y != null && x != null) {
|
||||
if(isLastPointNull) {
|
||||
segmentOffsets.add(j);
|
||||
segmentLen = 0;
|
||||
isLastPointNull = false;
|
||||
}
|
||||
|
||||
getPlot().getBounds().transformScreen(resultPoint, x, y, plotArea);
|
||||
points[j] = resultPoint.x;
|
||||
points[j + 1] = resultPoint.y;
|
||||
segmentLen+=2;
|
||||
|
||||
// if this is the last point, account for it in segment lengths:
|
||||
if(i == series.size()-1) {
|
||||
segmentLengths.add(segmentLen);
|
||||
}
|
||||
} else if(!isLastPointNull) {
|
||||
segmentLengths.add(segmentLen);
|
||||
isLastPointNull = true;
|
||||
}
|
||||
}
|
||||
|
||||
// draw segments
|
||||
if(formatter.linePaint != null || formatter.vertexPaint != null) {
|
||||
for (int i = 0; i < segmentOffsets.size(); i++) {
|
||||
final int len = segmentLengths.get(i);
|
||||
final int offset = segmentOffsets.get(i);
|
||||
drawSegment(canvas, points, offset, len, formatter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawSegment(@NonNull Canvas canvas,
|
||||
@NonNull float[] points,
|
||||
int offset,
|
||||
int len,
|
||||
Formatter formatter) {
|
||||
if(formatter.linePaint != null) {
|
||||
// draw lines:
|
||||
if (len >= MINIMUM_NUMBER_OF_POINTS_TO_DEFINE_A_LINE) {
|
||||
// optimization to avoid using 2x storage space to represent the full path:
|
||||
if ((len & 2) != 0) {
|
||||
canvas.drawLines(points, offset, len - 2, formatter.linePaint);
|
||||
canvas.drawLines(points, offset + 2, len - 2, formatter.linePaint);
|
||||
} else {
|
||||
canvas.drawLines(points, offset, len, formatter.linePaint);
|
||||
canvas.drawLines(points, offset + 2, len - 4, formatter.linePaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(formatter.vertexPaint != null) {
|
||||
// draw vertices:
|
||||
canvas.drawPoints(points, offset, len, formatter.vertexPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDrawLegendIcon(@NonNull Canvas canvas,
|
||||
@NonNull RectF rect,
|
||||
@NonNull Formatter formatter) {
|
||||
if(formatter.hasLinePaint()) {
|
||||
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
|
||||
}
|
||||
|
||||
if(formatter.hasVertexPaint()) {
|
||||
canvas.drawPoint(rect.centerX(), rect.centerY(), formatter.getVertexPaint());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatter designed to work in tandem with {@link AdvancedLineAndPointRenderer}.
|
||||
* @since 0.9.9
|
||||
*/
|
||||
public static class Formatter extends LineAndPointFormatter {
|
||||
|
||||
public Formatter(Integer lineColor, Integer vertexColor, PointLabelFormatter plf) {
|
||||
super(lineColor, vertexColor, null, plf);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initLinePaint(Integer lineColor) {
|
||||
super.initLinePaint(lineColor);
|
||||
|
||||
// disable anti-aliasing by default:
|
||||
getLinePaint().setAntiAlias(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends SeriesRenderer> getRendererClass() {
|
||||
return FastLineAndPointRenderer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
|
||||
return new FastLineAndPointRenderer(plot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* An implementation of {@link XYSeries} that defines additional methods to speed up rendering by
|
||||
* giving a hint to the renderer about the min/max values contained in the series.
|
||||
*
|
||||
* Note that these hints can only be leveraged if the containing XYPlot's constraints completely
|
||||
* contain the FastXYSeries min/max values. If this condition is not met then XYPlot falls back
|
||||
* to manually determining the min/max values of the series that exist within the defined constraints.
|
||||
*/
|
||||
public interface FastXYSeries extends XYSeries {
|
||||
|
||||
/**
|
||||
* TIP: You can use {@link RectRegion#union(Number, Number)} during
|
||||
* to keep a running tally of min/max values when iterating.
|
||||
* @return A {@link RectRegion} representing the min/max values that currently exist this series.
|
||||
*/
|
||||
RectRegion minMax();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* Defines which edge is used to close a fill path for drawing lines.
|
||||
*
|
||||
* TOP - Use the top edge of the plot.
|
||||
* BOTTOM - Use the bottom edge of the plot.
|
||||
* LEFT - (Not implemented) Use the left edge of the plot.
|
||||
* RIGHT - (Not implemented) Use the right edge of the plot.
|
||||
* DOMAIN_ORIGIN - (Not implemented) Use the domain origin line.
|
||||
* RANGE_ORIGIN - Use the range origin line.
|
||||
*/
|
||||
public enum FillDirection {
|
||||
TOP,
|
||||
BOTTOM,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
DOMAIN_ORIGIN,
|
||||
RANGE_ORIGIN
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.androidplot.util.FastNumber;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An efficient implementation of {@link EditableXYSeries} intended for use cases where
|
||||
* the total number of points visible is known ahead of time and is fairly static.
|
||||
*
|
||||
* {@link #resize(int)} may be used to resize the series when necessary, however it is a slow
|
||||
* operation and should be avoided as much as possible.
|
||||
*
|
||||
*/
|
||||
public class FixedSizeEditableXYSeries implements EditableXYSeries {
|
||||
|
||||
@NonNull
|
||||
private List<FastNumber> xVals = new ArrayList<>();
|
||||
|
||||
@NonNull
|
||||
private List<FastNumber> yVals = new ArrayList<>();
|
||||
private String title;
|
||||
|
||||
public FixedSizeEditableXYSeries(String title, int size) {
|
||||
setTitle(title);
|
||||
resize(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setX(@Nullable Number x, int index) {
|
||||
xVals.set(index, FastNumber.orNull(x));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setY(@Nullable Number y, int index) {
|
||||
yVals.set(index, FastNumber.orNull(y));
|
||||
}
|
||||
|
||||
/**
|
||||
* May be used to dynamically resize the series. This is a relatively slow operation, especially
|
||||
* as size increases so care should be taken to avoid unnecessary usage.
|
||||
* @param size
|
||||
*/
|
||||
@Override
|
||||
public void resize(int size) {
|
||||
resize(xVals, size);
|
||||
resize(yVals, size);
|
||||
}
|
||||
|
||||
protected void resize(@NonNull List list, int size) {
|
||||
if (size > list.size()) {
|
||||
while (list.size() < size) {
|
||||
list.add(null);
|
||||
}
|
||||
} else if (size < list.size()) {
|
||||
while (list.size() > size) {
|
||||
list.remove(list.size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return xVals.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getX(int index) {
|
||||
return xVals.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getY(int index) {
|
||||
return yVals.get(index);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2016 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.RectF;
|
||||
import android.util.Log;
|
||||
import com.androidplot.ui.RenderStack;
|
||||
import com.androidplot.ui.SeriesBundle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Renders data to an XYPlot that potentially contains more than a single yVal per index, or in other
|
||||
* words data with potentially more than two dimensions.
|
||||
* Examples of such data are bar plot groups and candlestick charts.
|
||||
* @since 0.9.7
|
||||
*/
|
||||
public abstract class GroupRenderer<FormatterType extends XYSeriesFormatter<XYRegionFormatter>>
|
||||
extends XYSeriesRenderer<XYSeries, FormatterType> {
|
||||
|
||||
private static final String TAG = GroupRenderer.class.getName();
|
||||
|
||||
public GroupRenderer(XYPlot plot) {
|
||||
super(plot);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRender(Canvas canvas, RectF plotArea, XYSeries series,
|
||||
FormatterType formatter, RenderStack stack) {
|
||||
|
||||
|
||||
// get all the data associated with this renderer:
|
||||
List<SeriesBundle<XYSeries, ? extends FormatterType>> sfList = getSeriesAndFormatterList();
|
||||
|
||||
// no data to render so exit:
|
||||
if(sfList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int seriesLength = sfList.get(0).getSeries().size();
|
||||
|
||||
// make sure all associated series are the same length:
|
||||
for(int i = 1; i < sfList.size(); i++) {
|
||||
if(sfList.get(i).getSeries().size() != seriesLength) {
|
||||
// series sizes are irregular so don't try to render:
|
||||
Log.w(TAG, getClass() + ": " + "not all associated series are of same size.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// this renderer uses special element-by-element z-indexing that results in
|
||||
// all series associated with the renderer being rendered in a single pass, so
|
||||
// we need to exclude the rest of the series on the render stack from being redrawn later:
|
||||
stack.disable(getClass());
|
||||
|
||||
onRender(canvas, plotArea, sfList, seriesLength, stack);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param canvas
|
||||
* @param plotArea
|
||||
* @param sfList
|
||||
* @param stack
|
||||
*/
|
||||
public abstract void onRender(Canvas canvas, RectF plotArea, List<SeriesBundle<XYSeries,
|
||||
? extends FormatterType>> sfList, int size, RenderStack stack);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* Created by nick_f on 9/25/14.
|
||||
*/
|
||||
public interface InterpolationParams<InterpolatorType extends Interpolator> {
|
||||
|
||||
Class<InterpolatorType> getInterpolatorClass();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by nick_f on 9/25/14.
|
||||
*/
|
||||
public interface Interpolator<ParamsType extends InterpolationParams> {
|
||||
|
||||
|
||||
List<XYCoords> interpolate(XYSeries series, ParamsType params);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.util.*;
|
||||
|
||||
/**
|
||||
* Adapted from:
|
||||
* https://github.com/drcrane/downsample
|
||||
*
|
||||
* Note that this implementation does not yet support null values.
|
||||
*
|
||||
* Basic usage example:
|
||||
* <pre>
|
||||
* {@code
|
||||
* // An instance of any implementation of XYSeries; SimpleXYSeries, etc:
|
||||
* XYSeries origalSeries = ...;
|
||||
*
|
||||
* // Sampled series with half the resolution of the
|
||||
* EditableXYSeries sampledSeries = new FixedSizeEditableXYSeries(
|
||||
* origalSeries.getTitle(), origalSeries.size() / 2);
|
||||
*
|
||||
* // does the actual sampling:
|
||||
* new LTTBSampler().run(origalSeries, sampledSeries);
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public class LTTBSampler implements Sampler {
|
||||
|
||||
public RectRegion run(XYSeries rawData, EditableXYSeries sampled) {
|
||||
RectRegion bounds = new RectRegion();
|
||||
final int threshold = sampled.size();
|
||||
final int dataLength = rawData.size();
|
||||
final int startIndex = 0;
|
||||
|
||||
if (threshold >= dataLength || threshold == 0) {
|
||||
//return data; // Nothing to do
|
||||
// TODO: set flag to return raw data
|
||||
throw new RuntimeException("Shouldnt be here!");
|
||||
}
|
||||
|
||||
int sampledIndex = 0;
|
||||
// Bucket size. Leave room for start and end data points
|
||||
final double bucketSize = (double) (dataLength - 2) / (threshold - 2);
|
||||
int a = 0; // Initially a is the first point in the triangle
|
||||
int nextA = 0;
|
||||
setSample(rawData, sampled, a + startIndex, sampledIndex, bounds);
|
||||
sampledIndex++;
|
||||
for (int i = 0; i < threshold - 2; i++) {
|
||||
// Calculate point average for next bucket (containing c)
|
||||
double pointCX = 0;
|
||||
double pointCY = 0;
|
||||
int pointCStart = (int) Math.floor((i + 1) * bucketSize) + 1;
|
||||
int pointCEnd = (int) Math.floor((i + 2) * bucketSize) + 1;
|
||||
pointCEnd = pointCEnd < dataLength ? pointCEnd : dataLength;
|
||||
final int pointCSize = pointCEnd - pointCStart;
|
||||
for (; pointCStart < pointCEnd; pointCStart++) {
|
||||
if(rawData.getX(pointCStart + startIndex) != null) {
|
||||
pointCX += rawData.getX(pointCStart + startIndex).doubleValue();
|
||||
}
|
||||
|
||||
if(rawData.getY(pointCStart + startIndex) != null) {
|
||||
pointCY += rawData.getY(pointCStart + startIndex).doubleValue();
|
||||
}
|
||||
}
|
||||
pointCX /= pointCSize;
|
||||
pointCY /= pointCSize;
|
||||
double pointAX = rawData.getX(a + startIndex).doubleValue();
|
||||
double pointAY = rawData.getY(a + startIndex).doubleValue();
|
||||
// Get the range for bucket b
|
||||
int pointBStart = (int) Math.floor((i + 0) * bucketSize) + 1;
|
||||
final int pointBEnd = (int) Math.floor((i + 1) * bucketSize) + 1;
|
||||
double maxArea = -1;
|
||||
XYCoords maxAreaPoint = null;
|
||||
for (; pointBStart < pointBEnd; pointBStart++) {
|
||||
final double area = Math.abs((pointAX - pointCX) * (rawData.getY(pointBStart + startIndex)
|
||||
.doubleValue() - pointAY) - (pointAX - rawData.getX(pointBStart + startIndex)
|
||||
.doubleValue())
|
||||
* (pointCY - pointAY)) * 0.5;
|
||||
if (area > maxArea) {
|
||||
if(rawData.getY(pointBStart + startIndex) == null) {
|
||||
Log.i("LTTB", "Null value encountered in raw data at index: " + pointBStart);
|
||||
}
|
||||
maxArea = area;
|
||||
maxAreaPoint = new XYCoords(rawData.getX(pointBStart + startIndex),
|
||||
rawData.getY(pointBStart + startIndex));
|
||||
nextA = pointBStart; // Next a is this b
|
||||
}
|
||||
}
|
||||
setSample(sampled, maxAreaPoint.x, maxAreaPoint.y, sampledIndex, bounds);
|
||||
sampledIndex++;
|
||||
a = nextA; // This a is the next a (chosen b)
|
||||
}
|
||||
setSample(rawData, sampled, (dataLength + startIndex) - 1, sampledIndex, bounds);
|
||||
sampledIndex++;
|
||||
return bounds;
|
||||
}
|
||||
|
||||
protected void setSample(XYSeries raw, EditableXYSeries sampled, int rawIndex, int sampleIndex, RectRegion bounds) {
|
||||
setSample(sampled, raw.getX(rawIndex), raw.getY(rawIndex), sampleIndex, bounds);
|
||||
}
|
||||
|
||||
protected void setSample(EditableXYSeries sampled, Number x, Number y, int sampleIndex, RectRegion bounds) {
|
||||
bounds.union(x, y);
|
||||
sampled.setX(x, sampleIndex);
|
||||
sampled.setY(y, sampleIndex);
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import com.androidplot.ui.SeriesRenderer;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
/**
|
||||
* Defines the visual aesthetics of an XYSeries; outline color and width, fill style,
|
||||
* vertex size and color, shadowing, etc.
|
||||
*/
|
||||
public class LineAndPointFormatter extends XYSeriesFormatter<XYRegionFormatter> {
|
||||
|
||||
private static final float DEFAULT_LINE_STROKE_WIDTH_DP = 1.5f;
|
||||
private static final float DEFAULT_VERTEX_STROKE_WIDTH_DP = 4.5f;
|
||||
|
||||
public FillDirection getFillDirection() {
|
||||
return fillDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets which edge to use to close the line's path for filling purposes.
|
||||
* See {@link FillDirection}.
|
||||
* @param fillDirection
|
||||
*/
|
||||
public void setFillDirection(FillDirection fillDirection) {
|
||||
this.fillDirection = fillDirection;
|
||||
}
|
||||
|
||||
protected FillDirection fillDirection = FillDirection.BOTTOM;
|
||||
protected Paint linePaint;
|
||||
protected Paint vertexPaint;
|
||||
protected Paint fillPaint;
|
||||
protected InterpolationParams interpolationParams;
|
||||
|
||||
public LineAndPointFormatter(Context context, int xmlCfgId) {
|
||||
super(context, xmlCfgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should only be used in conjunction with calls to configure()...
|
||||
*/
|
||||
public LineAndPointFormatter() {
|
||||
this(Color.RED, Color.GREEN, Color.BLUE, null);
|
||||
}
|
||||
|
||||
public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor,
|
||||
PointLabelFormatter plf) {
|
||||
this(lineColor, vertexColor, fillColor, plf, FillDirection.BOTTOM);
|
||||
}
|
||||
|
||||
public LineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor,
|
||||
PointLabelFormatter plf, FillDirection fillDir) {
|
||||
initLinePaint(lineColor);
|
||||
initVertexPaint(vertexColor);
|
||||
initFillPaint(fillColor);
|
||||
setFillDirection(fillDir);
|
||||
this.setPointLabelFormatter(plf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends SeriesRenderer> getRendererClass() {
|
||||
return LineAndPointRenderer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeriesRenderer doGetRendererInstance(XYPlot plot) {
|
||||
return new LineAndPointRenderer(plot);
|
||||
}
|
||||
|
||||
protected void initLinePaint(Integer lineColor) {
|
||||
if (lineColor == null) {
|
||||
linePaint = null;
|
||||
} else {
|
||||
linePaint = new Paint();
|
||||
linePaint.setAntiAlias(true);
|
||||
linePaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_LINE_STROKE_WIDTH_DP));
|
||||
linePaint.setColor(lineColor);
|
||||
linePaint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
}
|
||||
|
||||
protected void initVertexPaint(Integer vertexColor) {
|
||||
if (vertexColor == null) {
|
||||
vertexPaint = null;
|
||||
} else {
|
||||
vertexPaint = new Paint();
|
||||
vertexPaint.setAntiAlias(true);
|
||||
vertexPaint.setStrokeWidth(PixelUtils.dpToPix(DEFAULT_VERTEX_STROKE_WIDTH_DP));
|
||||
vertexPaint.setColor(vertexColor);
|
||||
vertexPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
}
|
||||
|
||||
protected void initFillPaint(Integer fillColor) {
|
||||
if (fillColor == null) {
|
||||
fillPaint = null;
|
||||
} else {
|
||||
fillPaint = new Paint();
|
||||
fillPaint.setAntiAlias(true);
|
||||
fillPaint.setColor(fillColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return True if linePaint has been set, false otherwise.
|
||||
*/
|
||||
public boolean hasLinePaint() {
|
||||
return linePaint != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Paint} used to draw lines. Will instantiate and a new default instance
|
||||
* if it is currently null. To run whether or not line paint has been set, use
|
||||
* {@link #hasLinePaint()}.
|
||||
* @return
|
||||
*/
|
||||
public Paint getLinePaint() {
|
||||
if(linePaint == null) {
|
||||
initLinePaint(Color.TRANSPARENT);
|
||||
}
|
||||
return linePaint;
|
||||
}
|
||||
|
||||
public void setLinePaint(Paint linePaint) {
|
||||
this.linePaint = linePaint;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return True if vertexPaint has been set, false otherwise.
|
||||
*/
|
||||
public boolean hasVertexPaint() {
|
||||
return vertexPaint != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Paint} used to draw vertices (points). Will instantiate and a new default instance
|
||||
* if it is currently null. To run whether or not vertex paint has been set, use
|
||||
* {@link #hasVertexPaint()}.
|
||||
* @return
|
||||
*/
|
||||
public Paint getVertexPaint() {
|
||||
if(vertexPaint == null) {
|
||||
initVertexPaint(Color.TRANSPARENT);
|
||||
}
|
||||
return vertexPaint;
|
||||
}
|
||||
|
||||
public void setVertexPaint(Paint vertexPaint) {
|
||||
this.vertexPaint = vertexPaint;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return True if fillPaint has been set, false otherwise.
|
||||
*/
|
||||
public boolean hasFillPaint() {
|
||||
return fillPaint != null;
|
||||
}
|
||||
/**
|
||||
* Get the {@link Paint} used to fill series areas. Will instantiate and a new default instance
|
||||
* if it is currently null. To run whether or not fill paint has been set, use
|
||||
* {@link #hasFillPaint()}.
|
||||
* @return
|
||||
*/
|
||||
public Paint getFillPaint() {
|
||||
if(fillPaint == null) {
|
||||
initFillPaint(Color.TRANSPARENT);
|
||||
}
|
||||
return fillPaint;
|
||||
}
|
||||
|
||||
public void setFillPaint(Paint fillPaint) {
|
||||
this.fillPaint = fillPaint;
|
||||
}
|
||||
|
||||
public InterpolationParams getInterpolationParams() {
|
||||
return interpolationParams;
|
||||
}
|
||||
|
||||
public void setInterpolationParams(InterpolationParams params) {
|
||||
this.interpolationParams = params;
|
||||
}
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import com.androidplot.Plot;
|
||||
import com.androidplot.PlotListener;
|
||||
import com.androidplot.Region;
|
||||
import com.androidplot.ui.RenderStack;
|
||||
import com.androidplot.util.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Renders a point as a line with the vertices marked. Requires 2 or more points to
|
||||
* be rendered.
|
||||
*/
|
||||
public class LineAndPointRenderer<FormatterType extends LineAndPointFormatter> extends XYSeriesRenderer<XYSeries, FormatterType> {
|
||||
|
||||
protected static final int ZERO = 0;
|
||||
protected static final int ONE = 1;
|
||||
|
||||
private final Path path = new Path();
|
||||
|
||||
protected final ConcurrentHashMap<XYSeries, ArrayList<PointF>> pointsCaches
|
||||
= new ConcurrentHashMap<>(2, 0.75f, 2);
|
||||
|
||||
public LineAndPointRenderer(XYPlot plot) {
|
||||
super(plot);
|
||||
plot.addListener(new PlotListener() {
|
||||
@Override
|
||||
public void onBeforeDraw(Plot source, Canvas canvas) {
|
||||
cullPointsCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAfterDraw(Plot source, Canvas canvas) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRender(Canvas canvas, RectF plotArea, XYSeries series, FormatterType formatter, RenderStack stack) {
|
||||
drawSeries(canvas, plotArea, series, formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doDrawLegendIcon(Canvas canvas, RectF rect, LineAndPointFormatter formatter) {
|
||||
// horizontal icon:
|
||||
float centerY = rect.centerY();
|
||||
float centerX = rect.centerX();
|
||||
|
||||
if(formatter.getFillPaint() != null) {
|
||||
canvas.drawRect(rect, formatter.getFillPaint());
|
||||
}
|
||||
if(formatter.hasLinePaint()) {
|
||||
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.top, formatter.getLinePaint());
|
||||
}
|
||||
|
||||
if(formatter.hasVertexPaint()) {
|
||||
canvas.drawPoint(centerX, centerY, formatter.getVertexPaint());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method exists for StepRenderer to override without having to duplicate any
|
||||
* additional code.
|
||||
*/
|
||||
protected void appendToPath(Path path, PointF thisPoint, PointF lastPoint) {
|
||||
|
||||
path.lineTo(thisPoint.x, thisPoint.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves or initializes a list for storing calculated screen-coords to render as points.
|
||||
* Also handles automatic resizing and culling of unused caches.
|
||||
* Should only be called once per render cycle.
|
||||
* @param series
|
||||
* @return
|
||||
*/
|
||||
protected ArrayList<PointF> getPointsCache(XYSeries series) {
|
||||
ArrayList<PointF> pointsCache = pointsCaches.get(series);
|
||||
final int seriesSize = series.size();
|
||||
if(pointsCache == null) {
|
||||
pointsCache = new ArrayList<>(seriesSize);
|
||||
pointsCaches.put(series, pointsCache);
|
||||
}
|
||||
|
||||
if(pointsCache.size() < seriesSize) {
|
||||
while(pointsCache.size() < seriesSize) {
|
||||
pointsCache.add(null);
|
||||
}
|
||||
} else if(pointsCache.size() > seriesSize) {
|
||||
while(pointsCache.size() > seriesSize) {
|
||||
pointsCache.remove(0);
|
||||
}
|
||||
}
|
||||
return pointsCache;
|
||||
}
|
||||
|
||||
protected void cullPointsCache() {
|
||||
for(XYSeries series : pointsCaches.keySet()) {
|
||||
if(!getPlot().getRegistry().contains(series, LineAndPointFormatter.class)) {
|
||||
pointsCaches.remove(series);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) {
|
||||
PointF thisPoint;
|
||||
PointF lastPoint = null;
|
||||
PointF firstPoint = null;
|
||||
path.reset();
|
||||
final List<PointF> points = getPointsCache(series);
|
||||
|
||||
int iStart = 0;
|
||||
int iEnd = series.size();
|
||||
if(SeriesUtils.getXYOrder(series) == OrderedXYSeries.XOrder.ASCENDING) {
|
||||
final Region iBounds = SeriesUtils.iBounds(series, getPlot().getBounds());
|
||||
iStart = iBounds.getMin().intValue();
|
||||
if(iStart > 0) {
|
||||
iStart--;
|
||||
}
|
||||
iEnd = iBounds.getMax().intValue() + 1;
|
||||
if(iEnd < series.size() - 1) {
|
||||
iEnd++;
|
||||
}
|
||||
}
|
||||
for (int i = iStart; i < iEnd; i++) {
|
||||
final Number y = series.getY(i);
|
||||
final Number x = series.getX(i);
|
||||
PointF iPoint = points.get(i);
|
||||
|
||||
if (y != null && x != null) {
|
||||
if(iPoint == null) {
|
||||
iPoint = new PointF();
|
||||
points.set(i, iPoint);
|
||||
}
|
||||
thisPoint = iPoint;
|
||||
getPlot().getBounds().transformScreen(thisPoint, x, y, plotArea);
|
||||
} else {
|
||||
thisPoint = null;
|
||||
iPoint = null;
|
||||
points.set(i, iPoint);
|
||||
}
|
||||
|
||||
// don't need to do any of this if the line isnt going to be drawn:
|
||||
if(formatter.hasLinePaint() && formatter.getInterpolationParams() == null) {
|
||||
if (thisPoint != null) {
|
||||
|
||||
// record the first point of the new Path
|
||||
if (firstPoint == null) {
|
||||
path.reset();
|
||||
firstPoint = thisPoint;
|
||||
|
||||
// create our first point at the bottom/x position so filling will look good:
|
||||
path.moveTo(firstPoint.x, firstPoint.y);
|
||||
}
|
||||
|
||||
if (lastPoint != null) {
|
||||
appendToPath(path, thisPoint, lastPoint);
|
||||
}
|
||||
|
||||
lastPoint = thisPoint;
|
||||
} else {
|
||||
if (lastPoint != null) {
|
||||
renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter);
|
||||
}
|
||||
firstPoint = null;
|
||||
lastPoint = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(formatter.hasLinePaint()) {
|
||||
if(formatter.getInterpolationParams() != null) {
|
||||
List<XYCoords> interpolatedPoints = getInterpolator(
|
||||
formatter.getInterpolationParams()).interpolate(series,
|
||||
formatter.getInterpolationParams());
|
||||
firstPoint = convertPoint(interpolatedPoints.get(ZERO), plotArea);
|
||||
lastPoint = convertPoint(interpolatedPoints.get(interpolatedPoints.size()-ONE), plotArea);
|
||||
path.reset();
|
||||
path.moveTo(firstPoint.x, firstPoint.y);
|
||||
for(int i = 1; i < interpolatedPoints.size(); i++) {
|
||||
thisPoint = convertPoint(interpolatedPoints.get(i), plotArea);
|
||||
path.lineTo(thisPoint.x, thisPoint.y);
|
||||
}
|
||||
}
|
||||
|
||||
if(firstPoint != null) {
|
||||
renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter);
|
||||
}
|
||||
}
|
||||
renderPoints(canvas, plotArea, series, iStart, iEnd, points, formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: retrieve from a persistent registry
|
||||
* @param params
|
||||
* @return An interpol
|
||||
*/
|
||||
protected Interpolator getInterpolator(InterpolationParams params) {
|
||||
try {
|
||||
return (Interpolator) params.getInterpolatorClass().newInstance();
|
||||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected PointF convertPoint(XYCoords coord, RectF plotArea) {
|
||||
return getPlot().getBounds().transformScreen(coord, plotArea);
|
||||
}
|
||||
|
||||
protected void renderPoints(Canvas canvas, RectF plotArea, XYSeries series, int iStart, int iEnd, List<PointF> points,
|
||||
LineAndPointFormatter formatter) {
|
||||
if (formatter.hasVertexPaint() || formatter.hasPointLabelFormatter()) {
|
||||
final Paint vertexPaint = formatter.hasVertexPaint() ? formatter.getVertexPaint() : null;
|
||||
final boolean hasPointLabelFormatter = formatter.hasPointLabelFormatter();
|
||||
final PointLabelFormatter plf = hasPointLabelFormatter ? formatter.getPointLabelFormatter() : null;
|
||||
final PointLabeler pointLabeler = hasPointLabelFormatter ? formatter.getPointLabeler() : null;
|
||||
for(int i = iStart; i < iEnd; i++) {
|
||||
PointF p = points.get(i);
|
||||
if(p != null) {
|
||||
|
||||
// if vertexPaint is available, draw vertex:
|
||||
if (vertexPaint != null) {
|
||||
canvas.drawPoint(p.x, p.y, vertexPaint);
|
||||
}
|
||||
|
||||
// if textPaint and pointLabeler are available, draw point's text label:
|
||||
if (pointLabeler != null) {
|
||||
canvas.drawText(pointLabeler.getLabel(series, i),
|
||||
p.x + plf.hOffset, p.y + plf.vOffset, plf.getTextPaint());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void renderPath(Canvas canvas, RectF plotArea, Path path, PointF firstPoint, PointF lastPoint, LineAndPointFormatter formatter) {
|
||||
Path outlinePath = new Path(path);
|
||||
|
||||
// determine how to close the path for filling purposes:
|
||||
// We always need to calculate this path because it is also used for
|
||||
// masking off for region highlighting.
|
||||
switch (formatter.getFillDirection()) {
|
||||
case BOTTOM:
|
||||
path.lineTo(lastPoint.x, plotArea.bottom);
|
||||
path.lineTo(firstPoint.x, plotArea.bottom);
|
||||
path.close();
|
||||
break;
|
||||
case TOP:
|
||||
path.lineTo(lastPoint.x, plotArea.top);
|
||||
path.lineTo(firstPoint.x, plotArea.top);
|
||||
path.close();
|
||||
break;
|
||||
case RANGE_ORIGIN:
|
||||
float originPix = (float) getPlot().getBounds().getxRegion()
|
||||
.transform(getPlot().getRangeOrigin()
|
||||
.doubleValue(), plotArea.top, plotArea.bottom, true);
|
||||
path.lineTo(lastPoint.x, originPix);
|
||||
path.lineTo(firstPoint.x, originPix);
|
||||
path.close();
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException(
|
||||
"Fill direction not yet implemented: " + formatter.getFillDirection());
|
||||
}
|
||||
|
||||
if (formatter.getFillPaint() != null) {
|
||||
canvas.drawPath(path, formatter.getFillPaint());
|
||||
}
|
||||
|
||||
final RectRegion bounds = getPlot().getBounds();
|
||||
final RectRegion plotRegion = new RectRegion(plotArea);
|
||||
|
||||
// draw each region:
|
||||
for (RectRegion thisRegion : bounds.intersects(formatter.getRegions().elements())) {
|
||||
XYRegionFormatter regionFormatter = formatter.getRegionFormatter(thisRegion);
|
||||
RectRegion thisRegionTransformed = bounds
|
||||
.transform(thisRegion, plotRegion, false, true);
|
||||
thisRegionTransformed.intersect(plotRegion);
|
||||
if(thisRegion.isFullyDefined()) {
|
||||
RectF thisRegionRectF = thisRegionTransformed.asRectF();
|
||||
if (thisRegionRectF != null) {
|
||||
try {
|
||||
canvas.save();
|
||||
canvas.clipPath(path);
|
||||
canvas.drawRect(thisRegionRectF, regionFormatter.getPaint());
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finally we draw the outline path on top of everything else:
|
||||
if(formatter.hasLinePaint()) {
|
||||
canvas.drawPath(outlinePath, formatter.getLinePaint());
|
||||
}
|
||||
|
||||
path.rewind();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
import com.androidplot.Region;
|
||||
import com.androidplot.util.SeriesUtils;
|
||||
|
||||
/**
|
||||
* Wrapper implementation of {@link XYSeries} that wraps another XYSeries, normalizing values in the range of 0 to 1.
|
||||
* Note that it's possible to push normed values outside of the standard 0, 1 range by applying
|
||||
* a sufficiently large offset.
|
||||
*/
|
||||
public class NormedXYSeries implements XYSeries {
|
||||
|
||||
private XYSeries rawData;
|
||||
|
||||
private Region minMaxX;
|
||||
private Region minMaxY;
|
||||
|
||||
private Region transformX;
|
||||
private Region transformY;
|
||||
|
||||
public static class Norm {
|
||||
|
||||
final Region minMax;
|
||||
final double offset;
|
||||
final boolean useOffsetCompression;
|
||||
|
||||
public Norm(Region minMax) {
|
||||
this(minMax, 0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param minMax Boundary to use when calculating the norm coefficient. Set to null to let
|
||||
* Androidplot auto calculate the bounds. (Very inefficient)
|
||||
* @param offset An extra offset to apply, generally within the range of -1 and 1.
|
||||
* This value is useful for adjusting the positioning of a series relative to another normalized series.
|
||||
* @param useOffsetCompression If true, the offset value will result in further scaling down
|
||||
* of the series data in order to ensure that all points within the specified bounds remain
|
||||
* visible on the screen. If set to true, the specified offset MUST be > -1 and < 1. Will be
|
||||
* ignored if bounds != null.
|
||||
*/
|
||||
public Norm(Region minMax, double offset, boolean useOffsetCompression) {
|
||||
this.minMax = minMax;
|
||||
this.offset = offset;
|
||||
this.useOffsetCompression = useOffsetCompression;
|
||||
|
||||
if (useOffsetCompression && (offset <= -1 || offset >= 1)) {
|
||||
throw new IllegalArgumentException(
|
||||
"When useOffsetCompression is true, offset must be > -1 and < 1.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes yVals only, auto calculating min/max.
|
||||
* @param rawData
|
||||
*/
|
||||
public NormedXYSeries(XYSeries rawData) {
|
||||
this(rawData, null, new Norm(null, 0, false));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rawData The XYSeries to be normalized.
|
||||
* @param x Normalization to apply to xVals. Set to null to disable normalization on the x axis.
|
||||
* @param y Normalization to apply to yVals. Set to null to disable normalization on the y axis.
|
||||
*/
|
||||
public NormedXYSeries(XYSeries rawData, Norm x, Norm y) {
|
||||
this.rawData = rawData;
|
||||
normalize(x, y);
|
||||
}
|
||||
|
||||
protected void normalize(Norm x, Norm y) {
|
||||
if( x != null) {
|
||||
this.minMaxX = x.minMax != null ? x.minMax : SeriesUtils.minMaxX(rawData);
|
||||
this.transformX = calculateTransform(x);
|
||||
}
|
||||
|
||||
if( y != null) {
|
||||
this.minMaxY = y.minMax != null ? y.minMax : SeriesUtils.minMaxY(rawData);
|
||||
this.transformY = calculateTransform(y);
|
||||
}
|
||||
}
|
||||
|
||||
protected Region calculateTransform(Norm norm) {
|
||||
if(norm.useOffsetCompression) {
|
||||
return new Region(
|
||||
norm.offset > 0 ? norm.offset : 0,
|
||||
norm.offset < 0 ? 1 + norm.offset : 1);
|
||||
} else {
|
||||
return new Region(0 + norm.offset, 1 + norm.offset);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return rawData.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return rawData.size();
|
||||
}
|
||||
|
||||
public Number denormalizeXVal(Number xVal) {
|
||||
if(xVal != null) {
|
||||
return transformX.transform(xVal.doubleValue(), minMaxX);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Number denormalizeYVal(Number yVal) {
|
||||
if(yVal != null) {
|
||||
return transformY.transform(yVal.doubleValue(), minMaxY);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getX(int index) {
|
||||
final Number xVal = rawData.getX(index);
|
||||
if(xVal != null && transformX != null) {
|
||||
return minMaxX.transform(xVal.doubleValue(), transformX);
|
||||
}
|
||||
return xVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getY(int index) {
|
||||
final Number yVal = rawData.getY(index);
|
||||
if(yVal != null && transformY != null) {
|
||||
return minMaxY.transform(yVal.doubleValue(), transformY);
|
||||
}
|
||||
return yVal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* An implementation of {@link XYSeries} that gives hints to it's renderer about the order
|
||||
* of the data being rendered.
|
||||
*/
|
||||
public interface OrderedXYSeries extends XYSeries {
|
||||
|
||||
enum XOrder {
|
||||
/**
|
||||
* XVals are in strict ascending order such that:
|
||||
* x(i) < x(i+1) == true
|
||||
*/
|
||||
ASCENDING,
|
||||
|
||||
/**
|
||||
* XVals are in strict descending order such that:
|
||||
* x(i) > x(i+1) == true
|
||||
*/
|
||||
DESCENDING,
|
||||
|
||||
/**
|
||||
* XVals appear in no particular order.
|
||||
*/
|
||||
NONE
|
||||
}
|
||||
|
||||
/**
|
||||
* The order of XVals as they appear in this series.
|
||||
* @return
|
||||
*/
|
||||
XOrder getXOrder();
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.PointF;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.*;
|
||||
|
||||
import com.androidplot.*;
|
||||
import com.androidplot.util.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Enables basic pan/zoom touch behavior for an {@link XYPlot}.
|
||||
* By default boundaries there are no boundaries imposed on scrolling and zooming. You can provide these boundaries
|
||||
* on your {@link XYPlot} using {@link XYPlot#getOuterLimits()}.
|
||||
* TODO: zoom using dynamic center point
|
||||
* TODO: stretch both mode
|
||||
*/
|
||||
public class PanZoom implements View.OnTouchListener {
|
||||
|
||||
protected static final float MIN_DIST_2_FING = 5f;
|
||||
protected static final int FIRST_FINGER = 0;
|
||||
protected static final int SECOND_FINGER = 1;
|
||||
|
||||
private XYPlot plot;
|
||||
private Pan pan;
|
||||
private Zoom zoom;
|
||||
|
||||
private ZoomLimit zoomLimit;
|
||||
private boolean isEnabled = true;
|
||||
|
||||
private DragState dragState = DragState.NONE;
|
||||
private PointF firstFingerPos;
|
||||
|
||||
// rectangle created by the space between two fingers
|
||||
protected RectF fingersRect;
|
||||
private View.OnTouchListener delegate;
|
||||
private State state = new State();
|
||||
|
||||
// Definition of the touch states
|
||||
protected enum DragState {
|
||||
NONE,
|
||||
ONE_FINGER,
|
||||
TWO_FINGERS
|
||||
}
|
||||
|
||||
public enum Pan {
|
||||
NONE,
|
||||
HORIZONTAL,
|
||||
VERTICAL,
|
||||
BOTH
|
||||
}
|
||||
|
||||
public enum Zoom {
|
||||
|
||||
/**
|
||||
* Comletely disable panning
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* Zoom on the horizontal axis only
|
||||
*/
|
||||
STRETCH_HORIZONTAL,
|
||||
|
||||
/**
|
||||
* Zoom on the vertical axis only
|
||||
*/
|
||||
STRETCH_VERTICAL,
|
||||
|
||||
/**
|
||||
* Zoom on the vertical axis by the vertical distance between each finger, while zooming
|
||||
* on the horizontal axis by the horizantal distance between each finger.
|
||||
*/
|
||||
STRETCH_BOTH,
|
||||
|
||||
/**
|
||||
* Zoom each axis by the same amount, specifically the total distance between each finger.
|
||||
*/
|
||||
SCALE
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits imposed on the zoom.
|
||||
*/
|
||||
public enum ZoomLimit {
|
||||
/**
|
||||
* Do not zoom outside the plots outer bounds, if they are defined.
|
||||
*/
|
||||
OUTER,
|
||||
|
||||
/**
|
||||
* Additionally to the outer bounds if plot.StepModel defines a value based increment
|
||||
* make sure at least one tick is visible by not zooming in further.
|
||||
*/
|
||||
MIN_TICKS
|
||||
}
|
||||
|
||||
// TODO: consider making this immutable / threadsafe
|
||||
public static class State implements Serializable {
|
||||
private Number domainLowerBoundary;
|
||||
private Number domainUpperBoundary;
|
||||
private Number rangeLowerBoundary;
|
||||
private Number rangeUpperBoundary;
|
||||
private BoundaryMode domainBoundaryMode;
|
||||
private BoundaryMode rangeBoundaryMode;
|
||||
|
||||
public void setDomainBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
|
||||
this.domainLowerBoundary = lowerBoundary;
|
||||
this.domainUpperBoundary = upperBoundary;
|
||||
this.domainBoundaryMode = mode;
|
||||
}
|
||||
|
||||
public void setRangeBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
|
||||
this.rangeLowerBoundary = lowerBoundary;
|
||||
this.rangeUpperBoundary = upperBoundary;
|
||||
this.rangeBoundaryMode = mode;
|
||||
}
|
||||
|
||||
public void applyDomainBoundaries(@NonNull XYPlot plot) {
|
||||
plot.setDomainBoundaries(domainLowerBoundary, domainUpperBoundary, domainBoundaryMode);
|
||||
}
|
||||
|
||||
public void applyRangeBoundaries(@NonNull XYPlot plot) {
|
||||
plot.setRangeBoundaries(rangeLowerBoundary, rangeUpperBoundary, rangeBoundaryMode);
|
||||
}
|
||||
|
||||
public void apply(@NonNull XYPlot plot) {
|
||||
applyDomainBoundaries(plot);
|
||||
applyRangeBoundaries(plot);
|
||||
}
|
||||
}
|
||||
|
||||
protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom) {
|
||||
this.plot = plot;
|
||||
this.pan = pan;
|
||||
this.zoom = zoom;
|
||||
this.zoomLimit = ZoomLimit.OUTER;
|
||||
}
|
||||
|
||||
// additional constructor not to break api
|
||||
protected PanZoom(@NonNull XYPlot plot, Pan pan, Zoom zoom, ZoomLimit limit) {
|
||||
this.plot = plot;
|
||||
this.pan = pan;
|
||||
this.zoom = zoom;
|
||||
this.zoomLimit = limit;
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public void setState(@NonNull State state) {
|
||||
this.state = state;
|
||||
state.apply(plot);
|
||||
}
|
||||
|
||||
protected void adjustRangeBoundary(Number lower, Number upper, BoundaryMode mode) {
|
||||
state.setRangeBoundaries(lower, upper, mode);
|
||||
state.applyRangeBoundaries(plot);
|
||||
}
|
||||
|
||||
protected void adjustDomainBoundary(Number lower, Number upper, BoundaryMode mode) {
|
||||
state.setDomainBoundaries(lower, upper, mode);
|
||||
state.applyDomainBoundaries(plot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using
|
||||
* a default behavior of {@link Pan#BOTH} and {@link Zoom#SCALE}.
|
||||
* Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior.
|
||||
* @param plot
|
||||
* @return
|
||||
*/
|
||||
public static PanZoom attach(@NonNull XYPlot plot) {
|
||||
return attach(plot, Pan.BOTH, Zoom.SCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Old method for enabling pan/zoom behavior on an instance of {@link XYPlot}, using
|
||||
* the default behavior of {@link ZoomLimit#OUTER}.
|
||||
* Use {@link PanZoom#attach(XYPlot, Pan, Zoom, ZoomLimit)} for finer grain control of this behavior.
|
||||
* @param plot
|
||||
* @param pan
|
||||
* @param zoom
|
||||
* @return
|
||||
*/
|
||||
public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom) {
|
||||
return attach(plot,pan,zoom, ZoomLimit.OUTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* New method for enabling pan/zoom behavior on an instance of {@link XYPlot}.
|
||||
* @param plot
|
||||
* @param pan
|
||||
* @param zoom
|
||||
* @param limit
|
||||
* @return
|
||||
*/
|
||||
public static PanZoom attach(@NonNull XYPlot plot, @NonNull Pan pan, @NonNull Zoom zoom, @NonNull ZoomLimit limit) {
|
||||
PanZoom pz = new PanZoom(plot, pan, zoom, limit);
|
||||
plot.setOnTouchListener(pz);
|
||||
return pz;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
isEnabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(final View view, final MotionEvent event) {
|
||||
boolean isConsumed = false;
|
||||
if (delegate != null) {
|
||||
isConsumed = delegate.onTouch(view, event);
|
||||
}
|
||||
if (isEnabled() && !isConsumed) {
|
||||
switch (event.getAction() & MotionEvent.ACTION_MASK) {
|
||||
case MotionEvent.ACTION_DOWN: // start gesture
|
||||
firstFingerPos = new PointF(event.getX(), event.getY());
|
||||
dragState = DragState.ONE_FINGER;
|
||||
break;
|
||||
case MotionEvent.ACTION_POINTER_DOWN: // second finger
|
||||
{
|
||||
setFingersRect(fingerDistance(event));
|
||||
// the distance run is done to avoid false alarms
|
||||
if (getFingersRect().width() > MIN_DIST_2_FING || getFingersRect().width() < -MIN_DIST_2_FING) {
|
||||
dragState = DragState.TWO_FINGERS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MotionEvent.ACTION_POINTER_UP: // end zoom
|
||||
dragState = DragState.NONE;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (dragState == DragState.ONE_FINGER) {
|
||||
pan(event);
|
||||
} else if (dragState == DragState.TWO_FINGERS) {
|
||||
zoom(event);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// we're forced to consume the event here as not consuming it will prevent future calls:
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance between two finger motion events.
|
||||
* @param firstFingerX
|
||||
* @param firstFingerY
|
||||
* @param secondFingerX
|
||||
* @param secondFingerY
|
||||
* @return
|
||||
*/
|
||||
protected RectF fingerDistance(float firstFingerX, float firstFingerY, float secondFingerX, float secondFingerY) {
|
||||
final float left = firstFingerX > secondFingerX ? secondFingerX : firstFingerX;
|
||||
final float right = firstFingerX > secondFingerX ? firstFingerX : secondFingerX;
|
||||
final float top = firstFingerY > secondFingerY ? secondFingerY : firstFingerY;
|
||||
final float bottom = firstFingerY > secondFingerY ? firstFingerY : secondFingerY;
|
||||
return new RectF(left, top, right, bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance between two finger motion events.
|
||||
* @param evt
|
||||
* @return
|
||||
*/
|
||||
protected RectF fingerDistance(final MotionEvent evt) {
|
||||
return fingerDistance(
|
||||
evt.getX(FIRST_FINGER),
|
||||
evt.getY(FIRST_FINGER),
|
||||
evt.getX(SECOND_FINGER),
|
||||
evt.getY(SECOND_FINGER));
|
||||
}
|
||||
|
||||
protected void pan(final MotionEvent motionEvent) {
|
||||
if (pan == Pan.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PointF oldFirstFinger = firstFingerPos; //save old position of finger
|
||||
firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position
|
||||
if (EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) {
|
||||
Region newBounds = new Region();
|
||||
calculatePan(oldFirstFinger, newBounds, true);
|
||||
adjustDomainBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
|
||||
}
|
||||
if (EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) {
|
||||
Region newBounds = new Region();
|
||||
calculatePan(oldFirstFinger, newBounds, false);
|
||||
adjustRangeBoundary(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
|
||||
}
|
||||
|
||||
plot.redraw();
|
||||
}
|
||||
|
||||
protected void calculatePan(final PointF oldFirstFinger, Region bounds, final boolean horizontal) {
|
||||
final float offset;
|
||||
// multiply the absolute finger movement for a factor.
|
||||
// the factor is dependent on the calculated min and max
|
||||
if (horizontal) {
|
||||
bounds.setMinMax(plot.getBounds().getxRegion());
|
||||
offset = (oldFirstFinger.x - firstFingerPos.x) *
|
||||
((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getWidth());
|
||||
} else {
|
||||
bounds.setMinMax(plot.getBounds().getyRegion());
|
||||
offset = -(oldFirstFinger.y - firstFingerPos.y) *
|
||||
((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getHeight());
|
||||
}
|
||||
// move the calculated offset
|
||||
bounds.setMin(bounds.getMin().floatValue() + offset);
|
||||
bounds.setMax(bounds.getMax().floatValue() + offset);
|
||||
|
||||
//get the distance between max and min
|
||||
final float diff = bounds.length().floatValue();
|
||||
|
||||
//run if we reached the limit of panning
|
||||
if (horizontal && plot.getOuterLimits().getxRegion().isDefined()) {
|
||||
if (bounds.getMin().floatValue() < plot.getOuterLimits().getMinX().floatValue()) {
|
||||
bounds.setMin(plot.getOuterLimits().getMinX());
|
||||
bounds.setMax(bounds.getMin().floatValue() + diff);
|
||||
}
|
||||
if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxX().floatValue()) {
|
||||
bounds.setMax(plot.getOuterLimits().getMaxX());
|
||||
bounds.setMin(bounds.getMax().floatValue() - diff);
|
||||
}
|
||||
} else if(plot.getOuterLimits().getyRegion().isDefined()) {
|
||||
if (bounds.getMin().floatValue() < plot.getOuterLimits().getMinY().floatValue()) {
|
||||
bounds.setMin(plot.getOuterLimits().getMinY());
|
||||
bounds.setMax(bounds.getMin().floatValue() + diff);
|
||||
}
|
||||
if (bounds.getMax().floatValue() > plot.getOuterLimits().getMaxY().floatValue()) {
|
||||
bounds.setMax(plot.getOuterLimits().getMaxY());
|
||||
bounds.setMin(bounds.getMax().floatValue() - diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isValidScale(float scale) {
|
||||
return !Float.isInfinite(scale)
|
||||
&& !Float.isNaN(scale)
|
||||
&& (!(scale > -0.001) || !(scale < 0.001));
|
||||
}
|
||||
|
||||
protected void zoom(final MotionEvent motionEvent) {
|
||||
if (zoom == Zoom.NONE) {
|
||||
return;
|
||||
}
|
||||
final RectF oldFingersRect = getFingersRect();
|
||||
final RectF newFingersRect = fingerDistance(motionEvent);
|
||||
setFingersRect(newFingersRect);
|
||||
if(oldFingersRect == null || RectFUtils.areIdentical(oldFingersRect, newFingersRect)) {
|
||||
// zooming gesture has not happened yet so skip:
|
||||
return;
|
||||
}
|
||||
RectF newRect = new RectF();
|
||||
|
||||
float scaleX = 1;
|
||||
float scaleY = 1;
|
||||
switch (zoom) {
|
||||
case STRETCH_HORIZONTAL:
|
||||
scaleX = oldFingersRect.width() / getFingersRect().width();
|
||||
if (!isValidScale(scaleX)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case STRETCH_VERTICAL:
|
||||
scaleY = oldFingersRect.height() / getFingersRect().height();
|
||||
if (!isValidScale(scaleY)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case STRETCH_BOTH:
|
||||
scaleX = oldFingersRect.width() / getFingersRect().width();
|
||||
scaleY = oldFingersRect.height() / getFingersRect().height();
|
||||
if (!isValidScale(scaleX) || !isValidScale(scaleY)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SCALE:
|
||||
float sc1 = (float) Math.hypot(oldFingersRect.height(), oldFingersRect.width());
|
||||
float sc2 = (float) Math.hypot(getFingersRect().height(), getFingersRect().width());
|
||||
float sc = sc1 / sc2;
|
||||
scaleX = sc;
|
||||
scaleY = sc;
|
||||
if (!isValidScale(scaleX) || !isValidScale(scaleY)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (EnumSet.of(
|
||||
Zoom.STRETCH_HORIZONTAL,
|
||||
Zoom.STRETCH_BOTH,
|
||||
Zoom.SCALE).contains(zoom)) {
|
||||
calculateZoom(newRect, scaleX, true);
|
||||
adjustDomainBoundary(newRect.left, newRect.right, BoundaryMode.FIXED);
|
||||
}
|
||||
if (EnumSet.of(
|
||||
Zoom.STRETCH_VERTICAL,
|
||||
Zoom.STRETCH_BOTH,
|
||||
Zoom.SCALE).contains(zoom)) {
|
||||
calculateZoom(newRect, scaleY, false);
|
||||
adjustRangeBoundary(newRect.top, newRect.bottom, BoundaryMode.FIXED);
|
||||
}
|
||||
plot.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param newRect RectF into which zoom calculation results should be placed.
|
||||
* @param scale
|
||||
* @param isHorizontal
|
||||
*/
|
||||
protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) {
|
||||
final float calcMax;
|
||||
final float span;
|
||||
final RectRegion bounds = plot.getBounds();
|
||||
if (isHorizontal) {
|
||||
calcMax = bounds.getMaxX().floatValue();
|
||||
span = calcMax - bounds.getMinX().floatValue();
|
||||
} else {
|
||||
calcMax = bounds.getMaxY().floatValue();
|
||||
span = calcMax - bounds.getMinY().floatValue();
|
||||
}
|
||||
|
||||
final float midPoint = calcMax - (span / 2.0f);
|
||||
float offset = span * scale / 2.0f;
|
||||
final RectRegion limits = plot.getOuterLimits();
|
||||
|
||||
if (isHorizontal ) {
|
||||
// zoom limited and increment by value StepMode?
|
||||
if (zoomLimit == ZoomLimit.MIN_TICKS) {
|
||||
// make sure we do not zoom in too far (there should be at least one grid line visible)
|
||||
if (plot.getDomainStepValue() > (scale*span)) {
|
||||
offset = (float)(plot.getDomainStepValue() / 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
newRect.left = midPoint - offset;
|
||||
newRect.right = midPoint + offset;
|
||||
if(limits.isFullyDefined()) {
|
||||
if (newRect.left < limits.getMinX().floatValue()) {
|
||||
newRect.left = limits.getMinX().floatValue();
|
||||
}
|
||||
if (newRect.right > limits.getMaxX().floatValue()) {
|
||||
newRect.right = limits.getMaxX().floatValue();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// zoom limited and increment by value StepMode?
|
||||
if (zoomLimit == ZoomLimit.MIN_TICKS) {
|
||||
// make sure we do not zoom in too far (there should be at least one grid line visible)
|
||||
if (plot.getRangeStepValue() > (scale*span)) {
|
||||
offset = (float)(plot.getRangeStepValue() / 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
newRect.top = midPoint - offset;
|
||||
newRect.bottom = midPoint + offset;
|
||||
if(limits.isFullyDefined()) {
|
||||
if (newRect.top < limits.getMinY().floatValue()) {
|
||||
newRect.top = limits.getMinY().floatValue();
|
||||
}
|
||||
if (newRect.bottom > limits.getMaxY().floatValue()) {
|
||||
newRect.bottom = limits.getMaxY().floatValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Pan getPan() {
|
||||
return pan;
|
||||
}
|
||||
|
||||
public void setPan(Pan pan) {
|
||||
this.pan = pan;
|
||||
}
|
||||
|
||||
public Zoom getZoom() {
|
||||
return zoom;
|
||||
}
|
||||
|
||||
public void setZoom(Zoom zoom) {
|
||||
this.zoom = zoom;
|
||||
}
|
||||
|
||||
public ZoomLimit getZoomLimit() {
|
||||
return zoomLimit;
|
||||
}
|
||||
|
||||
public void setZoomLimit(ZoomLimit zoomLimit) {
|
||||
this.zoomLimit = zoomLimit;
|
||||
}
|
||||
|
||||
public View.OnTouchListener getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a delegate to receive onTouch calls before this class does. If the delegate wishes
|
||||
* to consume the event, it should return true, otherwise it should return false. Returning
|
||||
* false will not prevent future onTouch events from filtering through the delegate as it normally
|
||||
* would when attaching directly to an instance of {@link View}.
|
||||
* @param delegate
|
||||
*/
|
||||
public void setDelegate(View.OnTouchListener delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.firstFingerPos = null;
|
||||
setFingersRect(null);
|
||||
this.setFingersRect(null);
|
||||
}
|
||||
|
||||
protected RectF getFingersRect() {
|
||||
return fingersRect;
|
||||
}
|
||||
|
||||
protected void setFingersRect(RectF fingersRect) {
|
||||
this.fingersRect = fingersRect;
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import com.androidplot.util.PixelUtils;
|
||||
|
||||
public class PointLabelFormatter {
|
||||
private static final float DEFAULT_H_OFFSET_DP = 0;
|
||||
private static final float DEFAULT_V_OFFSET_DP = -4;
|
||||
private static final float DEFAULT_TEXT_SIZE_SP = 12;
|
||||
private Paint textPaint;
|
||||
public float hOffset;
|
||||
public float vOffset;
|
||||
|
||||
public PointLabelFormatter() {
|
||||
this(Color.WHITE);
|
||||
}
|
||||
|
||||
public PointLabelFormatter(int textColor) {
|
||||
this(textColor, PixelUtils.dpToPix(DEFAULT_H_OFFSET_DP),
|
||||
PixelUtils.dpToPix(DEFAULT_V_OFFSET_DP));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param textColor
|
||||
* @param hOffset Horizontal offset of text in pixels.
|
||||
* @param vOffset Vertical offset of text in pixels. Offset is in screen coordinates;
|
||||
* positive values shift the text further down the screen.
|
||||
*/
|
||||
public PointLabelFormatter(int textColor, float hOffset, float vOffset) {
|
||||
initTextPaint(textColor);
|
||||
this.hOffset = hOffset;
|
||||
this.vOffset = vOffset;
|
||||
}
|
||||
|
||||
public boolean hasTextPaint() {
|
||||
return textPaint != null;
|
||||
}
|
||||
|
||||
public Paint getTextPaint() {
|
||||
if(textPaint == null) {
|
||||
initTextPaint(Color.TRANSPARENT);
|
||||
}
|
||||
return textPaint;
|
||||
}
|
||||
|
||||
public void setTextPaint(Paint textPaint) {
|
||||
this.textPaint = textPaint;
|
||||
}
|
||||
|
||||
protected void initTextPaint(Integer textColor) {
|
||||
if (textColor == null) {
|
||||
setTextPaint(null);
|
||||
} else {
|
||||
setTextPaint(new Paint());
|
||||
getTextPaint().setAntiAlias(true);
|
||||
getTextPaint().setColor(textColor);
|
||||
getTextPaint().setTextAlign(Paint.Align.CENTER);
|
||||
getTextPaint().setTextSize(PixelUtils.spToPix(DEFAULT_TEXT_SIZE_SP));
|
||||
//textPaint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
public interface PointLabeler<SeriesType extends XYSeries> {
|
||||
|
||||
String getLabel(SeriesType series, int index);
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import com.androidplot.Region;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RectRegion is just a rectangle with additional methods for determining
|
||||
* intersections with other RectRegion instances. RectRegion operates on
|
||||
* "native units" which are simply whatever unit type is passed into it by the user.
|
||||
*/
|
||||
public class RectRegion {
|
||||
|
||||
Region xRegion;
|
||||
Region yRegion;
|
||||
private String label;
|
||||
|
||||
public RectRegion() {
|
||||
xRegion = new Region();
|
||||
yRegion = new Region();
|
||||
}
|
||||
|
||||
public static RectRegion withDefaults(RectRegion defaults) {
|
||||
if(defaults == null || !defaults.isFullyDefined()) {
|
||||
throw new IllegalArgumentException("When specifying defaults, RectRegion param must contain no null values.");
|
||||
}
|
||||
|
||||
RectRegion r = new RectRegion();
|
||||
r.xRegion = Region.withDefaults(defaults.getxRegion());
|
||||
r.yRegion = Region.withDefaults(defaults.getyRegion());
|
||||
return r;
|
||||
}
|
||||
|
||||
public RectRegion(XYCoords min, XYCoords max) {
|
||||
this(min.x, max.x, min.y, max.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param minX
|
||||
* @param maxX
|
||||
* @param minY
|
||||
* @param maxY
|
||||
*/
|
||||
public RectRegion(Number minX, Number maxX, Number minY, Number maxY, String label) {
|
||||
xRegion = new Region(minX, maxX);
|
||||
yRegion = new Region(minY, maxY);
|
||||
this.setLabel(label);
|
||||
}
|
||||
|
||||
public RectRegion(RectF rect) {
|
||||
this(rect.left < rect.right ? rect.left : rect.right,
|
||||
rect.right > rect.left ? rect.right : rect.left,
|
||||
rect.bottom < rect.top ? rect.bottom : rect.top,
|
||||
rect.top > rect.bottom ? rect.top : rect.bottom);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
public RectRegion(Number minX, Number maxX, Number minY, Number maxY) {
|
||||
this(minX, maxX, minY, maxY, null);
|
||||
}
|
||||
|
||||
public XYCoords transform(Number x, Number y, RectRegion region2, boolean flipX, boolean flipY) {
|
||||
Number xx = xRegion.transform(x.doubleValue(), region2.xRegion, flipX);
|
||||
Number yy = yRegion.transform(y.doubleValue(), region2.yRegion, flipY);
|
||||
return new XYCoords(xx, yy);
|
||||
}
|
||||
|
||||
public XYCoords transform(Number x, Number y, RectRegion region2) {
|
||||
return transform(x, y, region2, false, false);
|
||||
}
|
||||
|
||||
public XYCoords transform(XYCoords value, RectRegion region2) {
|
||||
return transform(value.x, value.y, region2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a region (r) from the current region space (this) into the specified one (r2)
|
||||
* @param r The region to which the transformation applies
|
||||
* @param r2 The region into which r is being transformed
|
||||
* @return
|
||||
*/
|
||||
public RectRegion transform(RectRegion r, RectRegion r2, boolean flipX, boolean flipY) {
|
||||
return new RectRegion(
|
||||
transform(r.getMinX(), r.getMinY(), r2, flipX, flipY),
|
||||
transform(r.getMaxX(), r.getMaxY(), r2, flipX, flipY)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to transform into screen coordinate space. Equivalent to invoking
|
||||
* {@link #transform(Number, Number, RectF, boolean, boolean)} with
|
||||
* flipX = false and flipY = true.
|
||||
* @param x
|
||||
* @param y
|
||||
* @param region2
|
||||
* @return
|
||||
*/
|
||||
public PointF transformScreen(Number x, Number y, RectF region2) {
|
||||
return transform(x, y, region2, false, true);
|
||||
}
|
||||
|
||||
public void transformScreen(PointF result, Number x, Number y, RectF region2) {
|
||||
transform(result, x, y, region2, false, true);
|
||||
}
|
||||
|
||||
public void transform(PointF result, Number x, Number y, RectF region2, boolean flipX, boolean flipY) {
|
||||
result.x = (float) xRegion.transform(x.doubleValue(), region2.left, region2.right, flipX);
|
||||
result.y = (float) yRegion.transform(y.doubleValue(), region2.top, region2.bottom, flipY);
|
||||
}
|
||||
|
||||
public PointF transform(Number x, Number y, RectF region2, boolean flipX, boolean flipY) {
|
||||
PointF result = new PointF();
|
||||
transform(result, x, y, region2, flipX, flipY);
|
||||
return result;
|
||||
}
|
||||
|
||||
public PointF transformScreen(XYCoords value, RectF region2) {
|
||||
return transform(value, region2, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to transform into screen coordinate space. Equivalent to invoking
|
||||
* {@link #transform(XYCoords, RectF, boolean, boolean)} with flipX = false and flipY = true.
|
||||
* @param value
|
||||
* @param region2
|
||||
* @param flipX
|
||||
* @param flipY
|
||||
* @return
|
||||
*/
|
||||
public PointF transform(XYCoords value, RectF region2, boolean flipX, boolean flipY) {
|
||||
return transform(value.x, value.y, region2, flipX, flipY);
|
||||
}
|
||||
|
||||
public void union(Number x, Number y) {
|
||||
xRegion.union(x);
|
||||
yRegion.union(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the input bounds xy min/max vals against this instance's current xy min/max vals.
|
||||
* If the input.min is less than this.min then this.min will be set to input.min.
|
||||
* If the input.max is greater than this.max then this.max will be set to input.max
|
||||
*
|
||||
* The result will always have equal or greater area than the inputs.
|
||||
* @param input
|
||||
*/
|
||||
public void union(RectRegion input) {
|
||||
xRegion.union(input.xRegion);
|
||||
yRegion.union(input.yRegion);
|
||||
}
|
||||
|
||||
public boolean intersects(RectRegion region) {
|
||||
return intersects(region.getMinX(), region.getMaxX(), region.getMinY(), region.getMaxY());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether this region intersects the region defined by params. Use
|
||||
* null to represent infinity. Negative and positive infinity is implied by
|
||||
* the boundary edge, ie. a maxX of null equals positive infinity while a
|
||||
* minX of null equals negative infinity.
|
||||
* @param minX
|
||||
* @param maxX
|
||||
* @param minY
|
||||
* @param maxY
|
||||
* @return
|
||||
*/
|
||||
public boolean intersects(Number minX, Number maxX, Number minY, Number maxY) {
|
||||
return xRegion.intersects(minX, maxX) && yRegion.intersects(minY, maxY);
|
||||
}
|
||||
|
||||
public RectF asRectF() {
|
||||
return new RectF(getMinX().floatValue(), getMinY().floatValue(),
|
||||
getMaxX().floatValue(), getMaxY().floatValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of an intersect is always a RectRegion with an equal or smaller area.
|
||||
* @param clippingBounds
|
||||
*/
|
||||
public void intersect(RectRegion clippingBounds) {
|
||||
if(intersects(clippingBounds)) {
|
||||
xRegion.intersect(clippingBounds.xRegion);
|
||||
yRegion.intersect(clippingBounds.yRegion);
|
||||
} else {
|
||||
setMinY(null);
|
||||
setMaxY(null);
|
||||
setMinX(null);
|
||||
setMaxX(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of XYRegions that either completely or partially intersect the area
|
||||
* defined by params. A null value for any parameter represents infinity / no boundary.
|
||||
* @param regions The list of regions to search through
|
||||
* @return
|
||||
*/
|
||||
public List<RectRegion> intersects(List<RectRegion> regions) {
|
||||
ArrayList<RectRegion> intersectingRegions = new ArrayList<>();
|
||||
for (RectRegion r : regions) {
|
||||
if (r.intersects(getMinX(), getMaxX(), getMinY(), getMaxY())) {
|
||||
intersectingRegions.add(r);
|
||||
}
|
||||
}
|
||||
return intersectingRegions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Width of this region, in native units
|
||||
*/
|
||||
public Number getWidth() {
|
||||
return distanceBetween(getMinX(), getMaxX());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Height of this region, in native units
|
||||
*/
|
||||
public Number getHeight() {
|
||||
return distanceBetween(getMinY(), getMaxY());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the distance between two points in a single dimension.
|
||||
* @param x
|
||||
* @param y
|
||||
* @return
|
||||
*/
|
||||
private static Number distanceBetween(Number x, Number y) {
|
||||
return Math.abs(x.doubleValue() - y.doubleValue());
|
||||
}
|
||||
|
||||
public void set(Number minX, Number maxX, Number minY, Number maxY) {
|
||||
setMinX(minX);
|
||||
setMaxX(maxX);
|
||||
setMinY(minY);
|
||||
setMaxY(maxY);
|
||||
}
|
||||
|
||||
public boolean isMinXSet() {
|
||||
return xRegion.isMinSet();
|
||||
}
|
||||
|
||||
public Number getMinX() {
|
||||
return xRegion.getMin();
|
||||
}
|
||||
|
||||
public void setMinX(Number minX) {
|
||||
xRegion.setMin(minX);
|
||||
}
|
||||
|
||||
public boolean isMaxXSet() {
|
||||
return xRegion.isMaxSet();
|
||||
}
|
||||
|
||||
public Number getMaxX() {
|
||||
return xRegion.getMax();
|
||||
}
|
||||
|
||||
public void setMaxX(Number maxX) {
|
||||
xRegion.setMax(maxX);
|
||||
}
|
||||
|
||||
public boolean isMinYSet() {
|
||||
return yRegion.isMinSet();
|
||||
}
|
||||
|
||||
public Number getMinY() {
|
||||
return yRegion.getMin();
|
||||
}
|
||||
|
||||
public void setMinY(Number minY) {
|
||||
yRegion.setMin(minY);
|
||||
}
|
||||
|
||||
public boolean isMaxYSet() {
|
||||
return yRegion.isMaxSet();
|
||||
}
|
||||
|
||||
public Number getMaxY() {
|
||||
return yRegion.getMax();
|
||||
}
|
||||
|
||||
public void setMaxY(Number maxY) {
|
||||
yRegion.setMax(maxY);
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Region getxRegion() {
|
||||
return xRegion;
|
||||
}
|
||||
|
||||
public void setxRegion(Region xRegion) {
|
||||
this.xRegion = xRegion;
|
||||
}
|
||||
|
||||
public Region getyRegion() {
|
||||
return yRegion;
|
||||
}
|
||||
|
||||
public void setyRegion(Region yRegion) {
|
||||
this.yRegion = yRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return True if both xRegion and yRegion are defined, false otherwise
|
||||
*/
|
||||
public boolean isFullyDefined() {
|
||||
return xRegion.isDefined() && yRegion.isDefined();
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this region contains the specified coordinates.
|
||||
* @param x
|
||||
* @param y
|
||||
* @return
|
||||
*/
|
||||
public boolean contains(Number x, Number y) {
|
||||
return getxRegion().contains(x) && getyRegion().contains(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RectRegion{" +
|
||||
"xRegion=" + xRegion +
|
||||
", yRegion=" + yRegion +
|
||||
", label='" + label + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
import com.androidplot.*;
|
||||
import com.androidplot.util.SeriesUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* An implementation of {@link FastXYSeries} that samples its self into multiple levels to
|
||||
* achieve faster rendering / zoom behavior. By default, uses {@link LTTBSampler} as
|
||||
* it's sampling algorithm. Note that this algorithm does not yet support null values.
|
||||
*
|
||||
* Sampling behavior is controlled by two values:
|
||||
* Ratio: A value greater than 1; controls the sampling ratio of each successive series. For example,
|
||||
* a step of 2 would mean that each successive series contains 2x fewer points than the previous.
|
||||
*
|
||||
* Threshold: A value < the original series size; controls the lower limit at which point sampling should stop.
|
||||
* For example, sampling a series with size 1000 given a ratio of 2 and a threshold of 100,
|
||||
* three sampled resolutions will be generated:
|
||||
*
|
||||
* 500 - 2x sampling
|
||||
* 250 - 4x sampling
|
||||
* 125 - 8x sampling
|
||||
*
|
||||
*/
|
||||
public class SampledXYSeries implements FastXYSeries, OrderedXYSeries {
|
||||
private int threshold;
|
||||
private Sampler algorithm = new LTTBSampler();
|
||||
|
||||
private XYSeries rawData;
|
||||
|
||||
private List<EditableXYSeries> zoomLevels;
|
||||
|
||||
private XYSeries activeSeries;
|
||||
|
||||
private RectRegion bounds;
|
||||
private Exception lastResamplingException;
|
||||
|
||||
private final XOrder xOrder;
|
||||
private float ratio;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rawData
|
||||
* @param xOrder If your data is in ascending or descending order, specifying it here speed up
|
||||
* optimize render times.
|
||||
* @param ratio The ratio used to determine the size of each new sampled series. Must be > 1.
|
||||
* downsampled series until threshold is reached.
|
||||
* @param threshold The desired size of the smallest sample series. Must be < rawData.size.
|
||||
*/
|
||||
public SampledXYSeries(XYSeries rawData, XOrder xOrder, float ratio, int threshold) {
|
||||
this.rawData = rawData;
|
||||
this.xOrder = xOrder;
|
||||
this.setRatio(ratio);
|
||||
this.setThreshold(threshold);
|
||||
resample();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a SampledXYSeries from the input series.
|
||||
* @param rawData The original series to be downsampled
|
||||
* @param ratio The ratio used to determine the size of each new sampled series. Must be > 1.
|
||||
* downsampled series until threshold is reached.
|
||||
* @param threshold The desired size of the smallest sample series. Must be < rawData.size.
|
||||
*/
|
||||
public SampledXYSeries(XYSeries rawData, float ratio, int threshold) {
|
||||
this(rawData, SeriesUtils.getXYOrder(rawData), ratio, threshold);
|
||||
}
|
||||
|
||||
public void resample() {
|
||||
bounds = null;
|
||||
zoomLevels = new ArrayList<>();
|
||||
int t = (int) Math.ceil(rawData.size() / getRatio());
|
||||
List<Thread> threads = new ArrayList<>(zoomLevels.size());
|
||||
while (t > threshold) {
|
||||
final int thisThreshold = t;
|
||||
final EditableXYSeries thisSeries = new FixedSizeEditableXYSeries(getTitle(), thisThreshold);
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// TODO: make bounds a param to prevent calculating on each setZoomFactor level:
|
||||
RectRegion b = getAlgorithm()
|
||||
.run(rawData, thisSeries);
|
||||
if (bounds == null) {
|
||||
bounds = b;
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
lastResamplingException = ex;
|
||||
}
|
||||
}
|
||||
}, "Androidplot XY Series Sampler");
|
||||
getZoomLevels().add(thisSeries);
|
||||
threads.add(thread);
|
||||
thread.start();
|
||||
|
||||
t = (int) Math.ceil(t / getRatio());
|
||||
}
|
||||
for(Thread thread : threads) {
|
||||
try {
|
||||
thread.join();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if(lastResamplingException != null) {
|
||||
throw new RuntimeException("Exception encountered during resampling", lastResamplingException);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected List<EditableXYSeries> getZoomLevels() {
|
||||
return this.zoomLevels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set zoom factor; 2.5 = 2.5x zoom, 10.0 = 10x zoom etc. This method will set the zoom level
|
||||
* to the closest available factor to the specified factor; a specified factor of 4.5x may result
|
||||
* in an actual factor of 4x.
|
||||
* @param factor
|
||||
*/
|
||||
public void setZoomFactor(double factor) {
|
||||
if(factor <= 1) {
|
||||
activeSeries = rawData;
|
||||
} else {
|
||||
//int i = (int) Math.round(Math.sqrt(factor) - 1);
|
||||
int i = getZoomIndex(factor, getRatio());
|
||||
if (i < zoomLevels.size()) {
|
||||
activeSeries = zoomLevels.get(i);
|
||||
} else {
|
||||
activeSeries = zoomLevels.get(zoomLevels.size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static int getZoomIndex(double zoomFactor, double ratio) {
|
||||
final double lhs = Math.log(zoomFactor);
|
||||
final double rhs = Math.log(ratio);
|
||||
final double log = lhs / rhs;
|
||||
final int index = (int) Math.round(log);
|
||||
return index > 0 ? index - 1 : 0;
|
||||
}
|
||||
|
||||
public double getMaxZoomFactor() {
|
||||
return Math.pow(getRatio(), zoomLevels.size());
|
||||
}
|
||||
|
||||
public Sampler getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public void setAlgorithm(Sampler algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
resample();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return rawData.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return activeSeries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getX(int index) {
|
||||
return activeSeries.getX(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getY(int index) {
|
||||
return activeSeries.getY(index);
|
||||
}
|
||||
|
||||
public int getThreshold() {
|
||||
return threshold;
|
||||
}
|
||||
|
||||
public void setThreshold(int threshold) {
|
||||
if(threshold >= rawData.size()) {
|
||||
throw new IllegalArgumentException("Threshold must be < original series size.");
|
||||
}
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
public RectRegion getBounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public void setBounds(RectRegion bounds) {
|
||||
this.bounds = bounds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RectRegion minMax() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XOrder getXOrder() {
|
||||
return xOrder;
|
||||
}
|
||||
|
||||
public float getRatio() {
|
||||
return ratio;
|
||||
}
|
||||
|
||||
public void setRatio(float ratio) {
|
||||
if(ratio <= 1) {
|
||||
throw new IllegalArgumentException("Ratio must be greater than 1");
|
||||
}
|
||||
this.ratio = ratio;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* An algorithm used to to resample a larger set of data into a smaller set.
|
||||
*/
|
||||
public interface Sampler {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param input The original unsampled series
|
||||
* @param output The destination series to contain sampled result.
|
||||
* This series size should be set to the desired sampled size.
|
||||
* @return min/max values encountered while processing input.
|
||||
*/
|
||||
RectRegion run(XYSeries input, EditableXYSeries output);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* Wraps an existing {@link XYSeries} allowing easy scaling of that series' xy values.
|
||||
*/
|
||||
public class ScalingXYSeries implements XYSeries {
|
||||
|
||||
private double scale;
|
||||
private XYSeries series;
|
||||
private Mode mode;
|
||||
|
||||
public enum Mode {
|
||||
X_ONLY,
|
||||
Y_ONLY,
|
||||
X_AND_Y
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param series The {@link XYSeries} to be scaled
|
||||
* @param scale The initial scale to be applied
|
||||
* @param mode Determines which axis (or both) to which scaling will be applied.
|
||||
*/
|
||||
public ScalingXYSeries(XYSeries series, double scale, Mode mode) {
|
||||
this.series = series;
|
||||
this.scale = scale;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return series.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return series.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getX(int index) {
|
||||
Number x = series.getX(index);
|
||||
if(mode == Mode.X_ONLY || mode == Mode.X_AND_Y) {
|
||||
return x == null ? null : x.doubleValue() * scale;
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getY(int index) {
|
||||
Number y = series.getY(index);
|
||||
if(mode == Mode.Y_ONLY || mode == Mode.X_AND_Y) {
|
||||
return y == null ? null : y.doubleValue() * scale;
|
||||
} else {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
public double getScale() {
|
||||
return scale;
|
||||
}
|
||||
|
||||
public void setScale(double scale) {
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import com.androidplot.Plot;
|
||||
import com.androidplot.PlotListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
|
||||
/**
|
||||
* A convenience class used to create instances of XYPlot generated from Lists of Numbers.
|
||||
*/
|
||||
public class SimpleXYSeries implements EditableXYSeries, OrderedXYSeries, PlotListener {
|
||||
private volatile LinkedList<Number> xVals = new LinkedList<>();
|
||||
private volatile LinkedList<Number> yVals = new LinkedList<>();
|
||||
private volatile String title = null;
|
||||
|
||||
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
|
||||
private XOrder xOrder = XOrder.NONE;
|
||||
|
||||
public enum ArrayFormat {
|
||||
Y_VALS_ONLY,
|
||||
XY_VALS_INTERLEAVED
|
||||
}
|
||||
|
||||
public SimpleXYSeries(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public SimpleXYSeries(ArrayFormat format, String title, Number... model) {
|
||||
this(asNumberList(model), format, title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current x-ordering specified for this series. Default is
|
||||
* {@link com.androidplot.xy.OrderedXYSeries.XOrder#NONE}.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public XOrder getXOrder() {
|
||||
return xOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* If XVals are in strict ascending order, use this method to set
|
||||
* {@link com.androidplot.xy.OrderedXYSeries.XOrder#ASCENDING} to provide an optimization
|
||||
* hint to the renderer.
|
||||
* @param xOrder
|
||||
*/
|
||||
public void setXOrder(XOrder xOrder) {
|
||||
this.xOrder = xOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeDraw(Plot source, Canvas canvas) {
|
||||
lock.readLock().lock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAfterDraw(Plot source, Canvas canvas) {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
|
||||
protected static List<Number> asNumberList(Number... model) {
|
||||
List<Number> numbers = new ArrayList<>(model.length);
|
||||
Collections.addAll(numbers, model);
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an XYSeries instance from the List of numbers passed in. This is a convenience class
|
||||
* and should only be used for static data models; it is not suitable for representing dynamically
|
||||
* changing data.
|
||||
*
|
||||
* @param model A List of Number elements comprising the data model.
|
||||
* @param format Format of the model. A format of Y_VALS_ONLY means that the array only contains y-values.
|
||||
* For this format x values are autogenerated using values of 0 through n-1 where n is the size of the model.
|
||||
* @param title Title of the series
|
||||
*/
|
||||
public SimpleXYSeries(List<? extends Number> model, ArrayFormat format, String title) {
|
||||
this(title);
|
||||
setModel(model, format);
|
||||
}
|
||||
|
||||
public SimpleXYSeries(List<? extends Number> xVals, List<? extends Number> yVals, String title) {
|
||||
this(title);
|
||||
if(xVals == null || yVals == null) {
|
||||
throw new IllegalArgumentException("Neither the xVals nor the yVals parameters may be null.");
|
||||
}
|
||||
|
||||
if(xVals.size() != yVals.size()) {
|
||||
throw new IllegalArgumentException("xVals and yVals List parameters must be of the same size.");
|
||||
}
|
||||
|
||||
this.xVals.addAll(xVals);
|
||||
this.yVals.addAll(yVals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use index value as xVal, instead of explicit, user provided xVals.
|
||||
*/
|
||||
public void useImplicitXVals() {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
xVals = null;
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the provided list of Numbers as yVals and their corresponding indexes as xVals.
|
||||
* @param model A List of Number elements comprising the data model.
|
||||
* @param format Format of the model. A format of Y_VALS_ONLY means that the array only contains y-values.
|
||||
* For this format x values are autogenerated using values of 0 through n-1 where n is the size of the model.
|
||||
*/
|
||||
public void setModel(List<? extends Number> model, ArrayFormat format) {
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
// empty the current values:
|
||||
xVals.clear();
|
||||
yVals.clear();
|
||||
|
||||
// make sure the new model has data:
|
||||
if (model == null || model.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
|
||||
// array containing only y-vals. assume x = index:
|
||||
case Y_VALS_ONLY:
|
||||
yVals.addAll(model);
|
||||
for(int i = 0; i < yVals.size(); i++) {
|
||||
xVals.add(i);
|
||||
}
|
||||
break;
|
||||
|
||||
// xy interleaved array:
|
||||
case XY_VALS_INTERLEAVED:
|
||||
if (xVals == null) {
|
||||
xVals = new LinkedList<>();
|
||||
}
|
||||
if (model.size() % 2 != 0) {
|
||||
throw new IndexOutOfBoundsException("Cannot auto-generate series from odd-sized xy List.");
|
||||
}
|
||||
// always need an x and y array so init them now:
|
||||
int sz = model.size() / 2;
|
||||
for (int i = 0, j = 0; i < sz; i++, j += 2) {
|
||||
xVals.add(model.get(j));
|
||||
yVals.add(model.get(j + 1));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected enum value: " + format);
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets individual x value based on index
|
||||
* @param value
|
||||
* @param index
|
||||
*/
|
||||
public void setX(Number value, int index) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
xVals.set(index, value);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets individual y value based on index
|
||||
* @param value
|
||||
* @param index
|
||||
*/
|
||||
public void setY(Number value, int index) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
yVals.set(index, value);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int size) {
|
||||
try {
|
||||
lock.writeLock().lock();
|
||||
if (xVals.size() < size) {
|
||||
for (int i = xVals.size(); i < size; i++) {
|
||||
xVals.add(null);
|
||||
yVals.add(null);
|
||||
}
|
||||
} else if(xVals.size() > size) {
|
||||
for(int i = xVals.size(); i > size; i--) {
|
||||
xVals.removeLast();
|
||||
yVals.removeLast();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets xy values based on index
|
||||
* @param xVal
|
||||
* @param yVal
|
||||
* @param index
|
||||
*/
|
||||
public void setXY(Number xVal, Number yVal, int index) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
yVals.set(index, yVal);
|
||||
xVals.set(index, xVal);
|
||||
} finally {lock.writeLock().unlock();}
|
||||
}
|
||||
|
||||
public void addFirst(Number x, Number y) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (xVals != null) {
|
||||
xVals.addFirst(x);
|
||||
}
|
||||
yVals.addFirst(y);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {@link XYCoords} with first equal to x-val and second equal to y-val.
|
||||
*/
|
||||
public XYCoords removeFirst() {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (size() <= 0) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return new XYCoords(xVals != null ? xVals.removeFirst() : 0, yVals.removeFirst());
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void addLast(Number x, Number y) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (xVals != null) {
|
||||
xVals.addLast(x);
|
||||
}
|
||||
yVals.addLast(y);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {@link XYCoords} with first equal to x-val and second equal to y-val.
|
||||
*/
|
||||
public XYCoords removeLast() {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (size() <= 0) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return new XYCoords(xVals != null ? xVals.removeLast() : yVals.size() - 1, yVals.removeLast());
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
this.title = title;
|
||||
} finally {lock.writeLock().unlock();}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return yVals != null ? yVals.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getX(int index) {
|
||||
return xVals != null ? xVals.get(index) : index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number getY(int index) {
|
||||
return yVals.get(index);
|
||||
}
|
||||
|
||||
public LinkedList<Number> getxVals() {
|
||||
return xVals;
|
||||
}
|
||||
|
||||
public LinkedList<Number> getyVals() {
|
||||
return yVals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all values from the series
|
||||
*/
|
||||
public void clear() {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (xVals != null) {
|
||||
xVals.clear();
|
||||
}
|
||||
yVals.clear();
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2015 AndroidPlot.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.androidplot.xy;
|
||||
|
||||
/**
|
||||
* An immutable object generated by XYStepCalculator representing
|
||||
* a stepping model to be used by an XYPlot.
|
||||
*/
|
||||
public class Step {
|
||||
|
||||
private final double stepCount;
|
||||
private final double stepPix;
|
||||
private final double stepVal;
|
||||
|
||||
//public XYStep() {}
|
||||
|
||||
public Step(double stepCount, double stepPix, double stepVal) {
|
||||
this.stepCount = stepCount;
|
||||
this.stepPix = stepPix;
|
||||
this.stepVal = stepVal;
|
||||
}
|
||||
|
||||
public double getStepCount() {
|
||||
return stepCount;
|
||||
}
|
||||
|
||||
/*public void setStepCount(double stepCount) {
|
||||
this.stepCount = stepCount;
|
||||
}*/
|
||||
|
||||
public double getStepPix() {
|
||||
return stepPix;
|
||||
}
|
||||
|
||||
/*public void setStepPix(float stepPix) {
|
||||
this.stepPix = stepPix;
|
||||
}*/
|
||||
|
||||
public double getStepVal() {
|
||||
return stepVal;
|
||||
}
|
||||
|
||||
/*public void setStepVal(double stepVal) {
|
||||
this.stepVal = stepVal;
|
||||
}*/
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user