mirror of https://github.com/NekoX-Dev/NekoX.git
1610 lines
59 KiB
Java
1610 lines
59 KiB
Java
package org.telegram.ui.Charts;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ValueAnimator;
|
|
import android.content.Context;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Color;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Path;
|
|
import android.graphics.PorterDuff;
|
|
import android.graphics.PorterDuffXfermode;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.text.TextPaint;
|
|
import android.view.HapticFeedbackConstants;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewConfiguration;
|
|
|
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
|
|
|
import org.telegram.messenger.AndroidUtilities;
|
|
import org.telegram.ui.ActionBar.Theme;
|
|
import org.telegram.ui.Charts.data.ChartData;
|
|
import org.telegram.ui.Charts.view_data.ChartBottomSignatureData;
|
|
import org.telegram.ui.Charts.view_data.ChartHeaderView;
|
|
import org.telegram.ui.Charts.view_data.ChartHorizontalLinesData;
|
|
import org.telegram.ui.Charts.view_data.LegendSignatureView;
|
|
import org.telegram.ui.Charts.view_data.LineViewData;
|
|
import org.telegram.ui.Charts.view_data.TransitionParams;
|
|
import org.telegram.ui.Components.CubicBezierInterpolator;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
|
|
import tw.nekomimi.nekogram.NekoConfig;
|
|
|
|
public abstract class BaseChartView<T extends ChartData, L extends LineViewData> extends View implements ChartPickerDelegate.Listener {
|
|
|
|
public SharedUiComponents sharedUiComponents;
|
|
ArrayList<ChartHorizontalLinesData> horizontalLines = new ArrayList<>(10);
|
|
ArrayList<ChartBottomSignatureData> bottomSignatureDate = new ArrayList<>(25);
|
|
|
|
public ArrayList<L> lines = new ArrayList<>();
|
|
|
|
private final int ANIM_DURATION = 400;
|
|
public final static float HORIZONTAL_PADDING = AndroidUtilities.dpf2(16f);
|
|
private final static float LINE_WIDTH = 1;
|
|
private final static float SELECTED_LINE_WIDTH = AndroidUtilities.dpf2(1.5f);
|
|
private final static float SIGNATURE_TEXT_SIZE = AndroidUtilities.dpf2(12f);
|
|
public final static int SIGNATURE_TEXT_HEIGHT = AndroidUtilities.dp(18f);
|
|
private final static int BOTTOM_SIGNATURE_TEXT_HEIGHT = AndroidUtilities.dp(14f);
|
|
public final static int BOTTOM_SIGNATURE_START_ALPHA = AndroidUtilities.dp(10f);
|
|
protected final static int PICKER_PADDING = AndroidUtilities.dp(16f);
|
|
private final static int PICKER_CAPTURE_WIDTH = AndroidUtilities.dp(24);
|
|
private final static int LANDSCAPE_END_PADDING = AndroidUtilities.dp(16);
|
|
private final static int BOTTOM_SIGNATURE_OFFSET = AndroidUtilities.dp(10);
|
|
|
|
private final static int DP_12 = AndroidUtilities.dp(12);
|
|
private final static int DP_6 = AndroidUtilities.dp(6);
|
|
private final static int DP_5 = AndroidUtilities.dp(5);
|
|
private final static int DP_2 = AndroidUtilities.dp(2);
|
|
private final static int DP_1 = AndroidUtilities.dp(1);
|
|
|
|
protected boolean drawPointOnSelection = true;
|
|
float signaturePaintAlpha;
|
|
float bottomSignaturePaintAlpha;
|
|
int hintLinePaintAlpha;
|
|
int chartActiveLineAlpha;
|
|
|
|
public final static boolean USE_LINES = android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.P;
|
|
protected final static boolean ANIMATE_PICKER_SIZES = android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP;
|
|
public static FastOutSlowInInterpolator INTERPOLATOR = new FastOutSlowInInterpolator();
|
|
|
|
int chartBottom;
|
|
public float currentMaxHeight = 250;
|
|
public float currentMinHeight = 0;
|
|
|
|
float animateToMaxHeight = 0;
|
|
float animateToMinHeight = 0;
|
|
|
|
|
|
float thresholdMaxHeight = 0;
|
|
|
|
int startXIndex;
|
|
int endXIndex;
|
|
boolean invalidatePickerChart = true;
|
|
|
|
boolean landscape = false;
|
|
|
|
public boolean enabled = true;
|
|
|
|
|
|
Paint emptyPaint = new Paint();
|
|
|
|
Paint linePaint = new Paint();
|
|
Paint selectedLinePaint = new Paint();
|
|
Paint signaturePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
|
|
Paint signaturePaint2 = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
|
|
Paint bottomSignaturePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
|
|
Paint pickerSelectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
Paint unactiveBottomChartPaint = new Paint();
|
|
Paint selectionBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
Paint ripplePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
Paint whiteLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
|
|
Rect pickerRect = new Rect();
|
|
Path pathTmp = new Path();
|
|
|
|
Animator maxValueAnimator;
|
|
|
|
ValueAnimator alphaAnimator;
|
|
ValueAnimator alphaBottomAnimator;
|
|
Animator pickerAnimator;
|
|
ValueAnimator selectionAnimator;
|
|
boolean postTransition = false;
|
|
|
|
public ChartPickerDelegate pickerDelegate = new ChartPickerDelegate(this);
|
|
T chartData;
|
|
|
|
ChartBottomSignatureData currentBottomSignatures;
|
|
protected float pickerMaxHeight;
|
|
protected float pickerMinHeight;
|
|
protected float animatedToPickerMaxHeight;
|
|
protected float animatedToPickerMinHeight;
|
|
protected int tmpN;
|
|
protected int tmpI;
|
|
protected int bottomSignatureOffset;
|
|
|
|
private Bitmap bottomChartBitmap;
|
|
private Canvas bottomChartCanvas;
|
|
|
|
protected boolean chartCaptured = false;
|
|
protected int selectedIndex = -1;
|
|
protected float selectedCoordinate = -1;
|
|
|
|
public LegendSignatureView legendSignatureView;
|
|
public boolean legendShowing = false;
|
|
|
|
public float selectionA = 0f;
|
|
|
|
boolean superDraw = false;
|
|
boolean useAlphaSignature = false;
|
|
|
|
public int transitionMode = TRANSITION_MODE_NONE;
|
|
public TransitionParams transitionParams;
|
|
|
|
public final static int TRANSITION_MODE_CHILD = 1;
|
|
public final static int TRANSITION_MODE_PARENT = 2;
|
|
public final static int TRANSITION_MODE_ALPHA_ENTER = 3;
|
|
public final static int TRANSITION_MODE_NONE = 0;
|
|
|
|
private final int touchSlop;
|
|
|
|
public int pikerHeight = AndroidUtilities.dp(46);
|
|
public float pickerWidth;
|
|
public float chartStart;
|
|
public float chartEnd;
|
|
public float chartWidth;
|
|
public float chartFullWidth;
|
|
public RectF chartArea = new RectF();
|
|
|
|
private ValueAnimator.AnimatorUpdateListener pickerHeightUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
pickerMaxHeight = (float) animation.getAnimatedValue();
|
|
invalidatePickerChart = true;
|
|
invalidate();
|
|
}
|
|
};
|
|
|
|
private ValueAnimator.AnimatorUpdateListener pickerMinHeightUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
pickerMinHeight = (float) animation.getAnimatedValue();
|
|
invalidatePickerChart = true;
|
|
invalidate();
|
|
}
|
|
};
|
|
|
|
private ValueAnimator.AnimatorUpdateListener heightUpdateListener = animation -> {
|
|
currentMaxHeight = ((float) animation.getAnimatedValue());
|
|
invalidate();
|
|
};
|
|
|
|
private ValueAnimator.AnimatorUpdateListener minHeightUpdateListener = animation -> {
|
|
currentMinHeight = ((float) animation.getAnimatedValue());
|
|
invalidate();
|
|
};
|
|
private ValueAnimator.AnimatorUpdateListener selectionAnimatorListener = new ValueAnimator.AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
selectionA = (float) animation.getAnimatedValue();
|
|
legendSignatureView.setAlpha(selectionA);
|
|
invalidate();
|
|
}
|
|
};
|
|
private Animator.AnimatorListener selectorAnimatorEndListener = new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
super.onAnimationEnd(animation);
|
|
if (!animateLegentTo) {
|
|
legendShowing = false;
|
|
legendSignatureView.setVisibility(GONE);
|
|
invalidate();
|
|
}
|
|
|
|
postTransition = false;
|
|
|
|
}
|
|
};
|
|
protected boolean useMinHeight = false;
|
|
protected DateSelectionListener dateSelectionListener;
|
|
private float startFromMax;
|
|
private float startFromMin;
|
|
private float startFromMaxH;
|
|
private float startFromMinH;
|
|
private float minMaxUpdateStep;
|
|
|
|
public BaseChartView(Context context) {
|
|
super(context);
|
|
init();
|
|
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
|
}
|
|
|
|
protected void init() {
|
|
linePaint.setStrokeWidth(LINE_WIDTH);
|
|
selectedLinePaint.setStrokeWidth(SELECTED_LINE_WIDTH);
|
|
|
|
signaturePaint.setTextSize(SIGNATURE_TEXT_SIZE);
|
|
signaturePaint2.setTextSize(SIGNATURE_TEXT_SIZE);
|
|
signaturePaint2.setTextAlign(Paint.Align.RIGHT);
|
|
bottomSignaturePaint.setTextSize(SIGNATURE_TEXT_SIZE);
|
|
bottomSignaturePaint.setTextAlign(Paint.Align.CENTER);
|
|
|
|
selectionBackgroundPaint.setStrokeWidth(AndroidUtilities.dpf2(6f));
|
|
selectionBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
|
|
|
|
setLayerType(LAYER_TYPE_HARDWARE, null);
|
|
setWillNotDraw(false);
|
|
|
|
legendSignatureView = createLegendView();
|
|
|
|
|
|
legendSignatureView.setVisibility(GONE);
|
|
|
|
whiteLinePaint.setColor(Color.WHITE);
|
|
whiteLinePaint.setStrokeWidth(AndroidUtilities.dpf2(3));
|
|
whiteLinePaint.setStrokeCap(Paint.Cap.ROUND);
|
|
|
|
updateColors();
|
|
}
|
|
|
|
protected LegendSignatureView createLegendView() {
|
|
return new LegendSignatureView(getContext());
|
|
}
|
|
|
|
public void updateColors() {
|
|
if (useAlphaSignature) {
|
|
signaturePaint.setColor(Theme.getColor(Theme.key_statisticChartSignatureAlpha));
|
|
} else {
|
|
signaturePaint.setColor(Theme.getColor(Theme.key_statisticChartSignature));
|
|
}
|
|
|
|
bottomSignaturePaint.setColor(Theme.getColor(Theme.key_statisticChartSignature));
|
|
linePaint.setColor(Theme.getColor(Theme.key_statisticChartHintLine));
|
|
selectedLinePaint.setColor(Theme.getColor(Theme.key_statisticChartActiveLine));
|
|
pickerSelectorPaint.setColor(Theme.getColor(Theme.key_statisticChartActivePickerChart));
|
|
unactiveBottomChartPaint.setColor(Theme.getColor(Theme.key_statisticChartInactivePickerChart));
|
|
selectionBackgroundPaint.setColor(Theme.getColor(Theme.key_windowBackgroundWhite));
|
|
ripplePaint.setColor(Theme.getColor(Theme.key_statisticChartRipple));
|
|
legendSignatureView.recolor();
|
|
|
|
hintLinePaintAlpha = linePaint.getAlpha();
|
|
chartActiveLineAlpha = selectedLinePaint.getAlpha();
|
|
signaturePaintAlpha = signaturePaint.getAlpha() / 255f;
|
|
bottomSignaturePaintAlpha = bottomSignaturePaint.getAlpha() / 255f;
|
|
|
|
|
|
for (LineViewData l : lines) {
|
|
l.updateColors();
|
|
}
|
|
|
|
if (legendShowing && selectedIndex < chartData.x.length) {
|
|
legendSignatureView.setData(selectedIndex, chartData.x[selectedIndex], (ArrayList<LineViewData>) lines, false);
|
|
}
|
|
|
|
invalidatePickerChart = true;
|
|
}
|
|
|
|
int lastW = 0;
|
|
int lastH = 0;
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
if (!landscape) {
|
|
setMeasuredDimension(
|
|
MeasureSpec.getSize(widthMeasureSpec),
|
|
MeasureSpec.getSize(widthMeasureSpec)
|
|
);
|
|
} else {
|
|
setMeasuredDimension(
|
|
MeasureSpec.getSize(widthMeasureSpec),
|
|
AndroidUtilities.displaySize.y - AndroidUtilities.dp(56)
|
|
);
|
|
}
|
|
|
|
|
|
if (getMeasuredWidth() != lastW || getMeasuredHeight() != lastH) {
|
|
lastW = getMeasuredWidth();
|
|
lastH = getMeasuredHeight();
|
|
bottomChartBitmap = Bitmap.createBitmap((int) (getMeasuredWidth() - (HORIZONTAL_PADDING * 2f)), pikerHeight, Bitmap.Config.ARGB_4444);
|
|
bottomChartCanvas = new Canvas(bottomChartBitmap);
|
|
|
|
sharedUiComponents.getPickerMaskBitmap(pikerHeight, (int) (getMeasuredWidth() - HORIZONTAL_PADDING * 2));
|
|
measureSizes();
|
|
|
|
if (legendShowing)
|
|
moveLegend(chartFullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING);
|
|
|
|
onPickerDataChanged(false, true, false);
|
|
}
|
|
}
|
|
|
|
|
|
private void measureSizes() {
|
|
if (getMeasuredHeight() <= 0 || getMeasuredWidth() <= 0) {
|
|
return;
|
|
}
|
|
pickerWidth = getMeasuredWidth() - (HORIZONTAL_PADDING * 2);
|
|
chartStart = HORIZONTAL_PADDING;
|
|
chartEnd = getMeasuredWidth() - (landscape ? LANDSCAPE_END_PADDING : HORIZONTAL_PADDING);
|
|
chartWidth = chartEnd - chartStart;
|
|
chartFullWidth = (chartWidth / (pickerDelegate.pickerEnd - pickerDelegate.pickerStart));
|
|
|
|
updateLineSignature();
|
|
chartBottom = AndroidUtilities.dp(100f);
|
|
chartArea.set(chartStart - HORIZONTAL_PADDING, 0, chartEnd + HORIZONTAL_PADDING, getMeasuredHeight() - chartBottom);
|
|
|
|
if (chartData != null) {
|
|
bottomSignatureOffset = (int) (AndroidUtilities.dp(20) / ((float) pickerWidth / chartData.x.length));
|
|
}
|
|
measureHeightThreshold();
|
|
}
|
|
|
|
private void measureHeightThreshold() {
|
|
int chartHeight = getMeasuredHeight() - chartBottom;
|
|
if (animateToMaxHeight == 0 || chartHeight == 0) return;
|
|
thresholdMaxHeight = ((float) animateToMaxHeight / chartHeight) * SIGNATURE_TEXT_SIZE;
|
|
}
|
|
|
|
|
|
protected void drawPickerChart(Canvas canvas) {
|
|
|
|
}
|
|
|
|
|
|
@Override
|
|
protected void onDraw(Canvas canvas) {
|
|
if (superDraw) {
|
|
super.onDraw(canvas);
|
|
return;
|
|
}
|
|
tick();
|
|
int count = canvas.save();
|
|
canvas.clipRect(0, chartArea.top, getMeasuredWidth(), chartArea.bottom);
|
|
|
|
drawBottomLine(canvas);
|
|
tmpN = horizontalLines.size();
|
|
for (tmpI = 0; tmpI < tmpN; tmpI++) {
|
|
drawHorizontalLines(canvas, horizontalLines.get(tmpI));
|
|
}
|
|
|
|
drawChart(canvas);
|
|
|
|
for (tmpI = 0; tmpI < tmpN; tmpI++) {
|
|
drawSignaturesToHorizontalLines(canvas, horizontalLines.get(tmpI));
|
|
}
|
|
|
|
canvas.restoreToCount(count);
|
|
drawBottomSignature(canvas);
|
|
|
|
drawPicker(canvas);
|
|
drawSelection(canvas);
|
|
|
|
super.onDraw(canvas);
|
|
}
|
|
|
|
protected void tick() {
|
|
if (minMaxUpdateStep == 0) {
|
|
return;
|
|
}
|
|
if (currentMaxHeight != animateToMaxHeight) {
|
|
startFromMax += minMaxUpdateStep;
|
|
if (startFromMax > 1) {
|
|
startFromMax = 1;
|
|
currentMaxHeight = animateToMaxHeight;
|
|
} else {
|
|
currentMaxHeight = startFromMaxH + (animateToMaxHeight - startFromMaxH) * CubicBezierInterpolator.EASE_OUT.getInterpolation(startFromMax);
|
|
}
|
|
invalidate();
|
|
}
|
|
if (useMinHeight) {
|
|
if (currentMinHeight != animateToMinHeight) {
|
|
startFromMin += minMaxUpdateStep;
|
|
if (startFromMin > 1) {
|
|
startFromMin = 1;
|
|
currentMinHeight = animateToMinHeight;
|
|
} else {
|
|
currentMinHeight = startFromMinH + (animateToMinHeight - startFromMinH) * CubicBezierInterpolator.EASE_OUT.getInterpolation(startFromMin);
|
|
}
|
|
invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void drawBottomSignature(Canvas canvas) {
|
|
if (chartData == null) return;
|
|
|
|
tmpN = bottomSignatureDate.size();
|
|
|
|
float transitionAlpha = 1f;
|
|
if (transitionMode == TRANSITION_MODE_PARENT) {
|
|
transitionAlpha = 1f - transitionParams.progress;
|
|
} else if (transitionMode == TRANSITION_MODE_CHILD) {
|
|
transitionAlpha = transitionParams.progress;
|
|
} else if (transitionMode == TRANSITION_MODE_ALPHA_ENTER) {
|
|
transitionAlpha = transitionParams.progress;
|
|
}
|
|
|
|
for (tmpI = 0; tmpI < tmpN; tmpI++) {
|
|
int resultAlpha = bottomSignatureDate.get(tmpI).alpha;
|
|
int step = bottomSignatureDate.get(tmpI).step;
|
|
if (step == 0) step = 1;
|
|
|
|
int start = startXIndex - bottomSignatureOffset;
|
|
while (start % step != 0) {
|
|
start--;
|
|
}
|
|
|
|
int end = endXIndex - bottomSignatureOffset;
|
|
while (end % step != 0 || end < chartData.x.length - 1) {
|
|
end++;
|
|
}
|
|
|
|
start += bottomSignatureOffset;
|
|
end += bottomSignatureOffset;
|
|
|
|
|
|
float offset = chartFullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING;
|
|
|
|
for (int i = start; i < end; i += step) {
|
|
if (i < 0 || i >= chartData.x.length - 1) continue;
|
|
float xPercentage = (float) (chartData.x[i] - chartData.x[0]) /
|
|
(float) ((chartData.x[chartData.x.length - 1] - chartData.x[0]));
|
|
float xPoint = xPercentage * chartFullWidth - offset;
|
|
float xPointOffset = xPoint - BOTTOM_SIGNATURE_OFFSET;
|
|
if (xPointOffset > 0 &&
|
|
xPointOffset <= chartWidth + HORIZONTAL_PADDING) {
|
|
if (xPointOffset < BOTTOM_SIGNATURE_START_ALPHA) {
|
|
float a = 1f - (BOTTOM_SIGNATURE_START_ALPHA - xPointOffset) / BOTTOM_SIGNATURE_START_ALPHA;
|
|
bottomSignaturePaint.setAlpha((int) (resultAlpha * a * bottomSignaturePaintAlpha * transitionAlpha));
|
|
} else if (xPointOffset > chartWidth) {
|
|
float a = 1f - (xPointOffset - chartWidth) / HORIZONTAL_PADDING;
|
|
bottomSignaturePaint.setAlpha((int) (resultAlpha * a * bottomSignaturePaintAlpha * transitionAlpha));
|
|
} else {
|
|
bottomSignaturePaint.setAlpha((int) (resultAlpha * bottomSignaturePaintAlpha * transitionAlpha));
|
|
}
|
|
canvas.drawText(chartData.getDayString(i), xPoint, getMeasuredHeight() - chartBottom + BOTTOM_SIGNATURE_TEXT_HEIGHT + AndroidUtilities.dp(3), bottomSignaturePaint);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void drawBottomLine(Canvas canvas) {
|
|
if (chartData == null) {
|
|
return;
|
|
}
|
|
float transitionAlpha = 1f;
|
|
if (transitionMode == TRANSITION_MODE_PARENT) {
|
|
transitionAlpha = 1f - transitionParams.progress;
|
|
} else if (transitionMode == TRANSITION_MODE_CHILD) {
|
|
transitionAlpha = transitionParams.progress;
|
|
} else if (transitionMode == TRANSITION_MODE_ALPHA_ENTER) {
|
|
transitionAlpha = transitionParams.progress;
|
|
}
|
|
|
|
linePaint.setAlpha((int) (hintLinePaintAlpha * transitionAlpha));
|
|
signaturePaint.setAlpha((int) (255 * signaturePaintAlpha * transitionAlpha));
|
|
int textOffset = (int) (SIGNATURE_TEXT_HEIGHT - signaturePaint.getTextSize());
|
|
int y = (getMeasuredHeight() - chartBottom - 1);
|
|
canvas.drawLine(
|
|
chartStart,
|
|
y,
|
|
chartEnd,
|
|
y,
|
|
linePaint);
|
|
if (useMinHeight) return;
|
|
|
|
canvas.drawText("0", HORIZONTAL_PADDING, y - textOffset, signaturePaint);
|
|
}
|
|
|
|
protected void drawSelection(Canvas canvas) {
|
|
if (selectedIndex < 0 || !legendShowing || chartData == null) return;
|
|
|
|
int alpha = (int) (chartActiveLineAlpha * selectionA);
|
|
|
|
|
|
float fullWidth = (chartWidth / (pickerDelegate.pickerEnd - pickerDelegate.pickerStart));
|
|
float offset = fullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING;
|
|
|
|
float xPoint;
|
|
if (selectedIndex < chartData.xPercentage.length) {
|
|
xPoint = chartData.xPercentage[selectedIndex] * fullWidth - offset;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
selectedLinePaint.setAlpha(alpha);
|
|
canvas.drawLine(xPoint, 0, xPoint, chartArea.bottom, selectedLinePaint);
|
|
|
|
if (drawPointOnSelection) {
|
|
tmpN = lines.size();
|
|
for (tmpI = 0; tmpI < tmpN; tmpI++) {
|
|
LineViewData line = lines.get(tmpI);
|
|
if (!line.enabled && line.alpha == 0) continue;
|
|
float yPercentage = (line.line.y[selectedIndex] - currentMinHeight) / (currentMaxHeight - currentMinHeight);
|
|
float yPoint = getMeasuredHeight() - chartBottom - (yPercentage) * (getMeasuredHeight() - chartBottom - SIGNATURE_TEXT_HEIGHT);
|
|
|
|
line.selectionPaint.setAlpha((int) (255 * line.alpha * selectionA));
|
|
selectionBackgroundPaint.setAlpha((int) (255 * line.alpha * selectionA));
|
|
|
|
canvas.drawPoint(xPoint, yPoint, line.selectionPaint);
|
|
canvas.drawPoint(xPoint, yPoint, selectionBackgroundPaint);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void drawChart(Canvas canvas) {
|
|
}
|
|
|
|
protected void drawHorizontalLines(Canvas canvas, ChartHorizontalLinesData a) {
|
|
int n = a.values.length;
|
|
|
|
float additionalOutAlpha = 1f;
|
|
if (n > 2) {
|
|
float v = (a.values[1] - a.values[0]) / (float) (currentMaxHeight - currentMinHeight);
|
|
if (v < 0.1) {
|
|
additionalOutAlpha = v / 0.1f;
|
|
}
|
|
}
|
|
|
|
float transitionAlpha = 1f;
|
|
if (transitionMode == TRANSITION_MODE_PARENT) {
|
|
transitionAlpha = 1f - transitionParams.progress;
|
|
} else if (transitionMode == TRANSITION_MODE_CHILD) {
|
|
transitionAlpha = transitionParams.progress;
|
|
} else if (transitionMode == TRANSITION_MODE_ALPHA_ENTER) {
|
|
transitionAlpha = transitionParams.progress;
|
|
}
|
|
linePaint.setAlpha((int) (a.alpha * (hintLinePaintAlpha / 255f) * transitionAlpha * additionalOutAlpha));
|
|
signaturePaint.setAlpha((int) (a.alpha * signaturePaintAlpha * transitionAlpha * additionalOutAlpha));
|
|
int chartHeight = getMeasuredHeight() - chartBottom - SIGNATURE_TEXT_HEIGHT;
|
|
for (int i = useMinHeight ? 0 : 1; i < n; i++) {
|
|
int y = (int) ((getMeasuredHeight() - chartBottom) - chartHeight * ((a.values[i] - currentMinHeight) / (currentMaxHeight - currentMinHeight)));
|
|
canvas.drawRect(chartStart, y, chartEnd, y + 1, linePaint);
|
|
}
|
|
}
|
|
|
|
protected void drawSignaturesToHorizontalLines(Canvas canvas, ChartHorizontalLinesData a) {
|
|
int n = a.values.length;
|
|
|
|
float additionalOutAlpha = 1f;
|
|
if (n > 2) {
|
|
float v = (a.values[1] - a.values[0]) / (float) (currentMaxHeight - currentMinHeight);
|
|
if (v < 0.1) {
|
|
additionalOutAlpha = v / 0.1f;
|
|
}
|
|
}
|
|
|
|
float transitionAlpha = 1f;
|
|
if (transitionMode == TRANSITION_MODE_PARENT) {
|
|
transitionAlpha = 1f - transitionParams.progress;
|
|
} else if (transitionMode == TRANSITION_MODE_CHILD) {
|
|
transitionAlpha = transitionParams.progress;
|
|
} else if (transitionMode == TRANSITION_MODE_ALPHA_ENTER) {
|
|
transitionAlpha = transitionParams.progress;
|
|
}
|
|
linePaint.setAlpha((int) (a.alpha * (hintLinePaintAlpha / 255f) * transitionAlpha * additionalOutAlpha));
|
|
signaturePaint.setAlpha((int) (a.alpha * signaturePaintAlpha * transitionAlpha * additionalOutAlpha));
|
|
int chartHeight = getMeasuredHeight() - chartBottom - SIGNATURE_TEXT_HEIGHT;
|
|
|
|
int textOffset = (int) (SIGNATURE_TEXT_HEIGHT - signaturePaint.getTextSize());
|
|
for (int i = useMinHeight ? 0 : 1; i < n; i++) {
|
|
int y = (int) ((getMeasuredHeight() - chartBottom) - chartHeight * ((a.values[i] - currentMinHeight) / (currentMaxHeight - currentMinHeight)));
|
|
canvas.drawText(a.valuesStr[i], HORIZONTAL_PADDING, y - textOffset, signaturePaint);
|
|
}
|
|
}
|
|
|
|
void drawPicker(Canvas canvas) {
|
|
if (chartData == null) {
|
|
return;
|
|
}
|
|
pickerDelegate.pickerWidth = pickerWidth;
|
|
int bottom = getMeasuredHeight() - PICKER_PADDING;
|
|
int top = getMeasuredHeight() - pikerHeight - PICKER_PADDING;
|
|
|
|
int start = (int) (HORIZONTAL_PADDING + pickerWidth * pickerDelegate.pickerStart);
|
|
int end = (int) (HORIZONTAL_PADDING + pickerWidth * pickerDelegate.pickerEnd);
|
|
|
|
float transitionAlpha = 1f;
|
|
if (transitionMode == TRANSITION_MODE_CHILD) {
|
|
int startParent = (int) (HORIZONTAL_PADDING + pickerWidth * transitionParams.pickerStartOut);
|
|
int endParent = (int) (HORIZONTAL_PADDING + pickerWidth * transitionParams.pickerEndOut);
|
|
|
|
start += (startParent - start) * (1f - transitionParams.progress);
|
|
end += (endParent - end) * (1f - transitionParams.progress);
|
|
} else if (transitionMode == TRANSITION_MODE_ALPHA_ENTER) {
|
|
transitionAlpha = transitionParams.progress;
|
|
}
|
|
|
|
if (chartData != null) {
|
|
boolean instantDraw = false;
|
|
if (transitionMode == TRANSITION_MODE_NONE) {
|
|
for (int i = 0; i < lines.size(); i++) {
|
|
L l = lines.get(i);
|
|
if ((l.animatorIn != null && l.animatorIn.isRunning()) || (l.animatorOut != null && l.animatorOut.isRunning())) {
|
|
instantDraw = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (instantDraw) {
|
|
canvas.save();
|
|
canvas.clipRect(
|
|
HORIZONTAL_PADDING, getMeasuredHeight() - PICKER_PADDING - pikerHeight,
|
|
getMeasuredWidth() - HORIZONTAL_PADDING, getMeasuredHeight() - PICKER_PADDING
|
|
);
|
|
canvas.translate(HORIZONTAL_PADDING, getMeasuredHeight() - PICKER_PADDING - pikerHeight);
|
|
drawPickerChart(canvas);
|
|
canvas.restore();
|
|
} else if (invalidatePickerChart) {
|
|
bottomChartBitmap.eraseColor(0);
|
|
drawPickerChart(bottomChartCanvas);
|
|
invalidatePickerChart = false;
|
|
}
|
|
if (!instantDraw) {
|
|
if (transitionMode == TRANSITION_MODE_PARENT) {
|
|
|
|
float pY = top + (bottom - top) >> 1;
|
|
float pX = HORIZONTAL_PADDING + pickerWidth * transitionParams.xPercentage;
|
|
|
|
emptyPaint.setAlpha((int) ((1f - transitionParams.progress) * 255));
|
|
|
|
canvas.save();
|
|
canvas.clipRect(HORIZONTAL_PADDING, top, getMeasuredWidth() - HORIZONTAL_PADDING, bottom);
|
|
canvas.scale(1 + 2 * transitionParams.progress, 1f, pX, pY);
|
|
canvas.drawBitmap(bottomChartBitmap, HORIZONTAL_PADDING, getMeasuredHeight() - PICKER_PADDING - pikerHeight, emptyPaint);
|
|
canvas.restore();
|
|
|
|
|
|
} else if (transitionMode == TRANSITION_MODE_CHILD) {
|
|
float pY = top + (bottom - top) >> 1;
|
|
float pX = HORIZONTAL_PADDING + pickerWidth * transitionParams.xPercentage;
|
|
|
|
float dX = (transitionParams.xPercentage > 0.5f ? pickerWidth * transitionParams.xPercentage : pickerWidth * (1f - transitionParams.xPercentage)) * transitionParams.progress;
|
|
|
|
canvas.save();
|
|
canvas.clipRect(pX - dX, top, pX + dX, bottom);
|
|
|
|
emptyPaint.setAlpha((int) (transitionParams.progress * 255));
|
|
canvas.scale(transitionParams.progress, 1f, pX, pY);
|
|
canvas.drawBitmap(bottomChartBitmap, HORIZONTAL_PADDING, getMeasuredHeight() - PICKER_PADDING - pikerHeight, emptyPaint);
|
|
canvas.restore();
|
|
|
|
} else {
|
|
emptyPaint.setAlpha((int) ((transitionAlpha) * 255));
|
|
canvas.drawBitmap(bottomChartBitmap, HORIZONTAL_PADDING, getMeasuredHeight() - PICKER_PADDING - pikerHeight, emptyPaint);
|
|
}
|
|
}
|
|
|
|
|
|
if (transitionMode == TRANSITION_MODE_PARENT) {
|
|
return;
|
|
}
|
|
|
|
canvas.drawRect(HORIZONTAL_PADDING,
|
|
top,
|
|
start + DP_12,
|
|
bottom, unactiveBottomChartPaint);
|
|
|
|
canvas.drawRect(end - DP_12,
|
|
top,
|
|
getMeasuredWidth() - HORIZONTAL_PADDING,
|
|
bottom, unactiveBottomChartPaint);
|
|
} else {
|
|
canvas.drawRect(HORIZONTAL_PADDING,
|
|
top,
|
|
getMeasuredWidth() - HORIZONTAL_PADDING,
|
|
bottom, unactiveBottomChartPaint);
|
|
}
|
|
|
|
canvas.drawBitmap(
|
|
sharedUiComponents.getPickerMaskBitmap(pikerHeight, (int) (getMeasuredWidth() - HORIZONTAL_PADDING * 2)),
|
|
HORIZONTAL_PADDING, getMeasuredHeight() - PICKER_PADDING - pikerHeight, emptyPaint);
|
|
|
|
if (chartData != null) {
|
|
pickerRect.set(start,
|
|
top,
|
|
end,
|
|
bottom);
|
|
|
|
|
|
pickerDelegate.middlePickerArea.set(pickerRect);
|
|
|
|
|
|
canvas.drawPath(RoundedRect(pathTmp, pickerRect.left,
|
|
pickerRect.top - DP_1,
|
|
pickerRect.left + DP_12,
|
|
pickerRect.bottom + DP_1, DP_6, DP_6,
|
|
true, false, false, true), pickerSelectorPaint);
|
|
|
|
|
|
canvas.drawPath(RoundedRect(pathTmp, pickerRect.right - DP_12,
|
|
pickerRect.top - DP_1, pickerRect.right,
|
|
pickerRect.bottom + DP_1, DP_6, DP_6,
|
|
false, true, true, false), pickerSelectorPaint);
|
|
|
|
canvas.drawRect(pickerRect.left + DP_12,
|
|
pickerRect.bottom, pickerRect.right - DP_12,
|
|
pickerRect.bottom + DP_1, pickerSelectorPaint);
|
|
|
|
canvas.drawRect(pickerRect.left + DP_12,
|
|
pickerRect.top - DP_1, pickerRect.right - DP_12,
|
|
pickerRect.top, pickerSelectorPaint);
|
|
|
|
|
|
canvas.drawLine(pickerRect.left + DP_6, pickerRect.centerY() - DP_6,
|
|
pickerRect.left + DP_6, pickerRect.centerY() + DP_6, whiteLinePaint);
|
|
|
|
canvas.drawLine(pickerRect.right - DP_6, pickerRect.centerY() - DP_6,
|
|
pickerRect.right - DP_6, pickerRect.centerY() + DP_6, whiteLinePaint);
|
|
|
|
|
|
ChartPickerDelegate.CapturesData middleCap = pickerDelegate.getMiddleCaptured();
|
|
|
|
int r = ((pickerRect.bottom - pickerRect.top) >> 1);
|
|
int cY = pickerRect.top + r;
|
|
|
|
if (middleCap != null) {
|
|
// canvas.drawCircle(pickerRect.left + ((pickerRect.right - pickerRect.left) >> 1), cY, r * middleCap.aValue + HORIZONTAL_PADDING, ripplePaint);
|
|
} else {
|
|
ChartPickerDelegate.CapturesData lCap = pickerDelegate.getLeftCaptured();
|
|
ChartPickerDelegate.CapturesData rCap = pickerDelegate.getRightCaptured();
|
|
|
|
if (lCap != null)
|
|
canvas.drawCircle(pickerRect.left + DP_5, cY, r * lCap.aValue - DP_2, ripplePaint);
|
|
if (rCap != null)
|
|
canvas.drawCircle(pickerRect.right - DP_5, cY, r * rCap.aValue - DP_2, ripplePaint);
|
|
}
|
|
|
|
int cX = start;
|
|
pickerDelegate.leftPickerArea.set(
|
|
cX - PICKER_CAPTURE_WIDTH,
|
|
top,
|
|
cX + (PICKER_CAPTURE_WIDTH >> 1),
|
|
bottom
|
|
);
|
|
|
|
cX = end;
|
|
pickerDelegate.rightPickerArea.set(
|
|
cX - (PICKER_CAPTURE_WIDTH >> 1),
|
|
top,
|
|
cX + PICKER_CAPTURE_WIDTH,
|
|
bottom
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
long lastTime = 0;
|
|
|
|
private void setMaxMinValue(int newMaxHeight, int newMinHeight, boolean animated) {
|
|
setMaxMinValue(newMaxHeight, newMinHeight, animated, false, false);
|
|
}
|
|
|
|
protected void setMaxMinValue(int newMaxHeight, int newMinHeight, boolean animated, boolean force, boolean useAnimator) {
|
|
boolean heightChanged = true;
|
|
if ((Math.abs(ChartHorizontalLinesData.lookupHeight(newMaxHeight) - animateToMaxHeight) < thresholdMaxHeight) || newMaxHeight == 0) {
|
|
heightChanged = false;
|
|
}
|
|
|
|
if (!heightChanged && newMaxHeight == animateToMinHeight) return;
|
|
final ChartHorizontalLinesData newData = createHorizontalLinesData(newMaxHeight, newMinHeight);
|
|
newMaxHeight = newData.values[newData.values.length - 1];
|
|
newMinHeight = newData.values[0];
|
|
|
|
|
|
if (!useAnimator) {
|
|
float k = (currentMaxHeight - currentMinHeight) / (newMaxHeight - newMinHeight);
|
|
if (k > 1f) {
|
|
k = (newMaxHeight - newMinHeight) / (currentMaxHeight - currentMinHeight);
|
|
}
|
|
float s = 0.045f;
|
|
if (k > 0.7) {
|
|
s = 0.1f;
|
|
} else if (k < 0.1) {
|
|
s = 0.03f;
|
|
}
|
|
|
|
boolean update = false;
|
|
if (newMaxHeight != animateToMaxHeight) {
|
|
update = true;
|
|
}
|
|
if (useMinHeight && newMinHeight != animateToMinHeight) {
|
|
update = true;
|
|
}
|
|
if (update) {
|
|
if (maxValueAnimator != null) {
|
|
maxValueAnimator.removeAllListeners();
|
|
maxValueAnimator.cancel();
|
|
}
|
|
startFromMaxH = currentMaxHeight;
|
|
startFromMinH = currentMinHeight;
|
|
startFromMax = 0;
|
|
startFromMin = 0;
|
|
minMaxUpdateStep = s;
|
|
}
|
|
}
|
|
|
|
animateToMaxHeight = newMaxHeight;
|
|
animateToMinHeight = newMinHeight;
|
|
measureHeightThreshold();
|
|
|
|
long t = System.currentTimeMillis();
|
|
// debounce
|
|
if (t - lastTime < 320 && !force) {
|
|
return;
|
|
}
|
|
lastTime = t;
|
|
|
|
if (alphaAnimator != null) {
|
|
alphaAnimator.removeAllListeners();
|
|
alphaAnimator.cancel();
|
|
}
|
|
|
|
if (!animated) {
|
|
currentMaxHeight = newMaxHeight;
|
|
currentMinHeight = newMinHeight;
|
|
horizontalLines.clear();
|
|
horizontalLines.add(newData);
|
|
newData.alpha = 255;
|
|
return;
|
|
}
|
|
|
|
|
|
horizontalLines.add(newData);
|
|
|
|
if (useAnimator) {
|
|
if (maxValueAnimator != null) {
|
|
maxValueAnimator.removeAllListeners();
|
|
maxValueAnimator.cancel();
|
|
}
|
|
minMaxUpdateStep = 0;
|
|
|
|
AnimatorSet animatorSet = new AnimatorSet();
|
|
animatorSet.playTogether(createAnimator(currentMaxHeight, newMaxHeight, heightUpdateListener));
|
|
|
|
if (useMinHeight) {
|
|
animatorSet.playTogether(createAnimator(currentMinHeight, newMinHeight, minHeightUpdateListener));
|
|
}
|
|
|
|
maxValueAnimator = animatorSet;
|
|
maxValueAnimator.start();
|
|
}
|
|
|
|
int n = horizontalLines.size();
|
|
for (int i = 0; i < n; i++) {
|
|
ChartHorizontalLinesData a = horizontalLines.get(i);
|
|
if (a != newData) a.fixedAlpha = a.alpha;
|
|
}
|
|
|
|
alphaAnimator = createAnimator(0, 255, animation -> {
|
|
newData.alpha = (int) ((float) animation.getAnimatedValue());
|
|
for (ChartHorizontalLinesData a : horizontalLines) {
|
|
if (a != newData)
|
|
a.alpha = (int) ((a.fixedAlpha / 255f) * (255 - newData.alpha));
|
|
}
|
|
invalidate();
|
|
});
|
|
alphaAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
horizontalLines.clear();
|
|
horizontalLines.add(newData);
|
|
}
|
|
});
|
|
|
|
alphaAnimator.start();
|
|
}
|
|
|
|
protected ChartHorizontalLinesData createHorizontalLinesData(int newMaxHeight, int newMinHeight) {
|
|
return new ChartHorizontalLinesData(newMaxHeight, newMinHeight, useMinHeight);
|
|
}
|
|
|
|
ValueAnimator createAnimator(float f1, float f2, ValueAnimator.AnimatorUpdateListener l) {
|
|
ValueAnimator a = ValueAnimator.ofFloat(f1, f2);
|
|
a.setDuration(ANIM_DURATION);
|
|
a.setInterpolator(INTERPOLATOR);
|
|
a.addUpdateListener(l);
|
|
return a;
|
|
}
|
|
|
|
int lastX;
|
|
int lastY;
|
|
int capturedX;
|
|
int capturedY;
|
|
long capturedTime;
|
|
protected boolean canCaptureChartSelection;
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
if (chartData == null) {
|
|
return false;
|
|
}
|
|
if (!enabled) {
|
|
pickerDelegate.uncapture(event, event.getActionIndex());
|
|
getParent().requestDisallowInterceptTouchEvent(false);
|
|
chartCaptured = false;
|
|
return false;
|
|
}
|
|
|
|
|
|
int x = (int) event.getX(event.getActionIndex());
|
|
int y = (int) event.getY(event.getActionIndex());
|
|
|
|
switch (event.getActionMasked()) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
capturedTime = System.currentTimeMillis();
|
|
getParent().requestDisallowInterceptTouchEvent(true);
|
|
boolean captured = pickerDelegate.capture(x, y, event.getActionIndex());
|
|
if (captured) {
|
|
return true;
|
|
}
|
|
|
|
capturedX = lastX = x;
|
|
capturedY = lastY = y;
|
|
|
|
if (chartArea.contains(x, y)) {
|
|
if (selectedIndex < 0 || !animateLegentTo) {
|
|
chartCaptured = true;
|
|
selectXOnChart(x, y);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
case MotionEvent.ACTION_POINTER_DOWN:
|
|
return pickerDelegate.capture(x, y, event.getActionIndex());
|
|
case MotionEvent.ACTION_MOVE:
|
|
int dx = x - lastX;
|
|
int dy = y - lastY;
|
|
|
|
if (pickerDelegate.captured()) {
|
|
boolean rez = pickerDelegate.move(x, y, event.getActionIndex());
|
|
if (event.getPointerCount() > 1) {
|
|
x = (int) event.getX(1);
|
|
y = (int) event.getY(1);
|
|
pickerDelegate.move(x, y, 1);
|
|
}
|
|
|
|
getParent().requestDisallowInterceptTouchEvent(rez);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (chartCaptured) {
|
|
boolean disable;
|
|
if (canCaptureChartSelection && System.currentTimeMillis() - capturedTime > 200) {
|
|
disable = true;
|
|
} else {
|
|
disable = Math.abs(dx) > Math.abs(dy) || Math.abs(dy) < touchSlop;
|
|
}
|
|
lastX = x;
|
|
lastY = y;
|
|
|
|
getParent().requestDisallowInterceptTouchEvent(disable);
|
|
selectXOnChart(x, y);
|
|
} else if (chartArea.contains(capturedX, capturedY)){
|
|
int dxCaptured = capturedX - x;
|
|
int dyCaptured = capturedY - y;
|
|
if (Math.sqrt(dxCaptured * dxCaptured + dyCaptured * dyCaptured) > touchSlop || System.currentTimeMillis() - capturedTime > 200) {
|
|
chartCaptured = true;
|
|
selectXOnChart(x, y);
|
|
}
|
|
}
|
|
return true;
|
|
case MotionEvent.ACTION_POINTER_UP:
|
|
pickerDelegate.uncapture(event, event.getActionIndex());
|
|
return true;
|
|
case MotionEvent.ACTION_CANCEL:
|
|
case MotionEvent.ACTION_UP:
|
|
if (pickerDelegate.uncapture(event, event.getActionIndex())) {
|
|
return true;
|
|
}
|
|
if (chartArea.contains(capturedX, capturedY) && !chartCaptured) {
|
|
animateLegend(false);
|
|
}
|
|
pickerDelegate.uncapture();
|
|
updateLineSignature();
|
|
getParent().requestDisallowInterceptTouchEvent(false);
|
|
chartCaptured = false;
|
|
onActionUp();
|
|
invalidate();
|
|
int min = 0;
|
|
if (useMinHeight) min = findMinValue(startXIndex, endXIndex);
|
|
setMaxMinValue(findMaxValue(startXIndex, endXIndex), min, true, true, false);
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected void onActionUp() {
|
|
|
|
}
|
|
|
|
protected void selectXOnChart(int x, int y) {
|
|
int oldSelectedX = selectedIndex;
|
|
if (chartData == null) return;
|
|
float offset = chartFullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING;
|
|
float xP = (offset + x) / chartFullWidth;
|
|
selectedCoordinate = xP;
|
|
if (xP < 0) {
|
|
selectedIndex = 0;
|
|
selectedCoordinate = 0f;
|
|
} else if (xP > 1) {
|
|
selectedIndex = chartData.x.length - 1;
|
|
selectedCoordinate = 1f;
|
|
} else {
|
|
selectedIndex = chartData.findIndex(startXIndex, endXIndex, xP);
|
|
if (selectedIndex + 1 < chartData.xPercentage.length) {
|
|
float dx = Math.abs(chartData.xPercentage[selectedIndex] - xP);
|
|
float dx2 = Math.abs(chartData.xPercentage[selectedIndex + 1] - xP);
|
|
if (dx2 < dx) {
|
|
selectedIndex++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selectedIndex > endXIndex) selectedIndex = endXIndex;
|
|
if (selectedIndex < startXIndex) selectedIndex = startXIndex;
|
|
|
|
legendShowing = true;
|
|
animateLegend(true);
|
|
moveLegend(offset);
|
|
if (dateSelectionListener != null) {
|
|
dateSelectionListener.onDateSelected(getSelectedDate());
|
|
}
|
|
invalidate();
|
|
}
|
|
|
|
public boolean animateLegentTo = false;
|
|
|
|
public void animateLegend(boolean show) {
|
|
moveLegend();
|
|
if (animateLegentTo == show) return;
|
|
animateLegentTo = show;
|
|
if (selectionAnimator != null) {
|
|
selectionAnimator.removeAllListeners();
|
|
selectionAnimator.cancel();
|
|
}
|
|
selectionAnimator = createAnimator(selectionA, show ? 1f : 0f, selectionAnimatorListener)
|
|
.setDuration(200);
|
|
|
|
selectionAnimator.addListener(selectorAnimatorEndListener);
|
|
|
|
|
|
selectionAnimator.start();
|
|
}
|
|
|
|
public void moveLegend(float offset) {
|
|
if (chartData == null || selectedIndex == -1 || !legendShowing) return;
|
|
legendSignatureView.setData(selectedIndex, chartData.x[selectedIndex], (ArrayList<LineViewData>) lines, false);
|
|
legendSignatureView.setVisibility(VISIBLE);
|
|
legendSignatureView.measure(
|
|
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST),
|
|
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST)
|
|
);
|
|
float lXPoint = chartData.xPercentage[selectedIndex] * chartFullWidth - offset;
|
|
if (lXPoint > (chartStart + chartWidth) / 2f) {
|
|
lXPoint -= (legendSignatureView.getWidth() + DP_5);
|
|
} else {
|
|
lXPoint += DP_5;
|
|
}
|
|
if (lXPoint < 0) {
|
|
lXPoint = 0;
|
|
} else if (lXPoint + legendSignatureView.getMeasuredWidth() > getMeasuredWidth()) {
|
|
lXPoint = getMeasuredWidth() - legendSignatureView.getMeasuredWidth();
|
|
}
|
|
legendSignatureView.setTranslationX(
|
|
lXPoint
|
|
);
|
|
}
|
|
|
|
public int findMaxValue(int startXIndex, int endXIndex) {
|
|
int linesSize = lines.size();
|
|
int maxValue = 0;
|
|
for (int j = 0; j < linesSize; j++) {
|
|
if (!lines.get(j).enabled) continue;
|
|
int lineMax = lines.get(j).line.segmentTree.rMaxQ(startXIndex, endXIndex);
|
|
if (lineMax > maxValue)
|
|
maxValue = lineMax;
|
|
}
|
|
return maxValue;
|
|
}
|
|
|
|
|
|
public int findMinValue(int startXIndex, int endXIndex) {
|
|
int linesSize = lines.size();
|
|
int minValue = Integer.MAX_VALUE;
|
|
for (int j = 0; j < linesSize; j++) {
|
|
if (!lines.get(j).enabled) continue;
|
|
int lineMin = lines.get(j).line.segmentTree.rMinQ(startXIndex, endXIndex);
|
|
if (lineMin < minValue)
|
|
minValue = lineMin;
|
|
}
|
|
return minValue;
|
|
}
|
|
|
|
public void setData(T chartData) {
|
|
if (this.chartData != chartData) {
|
|
invalidate();
|
|
lines.clear();
|
|
if (chartData != null && chartData.lines != null) {
|
|
for (int i = 0; i < chartData.lines.size(); i++) {
|
|
lines.add(createLineViewData(chartData.lines.get(i)));
|
|
}
|
|
}
|
|
clearSelection();
|
|
this.chartData = chartData;
|
|
if (chartData != null) {
|
|
if (chartData.x[0] == 0) {
|
|
pickerDelegate.pickerStart = 0f;
|
|
pickerDelegate.pickerEnd = 1f;
|
|
} else {
|
|
pickerDelegate.minDistance = getMinDistance();
|
|
if (pickerDelegate.pickerEnd - pickerDelegate.pickerStart < pickerDelegate.minDistance) {
|
|
pickerDelegate.pickerStart = pickerDelegate.pickerEnd - pickerDelegate.minDistance;
|
|
if (pickerDelegate.pickerStart < 0) {
|
|
pickerDelegate.pickerStart = 0f;
|
|
pickerDelegate.pickerEnd = 1f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
measureSizes();
|
|
|
|
if (chartData != null) {
|
|
updateIndexes();
|
|
int min = useMinHeight ? findMinValue(startXIndex, endXIndex) : 0;
|
|
setMaxMinValue(findMaxValue(startXIndex, endXIndex), min, false);
|
|
pickerMaxHeight = 0;
|
|
pickerMinHeight = Integer.MAX_VALUE;
|
|
initPickerMaxHeight();
|
|
legendSignatureView.setSize(lines.size());
|
|
|
|
invalidatePickerChart = true;
|
|
updateLineSignature();
|
|
} else {
|
|
|
|
pickerDelegate.pickerStart = 0.7f;
|
|
pickerDelegate.pickerEnd = 1f;
|
|
|
|
pickerMaxHeight = pickerMinHeight = 0;
|
|
horizontalLines.clear();
|
|
|
|
if (maxValueAnimator != null) {
|
|
maxValueAnimator.cancel();
|
|
}
|
|
|
|
if (alphaAnimator != null) {
|
|
alphaAnimator.removeAllListeners();
|
|
alphaAnimator.cancel();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected float getMinDistance() {
|
|
if (chartData == null) {
|
|
return 0.1f;
|
|
}
|
|
|
|
int n = chartData.x.length;
|
|
if (n < 5) {
|
|
return 1f;
|
|
}
|
|
float r = 5f / n;
|
|
if (r < 0.1f) {
|
|
return 0.1f;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
protected void initPickerMaxHeight() {
|
|
for (LineViewData l : lines) {
|
|
if (l.enabled && l.line.maxValue > pickerMaxHeight) pickerMaxHeight = l.line.maxValue;
|
|
if (l.enabled && l.line.minValue < pickerMinHeight) pickerMinHeight = l.line.minValue;
|
|
if (pickerMaxHeight == pickerMinHeight) {
|
|
pickerMaxHeight++;
|
|
pickerMinHeight--;
|
|
}
|
|
}
|
|
}
|
|
|
|
public abstract L createLineViewData(ChartData.Line line);
|
|
|
|
public void onPickerDataChanged() {
|
|
onPickerDataChanged(true, false, false);
|
|
}
|
|
|
|
public void onPickerDataChanged(boolean animated, boolean force, boolean useAniamtor) {
|
|
if (chartData == null) return;
|
|
chartFullWidth = (chartWidth / (pickerDelegate.pickerEnd - pickerDelegate.pickerStart));
|
|
|
|
updateIndexes();
|
|
int min = useMinHeight ? findMinValue(startXIndex, endXIndex) : 0;
|
|
setMaxMinValue(findMaxValue(startXIndex, endXIndex), min, animated, force, useAniamtor);
|
|
|
|
if (legendShowing && !force) {
|
|
animateLegend(false);
|
|
moveLegend(chartFullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING);
|
|
}
|
|
invalidate();
|
|
}
|
|
|
|
public void onPickerJumpTo(float start, float end, boolean force) {
|
|
if (chartData == null) return;
|
|
if (force) {
|
|
int startXIndex = chartData.findStartIndex(Math.max(
|
|
start, 0f
|
|
));
|
|
int endXIndex = chartData.findEndIndex(startXIndex, Math.min(
|
|
end, 1f
|
|
));
|
|
setMaxMinValue(findMaxValue(startXIndex, endXIndex), findMinValue(startXIndex, endXIndex), true, true, false);
|
|
animateLegend(false);
|
|
} else {
|
|
updateIndexes();
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
protected void updateIndexes() {
|
|
if (chartData == null) return;
|
|
startXIndex = chartData.findStartIndex(Math.max(
|
|
pickerDelegate.pickerStart, 0f
|
|
));
|
|
endXIndex = chartData.findEndIndex(startXIndex, Math.min(
|
|
pickerDelegate.pickerEnd, 1f
|
|
));
|
|
if (endXIndex < startXIndex) {
|
|
endXIndex = startXIndex;
|
|
}
|
|
if (chartHeaderView != null) {
|
|
chartHeaderView.setDates(chartData.x[startXIndex], chartData.x[endXIndex]);
|
|
}
|
|
updateLineSignature();
|
|
}
|
|
|
|
private final static int BOTTOM_SIGNATURE_COUNT = 6;
|
|
|
|
private void updateLineSignature() {
|
|
if (chartData == null || chartWidth == 0) return;
|
|
float d = chartFullWidth * chartData.oneDayPercentage;
|
|
|
|
float k = chartWidth / d;
|
|
int step = (int) (k / BOTTOM_SIGNATURE_COUNT);
|
|
updateDates(step);
|
|
}
|
|
|
|
|
|
private void updateDates(int step) {
|
|
if (currentBottomSignatures == null || step >= currentBottomSignatures.stepMax || step <= currentBottomSignatures.stepMin) {
|
|
step = Integer.highestOneBit(step) << 1;
|
|
if (currentBottomSignatures != null && currentBottomSignatures.step == step) {
|
|
return;
|
|
}
|
|
|
|
if (alphaBottomAnimator != null) {
|
|
alphaBottomAnimator.removeAllListeners();
|
|
alphaBottomAnimator.cancel();
|
|
}
|
|
|
|
int stepMax = (int) (step + step * 0.2);
|
|
int stepMin = (int) (step - step * 0.2);
|
|
|
|
|
|
final ChartBottomSignatureData data = new ChartBottomSignatureData(step, stepMax, stepMin);
|
|
data.alpha = 255;
|
|
|
|
if (currentBottomSignatures == null) {
|
|
currentBottomSignatures = data;
|
|
data.alpha = 255;
|
|
bottomSignatureDate.add(data);
|
|
return;
|
|
}
|
|
|
|
currentBottomSignatures = data;
|
|
|
|
|
|
tmpN = bottomSignatureDate.size();
|
|
for (int i = 0; i < tmpN; i++) {
|
|
ChartBottomSignatureData a = bottomSignatureDate.get(i);
|
|
a.fixedAlpha = a.alpha;
|
|
}
|
|
|
|
bottomSignatureDate.add(data);
|
|
if (bottomSignatureDate.size() > 2) {
|
|
bottomSignatureDate.remove(0);
|
|
}
|
|
|
|
alphaBottomAnimator = createAnimator(0f, 1f, animation -> {
|
|
float alpha = (float) animation.getAnimatedValue();
|
|
for (ChartBottomSignatureData a : bottomSignatureDate) {
|
|
if (a == data) {
|
|
data.alpha = (int) (255 * alpha);
|
|
} else {
|
|
a.alpha = (int) ((1f - alpha) * (a.fixedAlpha));
|
|
}
|
|
}
|
|
invalidate();
|
|
}).setDuration(200);
|
|
alphaBottomAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
super.onAnimationEnd(animation);
|
|
bottomSignatureDate.clear();
|
|
bottomSignatureDate.add(data);
|
|
}
|
|
});
|
|
|
|
alphaBottomAnimator.start();
|
|
}
|
|
}
|
|
|
|
public void onCheckChanged() {
|
|
onPickerDataChanged(true, true, true);
|
|
tmpN = lines.size();
|
|
for (tmpI = 0; tmpI < tmpN; tmpI++) {
|
|
final LineViewData lineViewData = lines.get(tmpI);
|
|
|
|
if (lineViewData.enabled && lineViewData.animatorOut != null) {
|
|
lineViewData.animatorOut.cancel();
|
|
}
|
|
|
|
if (!lineViewData.enabled && lineViewData.animatorIn != null) {
|
|
lineViewData.animatorIn.cancel();
|
|
}
|
|
|
|
if (lineViewData.enabled && lineViewData.alpha != 1f) {
|
|
if (lineViewData.animatorIn != null && lineViewData.animatorIn.isRunning()) {
|
|
continue;
|
|
}
|
|
lineViewData.animatorIn = createAnimator(lineViewData.alpha, 1f, animation -> {
|
|
lineViewData.alpha = ((float) animation.getAnimatedValue());
|
|
invalidatePickerChart = true;
|
|
invalidate();
|
|
});
|
|
lineViewData.animatorIn.start();
|
|
}
|
|
|
|
if (!lineViewData.enabled && lineViewData.alpha != 0) {
|
|
if (lineViewData.animatorOut != null && lineViewData.animatorOut.isRunning()) {
|
|
continue;
|
|
}
|
|
lineViewData.animatorOut = createAnimator(lineViewData.alpha, 0f, animation -> {
|
|
lineViewData.alpha = ((float) animation.getAnimatedValue());
|
|
invalidatePickerChart = true;
|
|
invalidate();
|
|
});
|
|
lineViewData.animatorOut.start();
|
|
}
|
|
}
|
|
|
|
updatePickerMinMaxHeight();
|
|
if (legendShowing)
|
|
legendSignatureView.setData(selectedIndex, chartData.x[selectedIndex], (ArrayList<LineViewData>) lines, true);
|
|
}
|
|
|
|
protected void updatePickerMinMaxHeight() {
|
|
if (!ANIMATE_PICKER_SIZES) return;
|
|
int max = 0;
|
|
int min = Integer.MAX_VALUE;
|
|
for (LineViewData l : lines) {
|
|
if (l.enabled && l.line.maxValue > max) max = l.line.maxValue;
|
|
if (l.enabled && l.line.minValue < min) min = l.line.minValue;
|
|
}
|
|
|
|
if ((min != Integer.MAX_VALUE && min != animatedToPickerMinHeight) || (max > 0 && max != animatedToPickerMaxHeight)) {
|
|
animatedToPickerMaxHeight = max;
|
|
if (pickerAnimator != null) pickerAnimator.cancel();
|
|
AnimatorSet animatorSet = new AnimatorSet();
|
|
animatorSet.playTogether(
|
|
createAnimator(pickerMaxHeight, animatedToPickerMaxHeight, pickerHeightUpdateListener),
|
|
createAnimator(pickerMinHeight, animatedToPickerMinHeight, pickerMinHeightUpdateListener)
|
|
);
|
|
pickerAnimator = animatorSet;
|
|
pickerAnimator.start();
|
|
}
|
|
}
|
|
|
|
public void setLandscape(boolean b) {
|
|
landscape = b;
|
|
}
|
|
|
|
public void saveState(Bundle outState) {
|
|
if (outState == null) return;
|
|
|
|
outState.putFloat("chart_start", pickerDelegate.pickerStart);
|
|
outState.putFloat("chart_end", pickerDelegate.pickerEnd);
|
|
|
|
|
|
if (lines != null) {
|
|
int n = lines.size();
|
|
boolean[] bArray = new boolean[n];
|
|
for (int i = 0; i < n; i++) {
|
|
bArray[i] = lines.get(i).enabled;
|
|
}
|
|
outState.putBooleanArray("chart_line_enabled", bArray);
|
|
|
|
}
|
|
}
|
|
|
|
ChartHeaderView chartHeaderView;
|
|
|
|
public void setHeader(ChartHeaderView chartHeaderView) {
|
|
this.chartHeaderView = chartHeaderView;
|
|
}
|
|
|
|
public long getSelectedDate() {
|
|
if (selectedIndex < 0) {
|
|
return -1;
|
|
}
|
|
return chartData.x[selectedIndex];
|
|
}
|
|
|
|
public void clearSelection() {
|
|
selectedIndex = -1;
|
|
legendShowing = false;
|
|
animateLegentTo = false;
|
|
legendSignatureView.setVisibility(GONE);
|
|
selectionA = 0f;
|
|
}
|
|
|
|
public void selectDate(long activeZoom) {
|
|
selectedIndex = Arrays.binarySearch(chartData.x, activeZoom);
|
|
legendShowing = true;
|
|
legendSignatureView.setVisibility(VISIBLE);
|
|
selectionA = 1f;
|
|
moveLegend(chartFullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING);
|
|
if (!NekoConfig.disableVibration) {
|
|
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
|
|
}
|
|
}
|
|
|
|
public long getStartDate() {
|
|
return chartData.x[startXIndex];
|
|
}
|
|
|
|
public long getEndDate() {
|
|
return chartData.x[endXIndex];
|
|
}
|
|
|
|
public void updatePicker(ChartData chartData, long d) {
|
|
int n = chartData.x.length;
|
|
long startOfDay = d - d % 86400000L;
|
|
long endOfDay = startOfDay + 86400000L - 1;
|
|
int startIndex = 0;
|
|
int endIndex = 0;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
if (startOfDay > chartData.x[i]) startIndex = i;
|
|
if (endOfDay > chartData.x[i]) endIndex = i;
|
|
}
|
|
pickerDelegate.pickerStart = chartData.xPercentage[startIndex];
|
|
pickerDelegate.pickerEnd = chartData.xPercentage[endIndex];
|
|
}
|
|
|
|
public void moveLegend() {
|
|
moveLegend(chartFullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING);
|
|
}
|
|
|
|
@Override
|
|
public void requestLayout() {
|
|
super.requestLayout();
|
|
}
|
|
|
|
public static Path RoundedRect(
|
|
Path path,
|
|
float left, float top, float right, float bottom, float rx, float ry,
|
|
boolean tl, boolean tr, boolean br, boolean bl
|
|
) {
|
|
path.reset();
|
|
if (rx < 0) rx = 0;
|
|
if (ry < 0) ry = 0;
|
|
float width = right - left;
|
|
float height = bottom - top;
|
|
if (rx > width / 2) rx = width / 2;
|
|
if (ry > height / 2) ry = height / 2;
|
|
float widthMinusCorners = (width - (2 * rx));
|
|
float heightMinusCorners = (height - (2 * ry));
|
|
|
|
path.moveTo(right, top + ry);
|
|
if (tr)
|
|
path.rQuadTo(0, -ry, -rx, -ry);
|
|
else {
|
|
path.rLineTo(0, -ry);
|
|
path.rLineTo(-rx, 0);
|
|
}
|
|
path.rLineTo(-widthMinusCorners, 0);
|
|
if (tl)
|
|
path.rQuadTo(-rx, 0, -rx, ry);
|
|
else {
|
|
path.rLineTo(-rx, 0);
|
|
path.rLineTo(0, ry);
|
|
}
|
|
path.rLineTo(0, heightMinusCorners);
|
|
|
|
if (bl)
|
|
path.rQuadTo(0, ry, rx, ry);
|
|
else {
|
|
path.rLineTo(0, ry);
|
|
path.rLineTo(rx, 0);
|
|
}
|
|
|
|
path.rLineTo(widthMinusCorners, 0);
|
|
if (br)
|
|
path.rQuadTo(rx, 0, rx, -ry);
|
|
else {
|
|
path.rLineTo(rx, 0);
|
|
path.rLineTo(0, -ry);
|
|
}
|
|
|
|
path.rLineTo(0, -heightMinusCorners);
|
|
|
|
path.close();
|
|
return path;
|
|
}
|
|
|
|
public void setDateSelectionListener(DateSelectionListener dateSelectionListener) {
|
|
this.dateSelectionListener = dateSelectionListener;
|
|
}
|
|
|
|
public interface DateSelectionListener {
|
|
void onDateSelected(long date);
|
|
}
|
|
|
|
public static class SharedUiComponents {
|
|
|
|
private Bitmap pickerRoundBitmap;
|
|
private Canvas canvas;
|
|
|
|
|
|
private RectF rectF = new RectF();
|
|
private Paint xRefP = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
|
|
public SharedUiComponents() {
|
|
xRefP.setColor(0);
|
|
xRefP.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
|
}
|
|
|
|
int k = 0;
|
|
private boolean invalidate = true;
|
|
|
|
Bitmap getPickerMaskBitmap(int h, int w) {
|
|
if (h + w << 10 != k || invalidate) {
|
|
invalidate = false;
|
|
k = h + w << 10;
|
|
pickerRoundBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
|
canvas = new Canvas(pickerRoundBitmap);
|
|
|
|
rectF.set(0, 0, w, h);
|
|
canvas.drawColor(Theme.getColor(Theme.key_windowBackgroundWhite));
|
|
canvas.drawRoundRect(rectF, AndroidUtilities.dp(4), AndroidUtilities.dp(4), xRefP);
|
|
}
|
|
|
|
|
|
return pickerRoundBitmap;
|
|
}
|
|
|
|
public void invalidate(){
|
|
invalidate = true;
|
|
}
|
|
}
|
|
|
|
public void fillTransitionParams(TransitionParams params) {
|
|
|
|
}
|
|
}
|