Skip to content
Commits on Source (1)
/*
* Copyright 2014 serso aka se.solovyev
*
* 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.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Contact details
*
* Email: se.solovyev@gmail.com
* Site: http://se.solovyev.org
*/
apply plugin: 'com.android.application'
apply plugin: 'maven'
apply plugin: 'signing'
android {
compileSdkVersion android_sdk_version()
buildToolsVersion android_build_tools_version()
defaultConfig {
minSdkVersion android_min_sdk_version()
targetSdkVersion android_sdk_version()
versionCode version_code()
versionName version_name()
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'
zipAlignEnabled true
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'com.jakewharton:butterknife:7.0.1'
compile 'org.solovyev.android:material:0.1.4@aar'
compile 'com.android.support:recyclerview-v7:23.1.1'
compile 'org.solovyev.android.views:linear-layout-manager:0.5@aar'
compile 'com.squareup:otto:1.3.8'
compile files('libs/exp4j-0.4.6-SNAPSHOT.jar')
compile('ch.acra:acra:4.5.0') {
exclude group: 'org.json'
}
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
compile project(':lib')
}
task androidJavadocs(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
configurations.compile.each { File file -> classpath += project.files(file.path) }
}
task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
classifier = 'javadoc'
from androidJavadocs.destinationDir
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
artifacts {
archives androidSourcesJar
archives file: file('build/outputs/apk/plotter-release.apk'), name: 'plotter-app', classifier: 'signed', type: 'apk'
archives file: file('build/outputs/mapping/release/mapping.txt'), name: 'plotter-app', classifier: 'proguard', type: 'txt'
}
signing {
sign configurations.archives
}
group = "org.solovyev.android"
version = version_name()
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
artifactId = 'plotter-app'
name 'Android Plotter App'
packaging 'apk'
description 'Sample App for Android Plotter'
url 'https://github.com/serso/android-plotter'
scm {
url 'https://github.com/serso/android-plotter'
connection 'scm:https://serso@github.com/serso/android-plotter.git'
developerConnection 'scm:git://github.com/serso/android-plotter.git'
}
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
distribution 'repo'
}
}
developers {
developer {
id 'se.solovyev'
name 'Sergey Solovyev'
email 'se.solovyev@gmail.com'
}
}
}
}
}
}
#
#*********************************************************************
#
# PLOTTER
#
#*********************************************************************
#
-assumenosideeffects class org.solovyev.android.plotter.Check {
static void *(...);
}
#
#*********************************************************************
#
# LOGS
#
#*********************************************************************
#
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int d(...);
public static int i(...);
public static int w(...);
public static int e(...);
}
#
#*********************************************************************
#
# ACRA
#
#*********************************************************************
#
# we need line numbers in our stack traces otherwise they are pretty useless
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
# ACRA needs "annotations" so add this...
-keepattributes *Annotation*
# keep this class so that logging will show 'ACRA' and not a obfuscated name like 'a'.
# Note: if you are removing log messages elsewhere in this file then this isn't necessary
-keep class org.acra.ACRA {
*;
}
-keep class org.acra.ReportField {
*;
}
# keep this around for some enums that ACRA needs
-keep class org.acra.ReportingInteractionMode {
*;
}
# keep this otherwise it is removed by ProGuard
-keep public class org.acra.ErrorReporter
{
public void addCustomData(java.lang.String,java.lang.String);
}
# keep this otherwise it is removed by ProGuard
-keep public class org.acra.ErrorReporter
{
public org.acra.ErrorReporter$ReportsSenderWorker handleSilentException(java.lang.Throwable);
}
#
#*********************************************************************
#
# EVENT BUS
#
#*********************************************************************
#
-keepclassmembers class ** {
@com.squareup.otto.Subscribe public *;
@com.squareup.otto.Produce public *;
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2014 serso aka se.solovyev
~
~ 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.
~
~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ Contact details
~
~ Email: se.solovyev@gmail.com
~ Site: http://se.solovyev.org
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.solovyev.android.plotter.app">
<supports-screens android:smallScreens="true" />
<supports-screens android:normalScreens="true" />
<supports-screens android:largeScreens="true" />
<supports-screens
android:xlargeScreens="true"
tools:ignore="UnusedAttribute" />
<supports-screens android:anyDensity="true" />
<application
android:name=".PlotterApplication"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:supportsRtl="false"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<activity
android:name=".MainActivity"
android:theme="@style/Theme.AppCompat">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
package org.solovyev.android.drag;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import org.solovyev.android.plotter.app.R;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
public class DirectionDragButton extends DragButton {
@NonNull
private final static Float DEFAULT_DIRECTION_TEXT_SCALE_FLOAT = 0.4f;
@NonNull
private final static Integer DEFAULT_DIRECTION_TEXT_ALPHA = 140;
private final static int DEFAULT_DIRECTION_TEXT_COLOR = Color.WHITE;
@NonNull
private final static String DEFAULT_DIRECTION_TEXT_SCALE = "0.4;0.4;0.4;0.4";
@NonNull
private final Map<Direction, DirectionTextData> textDataMap = new EnumMap<>(Direction.class);
@NonNull
protected String directionTextScale = DEFAULT_DIRECTION_TEXT_SCALE;
@NonNull
protected Integer directionTextAlpha = DEFAULT_DIRECTION_TEXT_ALPHA;
protected int directionTextColor = DEFAULT_DIRECTION_TEXT_COLOR;
private boolean initialized = false;
public DirectionDragButton(Context context) {
super(context);
init(context, null);
}
public DirectionDragButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public DirectionDragButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public DirectionDragButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(@NonNull Context context, @Nullable AttributeSet attrs) {
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DirectionDragButton);
for (int i = 0; i < a.getIndexCount(); i++) {
int attr = a.getIndex(i);
if (a.hasValue(attr)) {
if (attr == R.styleable.DirectionDragButton_directionTextColor) {
this.directionTextColor = a.getColor(attr, DEFAULT_DIRECTION_TEXT_COLOR);
} else if (attr == R.styleable.DirectionDragButton_directionTextScale) {
this.directionTextScale = a.getString(attr);
} else if (attr == R.styleable.DirectionDragButton_directionTextAlpha) {
this.directionTextAlpha = a.getInt(attr, DEFAULT_DIRECTION_TEXT_ALPHA);
} else {
// try drag direction text
for (Direction direction : Direction.values()) {
if (direction.getAttributeId() == attr) {
this.textDataMap.put(direction, new DirectionTextData(direction, a.getString(attr)));
break;
}
}
}
}
}
a.recycle();
}
for (Map.Entry<Direction, Float> entry : getDirectionTextScales().entrySet()) {
final DirectionTextData td = textDataMap.get(entry.getKey());
if (td != null) {
td.scale = entry.getValue();
}
}
initialized = true;
}
@Override
public void onSizeChanged(int w, int h, int oldW, int oldH) {
measureText();
}
@Override
protected void onTextChanged(CharSequence text, int start, int before, int after) {
measureText();
}
protected void measureText() {
if (initialized) {
final int width = getWidth() - getPaddingLeft() - getPaddingRight();
final int height = getHeight() - getPaddingTop() - getPaddingBottom();
final Paint basePaint = getPaint();
for (DirectionTextData textData : textDataMap.values()) {
initDirectionTextPaint(basePaint, textData);
textData.position = textData.direction.getTextPosition(textData.paint, basePaint, textData.text, getText(), width, height);
}
invalidate();
}
}
protected void initDirectionTextPaint(@NonNull Paint basePaint, @NonNull DirectionTextData textData) {
textData.init(basePaint, directionTextColor, directionTextAlpha);
}
public void setDirectionTextColor(int directionTextColor) {
this.directionTextColor = directionTextColor;
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
final TextPaint paint = getPaint();
for (DirectionTextData td : textDataMap.values()) {
if (td.show) {
initDirectionTextPaint(paint, td);
final String text = td.text;
final PointF position = td.position;
canvas.drawText(text, 0, text.length(), position.x, position.y, td.paint);
}
}
}
@SuppressWarnings("UnusedDeclaration")
@Nullable
public String getTextUp() {
return getText(Direction.up);
}
@SuppressWarnings("UnusedDeclaration")
@Nullable
public String getTextDown() {
return getText(Direction.down);
}
@Nullable
public String getText(@NonNull DragDirection direction) {
final Direction guiDragDirection = Direction.valueOf(direction);
return guiDragDirection == null ? null : getText(guiDragDirection);
}
@SuppressWarnings("UnusedDeclaration")
public void showDirectionText(boolean show, @NonNull DragDirection direction) {
final Direction guiDragDirection = Direction.valueOf(direction);
final DirectionTextData td = this.textDataMap.get(guiDragDirection);
if (td != null) {
td.show = show;
}
}
@NonNull
public DirectionDragButton setText(@Nullable String text, @NonNull Direction direction) {
if (!TextUtils.isEmpty(text)) {
final DirectionTextData data = new DirectionTextData(direction, text);
initDirectionTextPaint(getPaint(), data);
textDataMap.put(direction, data);
} else {
textDataMap.remove(direction);
}
measureText();
return this;
}
@Nullable
private String getText(@NonNull Direction direction) {
DirectionTextData td = textDataMap.get(direction);
if (td == null) {
return null;
} else {
if (td.show) {
return td.text;
} else {
return null;
}
}
}
@NonNull
public String getDirectionTextScale() {
return directionTextScale;
}
@NonNull
private Map<Direction, Float> getDirectionTextScales() {
final List<Float> scales = new ArrayList<>();
final StringTokenizer t = new StringTokenizer(getDirectionTextScale(), ";");
while (t.hasMoreTokens()) {
final String scale = t.nextToken();
try {
scales.add(Float.valueOf(scale));
} catch (NumberFormatException e) {
}
}
final Map<Direction, Float> result = new HashMap<>();
for (Direction direction : Direction.values()) {
result.put(direction, DEFAULT_DIRECTION_TEXT_SCALE_FLOAT);
}
if (scales.size() == 1) {
final Float scale = scales.get(0);
for (Map.Entry<Direction, Float> entry : result.entrySet()) {
entry.setValue(scale);
}
} else {
for (int i = 0; i < scales.size(); i++) {
for (Direction direction : Direction.values()) {
if (direction.getAttributePosition() == i) {
result.put(direction, scales.get(i));
}
}
}
}
return result;
}
public static enum Direction {
up(DragDirection.up, 0) {
@Override
public int getAttributeId() {
return R.styleable.DirectionDragButton_textUp;
}
@NonNull
@Override
public PointF getTextPosition(@NonNull Paint paint, @NonNull Paint basePaint, @NonNull CharSequence text, CharSequence baseText, int w, int h) {
return getUpDownTextPosition(paint, basePaint, text, baseText, 1, w, h);
}
},
down(DragDirection.down, 2) {
@Override
public int getAttributeId() {
return R.styleable.DirectionDragButton_textDown;
}
@NonNull
@Override
public PointF getTextPosition(@NonNull Paint paint, @NonNull Paint basePaint, @NonNull CharSequence text, CharSequence baseText, int w, int h) {
return getUpDownTextPosition(paint, basePaint, text, baseText, -1, w, h);
}
},
left(DragDirection.left, 3) {
@Override
public int getAttributeId() {
return R.styleable.DirectionDragButton_textLeft;
}
@NonNull
@Override
public PointF getTextPosition(@NonNull Paint paint, @NonNull Paint basePaint, @NonNull CharSequence text, CharSequence baseText, int w, int h) {
return getLeftRightTextPosition(paint, basePaint, text, baseText, w, h, true);
}
},
right(DragDirection.right, 1) {
@Override
public int getAttributeId() {
return R.styleable.DirectionDragButton_textRight;
}
@NonNull
@Override
public PointF getTextPosition(@NonNull Paint paint, @NonNull Paint basePaint, @NonNull CharSequence text, CharSequence baseText, int w, int h) {
return getLeftRightTextPosition(paint, basePaint, text, baseText, w, h, false);
}
};
@NonNull
private final DragDirection dragDirection;
private final int attributePosition;
Direction(@NonNull DragDirection dragDirection, int attributePosition) {
this.dragDirection = dragDirection;
this.attributePosition = attributePosition;
}
@NonNull
private static PointF getLeftRightTextPosition(@NonNull Paint paint, @NonNull Paint basePaint, CharSequence text, @NonNull CharSequence baseText, int w, int h, boolean left) {
final PointF result = new PointF();
if (left) {
result.x = paint.measureText(" ");
} else {
float width = paint.measureText(text.toString() + " ");
result.x = w - width;
}
float selfHeight = paint.ascent() + paint.descent();
basePaint.measureText(getNotEmpty(baseText, "|"));
result.y = h / 2 - selfHeight / 2;
return result;
}
@NonNull
private static String getNotEmpty(@Nullable CharSequence s, @NonNull String def) {
return TextUtils.isEmpty(s) ? def : s.toString();
}
@NonNull
private static PointF getUpDownTextPosition(@NonNull Paint paint, @NonNull Paint basePaint, @NonNull CharSequence text, CharSequence baseText, float direction, int w, int h) {
final PointF result = new PointF();
float width = paint.measureText(text.toString() + " ");
result.x = w - width;
float selfHeight = paint.ascent() + paint.descent();
basePaint.measureText(getNotEmpty(baseText, "|"));
if (direction < 0) {
result.y = h / 2 + h / 3 - selfHeight / 2;
} else {
result.y = h / 2 - h / 3 - selfHeight / 2;
}
return result;
}
@Nullable
public static Direction valueOf(@NonNull DragDirection dragDirection) {
for (Direction direction : values()) {
if (direction.dragDirection == dragDirection) {
return direction;
}
}
return null;
}
public abstract int getAttributeId();
public int getAttributePosition() {
return attributePosition;
}
@NonNull
public abstract PointF getTextPosition(@NonNull Paint paint, @NonNull Paint basePaint, @NonNull CharSequence text, CharSequence baseText, int w, int h);
}
protected static class DirectionTextData {
@NonNull
private final Direction direction;
@NonNull
private final TextPaint paint = new TextPaint();
@NonNull
private String text;
@NonNull
private PointF position;
@NonNull
private Float scale = DEFAULT_DIRECTION_TEXT_SCALE_FLOAT;
private boolean show = true;
private DirectionTextData(@NonNull Direction direction, @NonNull String text) {
this.direction = direction;
this.text = text;
}
protected void init(@NonNull Paint basePaint,
int color,
int alpha) {
paint.set(basePaint);
paint.setColor(color);
paint.setAlpha(alpha);
paint.setTextSize(basePaint.getTextSize() * scale);
}
@NonNull
public Direction getDirection() {
return direction;
}
@NonNull
public String getText() {
return text;
}
@NonNull
public PointF getPosition() {
return position;
}
@NonNull
public TextPaint getPaint() {
return paint;
}
@NonNull
public Float getScale() {
return scale;
}
public boolean isShow() {
return show;
}
}
}
package org.solovyev.android.drag;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Button;
import android.widget.TextView;
public class DragButton extends Button {
@Nullable
private PointF startPoint = null;
@Nullable
private DragListener onDragListener;
private boolean showText = true;
@Nullable
private CharSequence textBackup;
public DragButton(Context context) {
super(context);
}
public DragButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DragButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public DragButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public static boolean drawDrawables(Canvas canvas, TextView textView) {
if (textView == null) {
throw new IllegalArgumentException("Argument 1 for @NotNull parameter of org/solovyev/android/view/AndroidViewUtils.drawDrawables must not be null");
} else {
int compoundPaddingLeft = textView.getCompoundPaddingLeft();
int compoundPaddingTop = textView.getCompoundPaddingTop();
int compoundPaddingRight = textView.getCompoundPaddingRight();
int compoundPaddingBottom = textView.getCompoundPaddingBottom();
int scrollX = textView.getScrollX();
int scrollY = textView.getScrollY();
int right = textView.getRight();
int left = textView.getLeft();
int bottom = textView.getBottom();
int top = textView.getTop();
Drawable[] drawables = textView.getCompoundDrawables();
int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
Drawable topDr = drawables[1];
if (topDr != null) {
canvas.save();
canvas.translate((float) (scrollX + compoundPaddingLeft + (hspace - topDr.getBounds().width()) / 2), (float) (scrollY + textView.getPaddingTop() + vspace / 2));
topDr.draw(canvas);
canvas.restore();
return true;
}
return false;
}
}
public void setOnDragListener(@Nullable DragListener onDragListener) {
this.onDragListener = onDragListener;
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
boolean consumed = false;
// in order to avoid possible NPEs
final PointF localStartPoint = startPoint;
final DragListener localOnDragListener = onDragListener;
if (localOnDragListener != null) {
// only if onDrag() listener specified
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// start tracking: set start point
startPoint = new PointF(event.getX(), event.getY());
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// stop tracking
startPoint = null;
if (localStartPoint != null) {
consumed = localOnDragListener.onDrag(DragButton.this, new DragEvent(localStartPoint, event));
if (consumed && localOnDragListener.isSuppressOnClickEvent()) {
final MotionEvent newEvent = MotionEvent.obtain(event);
newEvent.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(newEvent);
newEvent.recycle();
return true;
}
}
break;
}
}
return super.onTouchEvent(event) || consumed;
}
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
return super.dispatchTouchEvent(event);
}
@Override
protected void onDraw(Canvas canvas) {
CharSequence text = getText();
if (!TextUtils.isEmpty(text)) {
super.onDraw(canvas);
} else {
if (!drawDrawables(canvas, this)) {
super.onDraw(canvas);
}
}
}
public boolean isShowText() {
return showText;
}
public void setShowText(boolean showText) {
if (this.showText != showText) {
if (showText) {
setText(textBackup);
textBackup = null;
} else {
textBackup = this.getText();
setText(null);
}
this.showText = showText;
}
}
}
package org.solovyev.android.drag;
public enum DragDirection {
up,
down,
left,
right;
}
package org.solovyev.android.drag;
import android.graphics.PointF;
import android.support.annotation.NonNull;
import android.view.MotionEvent;
public class DragEvent {
@NonNull
private final PointF startPoint;
@NonNull
private final MotionEvent motionEvent;
public DragEvent(@NonNull PointF startPoint, @NonNull MotionEvent motionEvent) {
this.startPoint = startPoint;
this.motionEvent = motionEvent;
}
/**
* @return motion event started at start point
*/
@NonNull
public MotionEvent getMotionEvent() {
return motionEvent;
}
/**
* @return start point of dragging
*/
@NonNull
public PointF getStartPoint() {
return startPoint;
}
}
package org.solovyev.android.drag;
import android.support.annotation.NonNull;
import java.util.EventListener;
public interface DragListener extends EventListener {
/**
* @return 'true': if drag event has taken place (i.e. onDrag() method returned true) then click action will be suppresed
*/
boolean isSuppressOnClickEvent();
/**
* @param dragButton drag button object for which onDrag listener was set
* @param event drag event
* @return 'true' if drag event occurred, 'false' otherwise
*/
boolean onDrag(@NonNull DragButton dragButton, @NonNull DragEvent event);
}
package org.solovyev.android.drag;
import android.support.annotation.NonNull;
/**
* User: serso
* Date: 10/26/11
* Time: 10:37 PM
*/
public class DragListenerWrapper implements DragListener {
@NonNull
private final DragListener dragListener;
public DragListenerWrapper(@NonNull DragListener dragListener) {
this.dragListener = dragListener;
}
@Override
public boolean isSuppressOnClickEvent() {
return this.dragListener.isSuppressOnClickEvent();
}
@Override
public boolean onDrag(@NonNull DragButton dragButton, @NonNull DragEvent event) {
return this.dragListener.onDrag(dragButton, event);
}
}
package org.solovyev.android.drag;
/*
* Copyright 2013 serso aka se.solovyev
*
* 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.
*
* ---------------------------------------------------------------------
* Contact details
*
* Email: se.solovyev@gmail.com
* Site: http://se.solovyev.org
*/
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* User: serso
* Date: 9/19/11
* Time: 4:51 PM
*/
class Interval<T extends Comparable<T>> {
@NonNull
protected IntervalLimit<T> left;
@NonNull
protected IntervalLimit<T> right;
protected Interval() {
}
private Interval(@NonNull IntervalLimit<T> left,
@NonNull IntervalLimit<T> right) {
int c = left.compareTo(right);
if (c > 0) {
throw new IllegalArgumentException("Left limit must <= than right!");
} else if (c == 0) {
if (left.isOpened() && right.isOpened()) {
throw new IllegalArgumentException("Empty interval (x, x) is not possible!");
}
}
this.left = left;
this.right = right;
}
@NonNull
static <T extends Comparable<T>> Interval<T> create(@NonNull IntervalLimit<T> left,
@NonNull IntervalLimit<T> right) {
return new Interval<T>(left, right);
}
/**
* @return left border
*/
@Nullable
public T getLeftLimit() {
return left.getValue();
}
/**
* @return right border
*/
@Nullable
public T getRightLimit() {
return this.right.getValue();
}
@NonNull
public IntervalLimit<T> getRight() {
return this.right;
}
@NonNull
public IntervalLimit<T> getLeft() {
return this.left;
}
/**
* @param value value
* @return true if single value inside interval, false otherwise
*/
public boolean contains(@NonNull T value) {
return this.left.isLowerOrEqualsThan(value) && this.right.isHigherOrEqualsThan(value);
}
/**
* @param that interval.
* @return true if that interval is inside this interval, false otherwise
*/
public boolean contains(@NonNull Interval<T> that) {
return this.left.isLowerOrEqualsThan(that.getLeft()) && this.right.isHigherOrEqualsThan(that.getRight());
}
/**
* @return true if interval is closed (borders != null), false otherwise.
*/
public boolean isClosed() {
return this.left.isClosed() && this.right.isClosed();
}
/**
* @return true if interval is infinity (borders == null), false otherwise.
*/
public boolean isInfinite() {
return this.left.isLowest() && this.right.isHighest();
}
/**
* @return true if only one border is closed, false otherwise.
*/
public boolean isHalfClosed() {
return (this.left.isClosed() && !this.right.isClosed()) || (!this.left.isClosed() && this.right.isClosed());
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
if (this.left.isClosed()) {
sb.append("[");
} else {
sb.append("(");
}
sb.append(this.left).append(", ");
sb.append(this.right);
if (this.right.isClosed()) {
sb.append("]");
} else {
sb.append(")");
}
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Interval)) {
return false;
}
final Interval that = (Interval) o;
if (!this.left.equals(that.left)) {
return false;
}
if (!this.right.equals(that.right)) {
return false;
}
return true;
}
protected boolean areEqual(@NonNull T thisBorder, @Nullable Object thatBorder) {
return thisBorder.equals(thatBorder);
}
@Override
public int hashCode() {
int result = left.hashCode();
result = 31 * result + right.hashCode();
return result;
}
}
package org.solovyev.android.drag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
class IntervalLimit<T extends Comparable<T>> {
public static final Integer BOTH_NULLS_CONST = 0;
@NonNull
private final Type type;
@Nullable
private T value;
private boolean closed;
private IntervalLimit(@NonNull Type type) {
this.type = type;
}
@NonNull
public static <T extends Comparable<T>> IntervalLimit<T> create(@NonNull T value, boolean closed) {
final IntervalLimit<T> result = new IntervalLimit<>(Type.between);
result.value = value;
result.closed = closed;
return result;
}
@NonNull
public static <T extends Comparable<T>> IntervalLimit<T> newLowest() {
return create(Type.lowest);
}
@NonNull
public static <T extends Comparable<T>> IntervalLimit<T> newHighest() {
return create(Type.highest);
}
@NonNull
private static <T extends Comparable<T>> IntervalLimit<T> create(@NonNull Type type) {
final IntervalLimit<T> result = new IntervalLimit<>(type);
result.value = null;
result.closed = false;
return result;
}
public static int compare(Object value1, Object value2) {
Integer result = compareOnNullness(value1, value2);
if (result == null) {
if (value1 instanceof Comparable && value2 instanceof Comparable) {
//noinspection unchecked
result = ((Comparable) value1).compareTo(value2);
} else {
result = 0;
}
}
return result;
}
@Nullable
public static Integer compareOnNullness(Object o1, Object o2) {
Integer result;
if (o1 == null && o2 == null) {
result = BOTH_NULLS_CONST;
} else if (o1 == null) {
result = -1;
} else if (o2 == null) {
result = 1;
} else {
//both not nulls
result = null;
}
return result;
}
@Nullable
public T getValue() {
return this.value;
}
public boolean isClosed() {
return this.closed;
}
public boolean isOpened() {
return !this.closed;
}
public boolean isLowest() {
return this.type == Type.lowest;
}
public boolean isHighest() {
return this.type == Type.highest;
}
public boolean isLowerOrEqualsThan(@NonNull T that) {
if (this.isLowest()) {
return true;
} else if (this.isHighest()) {
return false;
} else {
if (this.isClosed()) {
assert this.value != null;
return this.value.compareTo(that) <= 0;
} else {
assert this.value != null;
return this.value.compareTo(that) < 0;
}
}
}
public boolean isLowerOrEqualsThan(@NonNull IntervalLimit<T> that) {
if (this.isLowest()) {
return that.isLowest();
} else if (this.isHighest()) {
return that.isHighest();
} else {
if (this.isClosed()) {
return this.compareTo(that) <= 0;
} else {
return this.compareTo(that) < 0;
}
}
}
public boolean isHigherOrEqualsThan(@NonNull T that) {
if (this.isHighest()) {
return true;
} else if (this.isLowest()) {
return false;
} else {
assert this.value != null;
if (this.isClosed()) {
return this.value.compareTo(that) >= 0;
} else {
return this.value.compareTo(that) > 0;
}
}
}
public boolean isHigherOrEqualsThan(@NonNull IntervalLimit<T> that) {
if (this.isHighest()) {
return that.isHighest();
} else if (this.isLowest()) {
return that.isLowest();
} else {
if (this.isClosed()) {
return this.compareTo(that) >= 0;
} else {
return this.compareTo(that) > 0;
}
}
}
public int compareTo(@NonNull IntervalLimit<T> that) {
if (this == that) {
return 0;
}
if (this.isLowest()) {
if (that.isLowest()) {
return 0;
} else {
return -1;
}
} else if (this.isHighest()) {
if (that.isHighest()) {
return 0;
} else {
return 1;
}
}
return compare(this.value, that.getValue());
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof IntervalLimit)) {
return false;
}
IntervalLimit that = (IntervalLimit) o;
if (closed != that.closed) {
return false;
}
if (type != that.type) {
return false;
}
if (value != null ? !value.equals(that.value) : that.value != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = value != null ? value.hashCode() : 0;
result = 31 * result + (closed ? 1 : 0);
result = 31 * result + type.hashCode();
return result;
}
@Override
public String toString() {
if (this.isLowest()) {
return "-Inf";
} else if (this.isHighest()) {
return "Inf";
} else {
return String.valueOf(this.value);
}
}
public enum Type {
lowest,
between,
highest
}
}
package org.solovyev.android.drag;
import android.support.annotation.NonNull;
public final class Intervals {
private Intervals() {
throw new AssertionError();
}
@NonNull
public static <T extends Comparable<T>> Interval<T> newPoint(@NonNull T point) {
return newInstance(IntervalLimit.create(point, true), IntervalLimit.create(point, true));
}
@NonNull
public static <T extends Comparable<T>> Interval<T> newInterval(@NonNull T left, boolean leftClosed, @NonNull T right, boolean rightClosed) {
return newInstance(newLimit(left, leftClosed), newLimit(right, rightClosed));
}
@NonNull
public static <T extends Comparable<T>> Interval<T> newClosedInterval(@NonNull T left, @NonNull T right) {
return newInstance(newClosedLimit(left), newClosedLimit(right));
}
@NonNull
public static <T extends Comparable<T>> IntervalLimit<T> newClosedLimit(@NonNull T value) {
return newLimit(value, true);
}
@NonNull
public static <T extends Comparable<T>> IntervalLimit<T> newLimit(@NonNull T value, boolean closed) {
return IntervalLimit.create(value, closed);
}
@NonNull
public static <T extends Comparable<T>> IntervalLimit<T> newOpenedLimit(@NonNull T value) {
return newLimit(value, false);
}
@NonNull
public static <T extends Comparable<T>> Interval<T> newInstance(@NonNull IntervalLimit<T> left,
@NonNull IntervalLimit<T> right) {
return Interval.create(left, right);
}
@NonNull
public static <T extends Comparable<T>> IntervalLimit<T> newLowestLimit() {
return IntervalLimit.newLowest();
}
@NonNull
public static <T extends Comparable<T>> IntervalLimit<T> newHighestLimit() {
return IntervalLimit.newHighest();
}
}
package org.solovyev.android.drag;
import android.graphics.PointF;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
public class Maths {
public static final float MIN_AMOUNT = 0.05f;
/**
* @param nominal nominal
* @param value value
* @return nearest value to specified value that can be divided by nominal value without remainder
*/
public static double getRoundedAmount(double nominal, double value) {
double result;
int numberOfTimes = (int) (value / nominal);
result = numberOfTimes * nominal;
return result;
}
/**
* @param l first number
* @param sign sign
* @param r second number
* @return sum or difference of two numbers (supposed: null = 0)
*/
public static double sumUp(Double l, int sign, Double r) {
double result = 0d;
if (l != null && r != null) {
result = l + sign * r;
} else if (l != null) {
result = l;
} else if (r != null) {
result = sign * r;
}
return result;
}
/**
* @param l first number
* @param r second number
* @return sum of tow numbers (supposed: null = 0)
*/
public static double sumUp(Double l, Double r) {
return sumUp(l, 1, r);
}
/**
* @param l fist number
* @param r second number
* @return difference of two numbers (supposed: null = 0)
*/
public static double subtract(Double l, Double r) {
return sumUp(l, -1, r);
}
/**
* Method compares two double values with specified precision
*
* @param d1 first value to compare
* @param d2 second value for compare
* @param precision number of digits after dot
* @return 'true' if values are equal with specified precision
*/
public static boolean equals(double d1, double d2, int precision) {
return Math.abs(d1 - d2) < getMaxPreciseAmount(precision);
}
/**
* Method tests if first value is less than second with specified precision
*
* @param d1 first value to compare
* @param d2 second value for compare
* @param precision number of digits after dot
* @return 'true' if first value is less than second with specified precision
*/
public static boolean less(double d1, double d2, int precision) {
return d1 < d2 - getMaxPreciseAmount(precision);
}
/**
* Method tests if first value is more than second with specified precision
*
* @param d1 first value to compare
* @param d2 second value for compare
* @param precision number of digits after dot
* @return 'true' if first value is more than second with specified precision
*/
public static boolean more(double d1, double d2, int precision) {
return d1 > d2 + getMaxPreciseAmount(precision);
}
private static double getMaxPreciseAmount(int precision) {
return Math.pow(0.1d, precision) / 2;
}
public static double getNotNull(@Nullable Double value) {
return value != null ? value : 0d;
}
public static double round(@NonNull Double value, int precision) {
double factor = Math.pow(10, precision);
return ((double) Math.round(value * factor)) / factor;
}
public static float getDistance(@NonNull PointF startPoint,
@NonNull PointF endPoint) {
return getNorm(subtract(endPoint, startPoint));
}
public static PointF subtract(@NonNull PointF p1, @NonNull PointF p2) {
return new PointF(p1.x - p2.x, p1.y - p2.y);
}
public static PointF sum(@NonNull PointF p1, @NonNull PointF p2) {
return new PointF(p1.x + p2.x, p1.y + p2.y);
}
public static float getNorm(@NonNull PointF point) {
return (float) Math.pow(Math.pow(point.x, 2) + Math.pow(point.y, 2), 0.5);
}
public static float getAngle(@NonNull PointF startPoint,
@NonNull PointF axisEndPoint,
@NonNull PointF endPoint,
@Nullable MutableObject<Boolean> left) {
final PointF axisVector = subtract(axisEndPoint, startPoint);
final PointF vector = subtract(endPoint, startPoint);
double a_2 = Math.pow(getDistance(vector, axisVector), 2);
double b = getNorm(vector);
double b_2 = Math.pow(b, 2);
double c = getNorm(axisVector);
double c_2 = Math.pow(c, 2);
if (left != null) {
left.setObject(axisVector.x * vector.y - axisVector.y * vector.x < 0);
}
return (float) Math.acos((-a_2 + b_2 + c_2) / (2 * b * c));
}
public static double countMean(@NonNull List<Double> objects) {
double sum = 0d;
for (Double object : objects) {
sum += object;
}
return objects.size() == 0 ? 0d : (sum / objects.size());
}
public static double countStandardDeviation(@NonNull Double mean, @NonNull List<Double> objects) {
double sum = 0d;
for (Double object : objects) {
sum += Math.pow(object - mean, 2);
}
return objects.size() == 0 ? 0d : Math.sqrt(sum / objects.size());
}
public static StatData getStatData(@NonNull List<Double> objects) {
final double mean = countMean(objects);
final double standardDeviation = countStandardDeviation(mean, objects);
return new StatData(mean, standardDeviation);
}
private static <T extends Comparable<T>> boolean earlier(@Nullable T t1,
boolean isNegativeInf1,
@Nullable T t2,
boolean isNegativeInf2) {
boolean result;
if (t1 == null && t2 == null && (isNegativeInf1 == isNegativeInf2)) {
// -inf and -inf or +inf and +inf
result = false;
} else if (t1 == null) {
// anything bigger then -inf if left
result = isNegativeInf1;
} else if (t2 == null) {
// anything lower then +inf if right
result = !isNegativeInf2;
} else {
result = t1.compareTo(t2) < 0;
}
return result;
}
static int pow(int value, int exponent) {
if (exponent == 0) {
return 1;
}
if (exponent == 1) {
return value;
}
if (exponent % 2 == 0) {
return pow(value * value, exponent / 2);
} else {
return value * pow(value * value, (exponent - 1) / 2);
}
}
public static enum ComparisonType {
min,
max
}
public static class StatData {
private final double mean;
private final double standardDeviation;
public StatData(double mean, double standardDeviation) {
this.mean = mean;
this.standardDeviation = standardDeviation;
}
public double getMean() {
return mean;
}
public double getStandardDeviation() {
return standardDeviation;
}
}
}
package org.solovyev.android.drag;
public class MutableObject<T> {
private volatile T object;
public MutableObject() {
}
public MutableObject(T object) {
this.object = object;
}
public T getObject() {
return object;
}
public void setObject(T object) {
this.object = object;
}
}
package org.solovyev.android.drag;
import android.content.Context;
import android.graphics.PointF;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.MotionEvent;
import org.solovyev.android.plotter.app.R;
import java.util.EnumMap;
public class SimpleDragListener implements DragListener {
@NonNull
private static final PointF axis = new PointF(0, 1);
@NonNull
private static final EnumMap<DragDirection, Interval<Float>> sAngleIntervals = new EnumMap<>(DragDirection.class);
static {
for (DragDirection direction : DragDirection.values()) {
sAngleIntervals.put(direction, makeAngleInterval(direction, 0, 45));
}
}
@NonNull
private final DragProcessor processor;
private final float minDistancePxs;
public SimpleDragListener(@NonNull DragProcessor processor, @NonNull Context context) {
this.processor = processor;
this.minDistancePxs = context.getResources().getDimensionPixelSize(R.dimen.drag_min_distance);
}
@NonNull
private static Interval<Float> makeAngleInterval(@NonNull DragDirection direction,
float leftLimit,
float rightLimit) {
final Float newLeftLimit;
final Float newRightLimit;
switch (direction) {
case up:
newLeftLimit = 180f - rightLimit;
newRightLimit = 180f - leftLimit;
break;
case down:
newLeftLimit = leftLimit;
newRightLimit = rightLimit;
break;
case left:
newLeftLimit = 90f - rightLimit;
newRightLimit = 90f + rightLimit;
break;
case right:
newLeftLimit = 90f - rightLimit;
newRightLimit = 90f + rightLimit;
break;
default:
throw new AssertionError();
}
return Intervals.newClosedInterval(newLeftLimit, newRightLimit);
}
@Override
public boolean onDrag(@NonNull DragButton dragButton, @NonNull DragEvent event) {
boolean consumed = false;
final MotionEvent motionEvent = event.getMotionEvent();
final PointF start = event.getStartPoint();
final PointF end = new PointF(motionEvent.getX(), motionEvent.getY());
final float distance = Maths.getDistance(start, end);
final MutableObject<Boolean> right = new MutableObject<>();
final double angle = Math.toDegrees(Maths.getAngle(start, Maths.sum(start, axis), end, right));
final long duration = motionEvent.getEventTime() - motionEvent.getDownTime();
final DragDirection direction = getDirection(distance, (float) angle, right.getObject());
if (direction != null && duration > 40 && duration < 2500) {
consumed = processor.processDragEvent(direction, dragButton, start, motionEvent);
}
return consumed;
}
@Nullable
private DragDirection getDirection(float distance, float angle, boolean right) {
if (distance > minDistancePxs) {
for (DragDirection direction : DragDirection.values()) {
final Interval<Float> angleInterval = sAngleIntervals.get(direction);
final boolean wrongDirection = (direction == DragDirection.left && right) ||
(direction == DragDirection.right && !right);
if (!wrongDirection && angleInterval.contains(angle)) {
return direction;
}
}
}
return null;
}
@Override
public boolean isSuppressOnClickEvent() {
return true;
}
public interface DragProcessor {
boolean processDragEvent(@NonNull DragDirection dragDirection, @NonNull DragButton dragButton, @NonNull PointF startPoint2d, @NonNull MotionEvent motionEvent);
}
}
\ No newline at end of file
package org.solovyev.android.io;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
abstract class BaseFileLoader {
@NonNull
protected final Context context;
public BaseFileLoader(@NonNull Context context) {
this.context = context;
}
static void close(@Nullable Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
Log.e("Closeable", e.getMessage(), e);
}
}
@Nullable
public CharSequence load() {
BufferedReader reader = null;
try {
final InputStream is = getInputStream();
if (is == null) {
return null;
}
reader = new BufferedReader(new InputStreamReader(is));
final StringBuilder result = new StringBuilder();
String line = reader.readLine();
while (line != null) {
result.append(line).append("\n");
line = reader.readLine();
}
return result;
} catch (IOException e) {
Log.e(getClass().getSimpleName(), e.getMessage(), e);
} finally {
close(reader);
}
return null;
}
@Nullable
protected abstract InputStream getInputStream() throws IOException;
}
package org.solovyev.android.io;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileLoader extends BaseFileLoader {
@NonNull
private final File file;
private FileLoader(@NonNull Context context, @NonNull File file) {
super(context);
this.file = file;
}
@NonNull
public static FileLoader create(@NonNull Context context, @NonNull File file) {
return new FileLoader(context, file);
}
@NonNull
public static FileLoader create(@NonNull Context context, @NonNull String dir, @NonNull String name) {
return new FileLoader(context, getFile(context, dir, name));
}
@NonNull
public static FileLoader create(@NonNull Context context, @NonNull String name) {
return new FileLoader(context, getFile(context, null, name));
}
@NonNull
private static File getFile(@NonNull Context context, @Nullable String dir, @NonNull String name) {
return new File(getDir(context, dir), name);
}
@NonNull
private static File getDir(@NonNull Context context, @Nullable String dir) {
if (dir != null) {
return context.getDir(dir, Context.MODE_PRIVATE);
}
return context.getFilesDir();
}
@Nullable
@Override
protected InputStream getInputStream() throws IOException {
if (!file.exists()) {
return null;
}
return new FileInputStream(file);
}
}
package org.solovyev.android.io;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
public class FileSaver implements Runnable {
@NonNull
private final File file;
@NonNull
private final CharSequence data;
private FileSaver(@NonNull File file, @NonNull CharSequence data) {
this.file = file;
this.data = data;
}
public static void save(@NonNull File file, @NonNull CharSequence data) {
final FileSaver fileSaver = new FileSaver(file, data);
fileSaver.save();
}
public void save() {
OutputStreamWriter out = null;
try {
out = new OutputStreamWriter(new FileOutputStream(file));
out.write(data.toString());
} catch (IOException e) {
Log.e("FileSaver", e.getMessage(), e);
} finally {
BaseFileLoader.close(out);
}
}
@Override
public void run() {
save();
}
}
package org.solovyev.android.plotter.app;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import org.solovyev.android.plotter.PlotFunction;
import org.solovyev.android.plotter.math.ExpressionFunction;
import org.solovyev.android.plotter.meshes.MeshSpec;
public class AddFunctionDialog extends FunctionDialog {
@NonNull
public static AddFunctionDialog create() {
return new AddFunctionDialog();
}
protected void applyData() {
final ExpressionFunction function = ExpressionFunction.createNamed(getName(), getBody(), "x", "y");
App.getPlotter().add(PlotFunction.create(function, applyMeshSpec()));
}
@NonNull
@Override
protected View onCreateDialogView(@NonNull Context context, @NonNull LayoutInflater inflater, Bundle savedInstanceState) {
final View view = super.onCreateDialogView(context, inflater, savedInstanceState);
if (savedInstanceState == null) {
colorPicker.setSelectedColorPosition(0);
lineWidthSeekBar.setProgress(MeshSpec.defaultWidth(getActivity()) - MeshSpec.MIN_WIDTH);
iconView.setMeshSpec(applyMeshSpec());
}
return view;
}
public static class ShowEvent {
}
}