From dee73d3de27871daca751941341725f38be5de6f Mon Sep 17 00:00:00 2001 From: Me7c7 Date: Thu, 21 Aug 2025 12:57:37 +0300 Subject: [PATCH] Charts: Simple heart rate overlay to sleep chart added --- .../charts/AbstractActivityChartFragment.java | 36 +++ .../activities/charts/SleepDailyFragment.java | 42 +++- .../sleep/SimpleSleepDetailsHROverlay.java | 209 ++++++++++++++++++ .../charts/sleep/SleepDetailsOverlay.java | 28 +++ .../charts/{ => sleep}/SleepDetailsView.java | 209 ++++++++++-------- .../main/res/layout/fragment_sleepchart.xml | 30 ++- app/src/main/res/layout/layout_chart_chip.xml | 9 + 7 files changed, 459 insertions(+), 104 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SimpleSleepDetailsHROverlay.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SleepDetailsOverlay.java rename app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/{ => sleep}/SleepDetailsView.java (79%) create mode 100644 app/src/main/res/layout/layout_chart_chip.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractActivityChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractActivityChartFragment.java index a8aaba3c34..03ed1adad6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractActivityChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractActivityChartFragment.java @@ -39,6 +39,8 @@ import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.sleep.SimpleSleepDetailsHROverlay; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.sleep.SleepDetailsView; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; @@ -343,6 +345,40 @@ public abstract class AbstractActivityChartFragment extend return result; } + public int[] prepareHR(List samples) { + if (samples.isEmpty()) { + return null; + } + + //NOTE: The start and end times of the samples should correspond to the sleep stages. + // The time interval between samples is not important (it can be in minutes, seconds, etc.), but all intervals must be filled. + // If there is a gap in the raw data, 'SimpleSleepDetailsHROverlay.NO_DATA' should be used. + // As we exactly know the intervals, we can use primitive types (which have better performance during drawing). + // Samples already prepared in minute's interval can simply be copied with filtering. + + TimestampTranslation tsTranslation = new TimestampTranslation(); + HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance(); + + int[] result = new int[samples.size()]; + int idx = 0; + int lastTsShorten = 0; + for (ActivitySample sample : samples) { + if (sample.getKind() == ActivityKind.NOT_WORN || !heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) { + result[idx++] = -1; + continue; + } + int tsShorten = tsTranslation.shorten(sample.getTimestamp()); + if (lastTsShorten == 0 || (tsShorten - lastTsShorten) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { + result[idx++] = sample.getHeartRate(); + } else { + result[idx++] = SimpleSleepDetailsHROverlay.NO_DATA; + } + lastTsShorten = tsShorten; + } + + return result; + } + protected int getIndexOfActivity(ActivityKind kind) { return switch (kind) { case DEEP_SLEEP -> 0; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepDailyFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepDailyFragment.java index 723e288509..ef4641fcc5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepDailyFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepDailyFragment.java @@ -27,11 +27,12 @@ import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ScrollView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import com.github.mikephil.charting.animation.Easing; @@ -39,6 +40,8 @@ import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.components.*; import com.github.mikephil.charting.data.LineData; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; import org.apache.commons.lang3.tuple.Triple; import org.slf4j.Logger; @@ -57,6 +60,8 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.activities.charts.SleepAnalysis.SleepSession; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.sleep.SimpleSleepDetailsHROverlay; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.sleep.SleepDetailsView; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.GaugeDrawer; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -138,7 +143,9 @@ public class SleepDailyFragment extends SleepFragment intensityData = calculateIntensityData(samples); List stages = prepareStages(samples); - return new MyChartsData(mySleepChartsData, chartsData, hrData.getLeft(), hrData.getMiddle(), hrData.getRight(), intensityData.getLeft(), intensityData.getMiddle(), intensityData.getRight(), stages); + + int[] hr = hrOverlay?prepareHR(samples):null; + return new MyChartsData(mySleepChartsData, chartsData, hrData.getLeft(), hrData.getMiddle(), hrData.getRight(), intensityData.getLeft(), intensityData.getMiddle(), intensityData.getRight(), stages, hr); } private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List samples, List sleepScoreSamples) { @@ -237,6 +244,10 @@ public class SleepDailyFragment extends SleepFragment stages; - public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData, float heartRateAverage, int heartRateAxisMin, int heartRateAxisMax, float intensityTotal, float intensityAxisMin, float intensityAxisMax, List stages) { + private final int[] hr; + + public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData, float heartRateAverage, int heartRateAxisMin, int heartRateAxisMax, float intensityTotal, float intensityAxisMin, float intensityAxisMax, List stages, int[] hr) { this.pieData = pieData; this.chartsData = chartsData; this.heartRateAverage = heartRateAverage; @@ -561,6 +592,7 @@ public class SleepDailyFragment extends SleepFragment getStages() { return stages; } + + public int[] getHr() { + return hr; + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SimpleSleepDetailsHROverlay.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SimpleSleepDetailsHROverlay.java new file mode 100644 index 0000000000..e6a3ea5f34 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SimpleSleepDetailsHROverlay.java @@ -0,0 +1,209 @@ +/* Copyright (C) 2025 Me7c7 + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.charts.sleep; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; + +import java.util.Locale; + +public class SimpleSleepDetailsHROverlay implements SleepDetailsOverlay { + + public static int NO_DATA = Integer.MIN_VALUE; + + private final int[] data; + + private final int average; + + private final int yMin; + private final int yMax; + private final int yDelta; + + private final Path linePath = new Path(); + private final Paint linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final Paint averagePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final Paint dotPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint hudPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint hudBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final Paint labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Rect labelRect = new Rect(); + + private final Paint scalePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + public SimpleSleepDetailsHROverlay(int yMin, int yMax, int[] data, int average, int mainColor, int hubBackgroundColor, int averageColor) { + linePaint.setStyle(Paint.Style.STROKE); + linePaint.setStrokeWidth(4); + linePaint.setColor(mainColor); + + dotPaint.setStyle(Paint.Style.FILL); + dotPaint.setColor(mainColor); + + hudPaint.setStyle(Paint.Style.STROKE); + hudPaint.setStrokeWidth(2); + hudPaint.setColor(mainColor); + + hudBackgroundPaint.setStyle(Paint.Style.FILL); + hudBackgroundPaint.setColor(hubBackgroundColor); + + labelPaint.setColor(Color.GRAY); + labelPaint.setTextAlign(Paint.Align.CENTER); + labelPaint.setTextSize(25); + + scalePaint.setColor(Color.GRAY); + scalePaint.setTextAlign(Paint.Align.RIGHT); + scalePaint.setTextSize(25); + + averagePaint.setColor(averageColor); + averagePaint.setStrokeWidth(2); + averagePaint.setStrokeCap(Paint.Cap.ROUND); + averagePaint.setStyle(Paint.Style.STROKE); + averagePaint.setPathEffect(new DashPathEffect(new float[]{15f, 5f}, 0f)); + + this.data = data; + this.yMax = yMax; + this.yMin = yMin; + + this.average = average; + + this.yDelta = this.yMax - this.yMin; + } + + private float getAdjustedValue(float val) { + if(val < yMin) { + return 0; + } + if(val > yMax) { + return yDelta; + } + return val - yMin; + } + + @Override + public void drawOverlayScale(Canvas canvas, int lineSpacing, int horizontalLineCount, int chartTopStart, int x) { + + int yLabelsDelta = yDelta / horizontalLineCount; + int yStart = yMax; + for (int i = 0; i <= horizontalLineCount; i++) { + final float y = (i * lineSpacing); + final String currentValue = String.format(Locale.ROOT, "%d", yStart); + canvas.drawText(currentValue, x, chartTopStart + y, scalePaint); + yStart -= yLabelsDelta; + } + } + + @Override + public void drawOverlay(Canvas canvas, int left, int top, int height, int width) { + if (data == null || data.length == 0) + return; + + float unitHeight = ((float) height / yDelta); + float unitWidth = ((float) width / data.length); + linePath.reset(); + boolean first = true; + float prevX = 0; + float prevY = 0; + for (int i = 0; i < data.length; i++) { + if(data[i] == NO_DATA) { + if(!first) { + canvas.drawPath(linePath, linePaint); + linePath.reset(); + } + first = true; + } + if (data[i] < 0) { + continue; + } + float curX = left + i * unitWidth; + float curY = top + (height - (getAdjustedValue(data[i]) * unitHeight)); + if (first) { + linePath.moveTo(curX, curY); + first = false; + } else { + float conX1, conY1, conX2, conY2; + conX1 = (prevX + curX) / 2; + conY1 = prevY; + conX2 = (prevX + curX) / 2; + conY2 = curY; + linePath.cubicTo( + conX1, conY1, conX2, conY2, + curX, curY + ); + } + prevX = curX; + prevY = curY; + } + canvas.drawPath(linePath, linePaint); + if(average > 0) { + float avgY = top + (height - (getAdjustedValue(average) * unitHeight)); + canvas.drawLine(left, avgY, left + width, avgY, averagePaint); + } + } + + @Override + public void drawOverlaySelector(Canvas canvas, int left, int top, int height, int width, float selectorPos) { + final float unitWidth = ((float) width / data.length); + + int idx = (int) ((selectorPos - left) / unitWidth); + if (idx > 0 && idx < data.length && data[idx] > 0) { + float unitHeight = ((float) height / yDelta); + + final int verticalPadding = 12; + final int horizontalPadding = 16; + final float cornerRadius = 10F; + final float lineLength = 15F; + + final float dataValue = data[idx]; + + final float centerX = left + idx * unitWidth; + final float centerY = top + height - (getAdjustedValue(dataValue) * unitHeight); + + final String currentValue = String.format(Locale.ROOT, "%d", (int) dataValue); + + canvas.drawCircle(centerX, centerY, 7, dotPaint); + + labelPaint.getTextBounds(currentValue, 0, currentValue.length(), labelRect); + + float bgLeft = centerX - (labelRect.width() / 2f) - horizontalPadding; + float bgBottom = centerY - lineLength - verticalPadding; + float bgTop = bgBottom - labelRect.height() - 2 * verticalPadding; + float bgRight = centerX + (labelRect.width() / 2f) + horizontalPadding; + + if (bgLeft < left + verticalPadding) { + bgLeft = left + verticalPadding; + bgRight = bgLeft + labelRect.width() + 2 * horizontalPadding; + } + if (bgRight > left + width - verticalPadding) { + bgRight = left + width - verticalPadding; + bgLeft = bgRight - (labelRect.width() + 2 * horizontalPadding); + } + + float adjustedCenterX = (bgLeft + bgRight) / 2f; + + canvas.drawLine(centerX, centerY, adjustedCenterX, bgBottom, dotPaint); + canvas.drawRoundRect(bgLeft, bgTop, bgRight, bgBottom, cornerRadius, cornerRadius, hudBackgroundPaint); + canvas.drawRoundRect(bgLeft, bgTop, bgRight, bgBottom, cornerRadius, cornerRadius, hudPaint); + canvas.drawText(currentValue, adjustedCenterX, bgBottom - verticalPadding, labelPaint); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SleepDetailsOverlay.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SleepDetailsOverlay.java new file mode 100644 index 0000000000..c7637ebabc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SleepDetailsOverlay.java @@ -0,0 +1,28 @@ +/* Copyright (C) 2025 Me7c7 + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.charts.sleep; + +import android.graphics.Canvas; + +public interface SleepDetailsOverlay { + + void drawOverlayScale(Canvas canvas, int lineSpacing, int horizontalLineCount, int chartTopStart, int x); + + void drawOverlay(Canvas canvas, int left, int top, int height, int width); + + void drawOverlaySelector(Canvas canvas, int left, int top, int height, int width, float selectorPos); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepDetailsView.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SleepDetailsView.java similarity index 79% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepDetailsView.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SleepDetailsView.java index ef53b1591c..2573b73585 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepDetailsView.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/sleep/SleepDetailsView.java @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.activities.charts; +package nodomain.freeyourgadget.gadgetbridge.activities.charts.sleep; import android.content.Context; import android.graphics.Canvas; @@ -25,9 +25,7 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.Region; import android.graphics.Shader; -import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -71,6 +69,8 @@ public class SleepDetailsView extends View { private List sleepDetails = new ArrayList<>(); private int sleepMinutesCount = 0; + private SleepDetailsOverlay curOverlay = null; + private long startTimestamp; private long endTimestamp; @@ -91,12 +91,14 @@ public class SleepDetailsView extends View { private final Paint timeInfoTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint infoTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Path cornerPath = new Path(); - private final Path rectPath = new Path(); + private final Path detailBarPath = new Path(); private final RectF rectF = new RectF(); private final Rect timeInfoTextRect = new Rect(); private final Rect infoTextRect = new Rect(); + + private final int overlayAlpha = 128; + private final int infoTextSize = 25; private final float cornerRadius = 10.0F; @@ -180,6 +182,10 @@ public class SleepDetailsView extends View { canvas.drawLine(chartLeftStart, chartTopStart, chartLeftStart, chartTopStart + chartHeight, gridPaint); canvas.drawLine(chartLeftStart + chartWidth, chartTopStart, chartLeftStart + chartWidth, chartTopStart + chartHeight, gridPaint); + + if(curOverlay != null) { + curOverlay.drawOverlayScale(canvas, lineSpacing, horizontalLineCount, chartTopStart, chartLeftStart + chartWidth); + } } private int getPositionFromType(int type) { @@ -201,82 +207,6 @@ public class SleepDetailsView extends View { return config[idx].color; } - private enum Corner { - TOP_LEFT, - BOTTOM_LEFT, - TOP_RIGHT, - BOTTOM_RIGHT - } - - private void drawCorner(Canvas canvas, float tipX, float tipY, float left, float barWidth, int color, Corner corner) { - final float halfBarHeight = barHeight / 2f; - final float curveEdgeHeight = halfBarHeight + 3f; - final float halfWidth = barWidth / 2f; - - final boolean isLeft = (corner == Corner.TOP_LEFT || corner == Corner.BOTTOM_LEFT); - final boolean isTop = (corner == Corner.TOP_LEFT || corner == Corner.TOP_RIGHT); - final float horizontalOffset = isLeft ? -0.5f : 0.5f; - final float verticalOffset = isTop ? -curveEdgeHeight : curveEdgeHeight; - - final float secondX = tipX + horizontalOffset; - final float thirdX = tipX + (horizontalOffset < 0 ? halfWidth : -halfWidth); - - cornerPath.reset(); - cornerPath.moveTo(tipX, tipY); - cornerPath.lineTo(secondX, tipY + verticalOffset); - cornerPath.lineTo(thirdX, tipY + verticalOffset); - cornerPath.lineTo(thirdX, tipY); - cornerPath.close(); - - final float top = isTop ? tipY - barHeight : tipY + halfBarHeight; - final float bottom = top + halfBarHeight; - rectPath.reset(); - rectF.set(left, top, left + barWidth, bottom); - rectPath.addRoundRect(rectF, cornerRadius, cornerRadius, Path.Direction.CW); - - cornerPaint.setColor(color); - - canvas.save(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - canvas.clipOutPath(rectPath); - } else { - canvas.clipPath(rectPath, Region.Op.DIFFERENCE); - } - - canvas.drawPath(cornerPath, fillPaint); - canvas.restore(); - } - - private void drawEdges(Canvas canvas, List details, int currentIndex, float left, float top) { - if (currentIndex < 0 || currentIndex >= details.size()) return; - - SleepDetail currentData = details.get(currentIndex); - final int currentType = currentData.type; - final int currentIdx = getIndexByType(currentType); - if (currentIdx == -1) return; - - final float posY = top + (barHeight / 2f); - final int color = getColorFromType(currentType); - - final float currentUnitWidth = currentData.durationMinutes * unitWidth; - if (currentIndex > 0) { - final int prevIdx = getIndexByType(details.get(currentIndex - 1).type); - if (prevIdx != -1) { - Corner corner = (currentIdx > prevIdx) ? Corner.TOP_LEFT : Corner.BOTTOM_LEFT; - drawCorner(canvas, left, posY, left, currentUnitWidth, color, corner); - } - } - - if (currentIndex < details.size() - 1) { - final int nextIdx = getIndexByType(details.get(currentIndex + 1).type); - if (nextIdx != -1) { - final float rightX = left + currentUnitWidth; - Corner corner = (currentIdx > nextIdx) ? Corner.TOP_RIGHT : Corner.BOTTOM_RIGHT; - drawCorner(canvas, rightX, posY, left, currentUnitWidth, color, corner); - } - } - } - private void drawConnections(Canvas canvas, List details, int currentIndex, float left) { if (currentIndex <= 0 || currentIndex >= details.size()) { return; @@ -302,10 +232,96 @@ public class SleepDetailsView extends View { Shader shader = new LinearGradient(left, topY, left, bottomY, startColor, endColor, Shader.TileMode.MIRROR); connectionPaint.setShader(shader); + if (curOverlay != null) { + connectionPaint.setAlpha(overlayAlpha); + } canvas.drawLine(left, topY, left, bottomY, connectionPaint); connectionPaint.setShader(null); } + public static final int CORNER_TOP_LEFT = 1; + public static final int CORNER_TOP_RIGHT = 2; + public static final int CORNER_BOTTOM_LEFT = 4; + public static final int CORNER_BOTTOM_RIGHT = 8; + + public static boolean isFlagSet(int value, int flag) { + return (value & flag) == flag; + } + + public static int setFlag(int value, int flag) { + return value | flag; + } + + private void drawRoundedRectWithCorners(Canvas canvas, float left, float top, float width, float height, int corners) { + final float right = left + width; + final float bottom = top + height; + final float centerX = (left + right) / 2f; + + detailBarPath.reset(); + + if (left + cornerRadius < centerX) { + detailBarPath.moveTo(left + cornerRadius, top); + detailBarPath.lineTo(right - cornerRadius, top); + } else { + detailBarPath.moveTo(centerX, top); + } + + detailBarPath.quadTo(right, top, right, isFlagSet(corners, CORNER_TOP_RIGHT) ? top - cornerRadius : top + cornerRadius); + + detailBarPath.lineTo(right, isFlagSet(corners, CORNER_BOTTOM_RIGHT) ? bottom + cornerRadius : bottom - cornerRadius); + + if (left + cornerRadius < centerX) { + detailBarPath.quadTo(right, bottom, right - cornerRadius, bottom); + detailBarPath.lineTo(left + cornerRadius, bottom); + } else { + detailBarPath.quadTo(right, bottom, centerX, bottom); + } + + detailBarPath.quadTo(left, bottom, left, isFlagSet(corners, CORNER_BOTTOM_LEFT) ? bottom + cornerRadius : bottom - cornerRadius); + + detailBarPath.lineTo(left, isFlagSet(corners, CORNER_TOP_LEFT) ? top - cornerRadius : top + cornerRadius); + + if (left + cornerRadius < centerX) { + detailBarPath.quadTo(left, top, left + cornerRadius, top); + } else { + detailBarPath.quadTo(left, top, centerX, top); + } + + detailBarPath.close(); + + if (curOverlay != null) { + fillPaint.setAlpha(overlayAlpha); + } + canvas.drawPath(detailBarPath, fillPaint); + } + + + private int getEdgeFlags(List details, int currentIndex) { + int ret = 0; + + if (currentIndex < 0 || currentIndex >= details.size()) return ret; + + SleepDetail currentData = details.get(currentIndex); + final int currentType = currentData.type; + final int currentIdx = getIndexByType(currentType); + if (currentIdx == -1) return ret; + + if (currentIndex > 0) { + final int prevIdx = getIndexByType(details.get(currentIndex - 1).type); + if (prevIdx != -1) { + ret = setFlag(ret, (currentIdx > prevIdx) ? CORNER_TOP_LEFT : CORNER_BOTTOM_LEFT); + } + } + + if (currentIndex < details.size() - 1) { + final int nextIdx = getIndexByType(details.get(currentIndex + 1).type); + if (nextIdx != -1) { + ret = setFlag(ret, (currentIdx > nextIdx) ? CORNER_TOP_RIGHT : CORNER_BOTTOM_RIGHT); + } + } + return ret; + } + private void drawDetails(Canvas canvas) { if (sleepDetails.isEmpty()) return; @@ -318,10 +334,8 @@ public class SleepDetailsView extends View { final float currentUnitWidth = sd.durationMinutes * unitWidth; - rectF.set(x, top, x + currentUnitWidth, top + barHeight); - canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, fillPaint); - - drawEdges(canvas, sleepDetails, i, x, top); + int corners = getEdgeFlags(sleepDetails, i); + drawRoundedRectWithCorners(canvas, x, top, currentUnitWidth, barHeight, corners); drawConnections(canvas, sleepDetails, i, x); x += currentUnitWidth; @@ -371,12 +385,12 @@ public class SleepDetailsView extends View { final int iconSize = infoTextSize; - final float infoIconX = chartLeftStart + ((float) chartWidth /2) - (float) infoTextRect.width() / 2 - iconSize - 5; + final float infoIconX = chartLeftStart + ((float) chartWidth / 2) - (float) infoTextRect.width() / 2 - iconSize - 5; rectF.set(infoIconX, infoTextRect.height() - iconSize, infoIconX + iconSize, infoTextRect.height()); canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, fillPaint); - final float infoTextX = chartLeftStart + ((float) chartWidth /2) - (float) infoTextRect.width() / 2; + final float infoTextX = chartLeftStart + ((float) chartWidth / 2) - (float) infoTextRect.width() / 2; canvas.drawText(info, infoTextX, infoTextRect.height(), infoTextPaint); } @@ -405,6 +419,10 @@ public class SleepDetailsView extends View { drawGrid(canvas); drawDetails(canvas); + if (curOverlay != null) { + curOverlay.drawOverlay(canvas, chartLeftStart, chartTopStart, chartHeight, chartWidth); + } + //draw start/end Time Info final String endDate = formatDurationHoursMinutes(this.endTimestamp); float timeInfoY = chartTopStart + chartHeight + timeInfoHeight; @@ -414,6 +432,9 @@ public class SleepDetailsView extends View { //draw selector if (selectorPos > chartLeftStart && selectorPos < chartLeftStart + chartWidth) { canvas.drawLine(selectorPos, chartTopStart, selectorPos, chartTopStart + chartHeight, selectorPaint); + if (curOverlay != null) { + curOverlay.drawOverlaySelector(canvas, chartLeftStart, chartTopStart, chartHeight, chartWidth, selectorPos); + } if (selectorPos > (chartLeftStart + timeInfoTextRect.width()) && selectorPos < (chartLeftStart + chartWidth - timeInfoTextRect.width())) { final String selectedDate = formatDurationHoursMinutes(this.startTimestamp + (long) ((selectorPos - chartLeftStart) / unitWidth) * 60000L); canvas.drawText(selectedDate, selectorPos, timeInfoY, timeInfoTextPaint); @@ -429,6 +450,7 @@ public class SleepDetailsView extends View { } public void setData(List sleepDetails) { + curOverlay = null; this.sleepDetails = sleepDetails; this.sleepMinutesCount = 0; for (SleepDetail sd : sleepDetails) { @@ -443,6 +465,11 @@ public class SleepDetailsView extends View { invalidate(); } + public void setOverlay(SleepDetailsOverlay overlay) { + curOverlay = overlay; + } + + @Override public boolean performClick() { @@ -466,12 +493,12 @@ public class SleepDetailsView extends View { case MotionEvent.ACTION_MOVE: { final float x = ev.getX(); final float dx = x - moveEventLastX; - if(dx != 0) { - if(selectorPos < 0) { - selectorPos = (dx < 0)?chartLeftStart + chartWidth:0; + if (dx != 0) { + if (selectorPos < 0) { + selectorPos = (dx < 0) ? chartLeftStart + chartWidth : 0; } selectorPos = Math.max(0, Math.min(chartLeftStart + chartWidth, selectorPos + dx)); - if (isStart && Math.abs(dx) > 5.0) { + if (isStart && Math.abs(dx) > 10.0) { getParent().requestDisallowInterceptTouchEvent(true); isStart = false; } diff --git a/app/src/main/res/layout/fragment_sleepchart.xml b/app/src/main/res/layout/fragment_sleepchart.xml index 3ad1998d07..a72612d283 100644 --- a/app/src/main/res/layout/fragment_sleepchart.xml +++ b/app/src/main/res/layout/fragment_sleepchart.xml @@ -1,4 +1,5 @@ @@ -171,24 +172,25 @@ - - - + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_chart_chip.xml b/app/src/main/res/layout/layout_chart_chip.xml new file mode 100644 index 0000000000..41fdd86964 --- /dev/null +++ b/app/src/main/res/layout/layout_chart_chip.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file