package com.github.florent37.androidslidr; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.support.annotation.Nullable; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.FrameLayout; /** * Created by florentchampigny on 20/04/2017. */ public class Sushi extends FrameLayout { private static final float DISTANCE_TEXT_BAR = 35; private static final float BUBBLE_PADDING_HORIZONTAL = 15; private static final float BUBBLE_PADDING_VERTICAL = 3; private static final float BUBBLE_MIN_WITH = 0; private Settings settings; private float max = 1000; private float min = 0; private float currentValue = 0; private float barY; private float barWidth; private float indicatorX; private float barCenterY; private Bubble bubble = new Bubble(); private TextFormatter textFormatter = new EurosTextFormatter(); private RegionTextFormatter regionTextFormatter = null; private int calculatedHieght = 0; public Sushi(Context context) { this(context, null); } public Sushi(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public Sushi(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, @Nullable AttributeSet attrs) { setWillNotDraw(false); this.settings = new Settings(this); this.settings.init(context, attrs); } //region getters private float dpToPx(int size) { return size * getResources().getDisplayMetrics().density; } private float pxToDp(int size) { return size / getResources().getDisplayMetrics().density; } public float getMax() { return max; } public void setMax(float max) { this.max = max; updateValues(); update(); } public void setMin(float min) { this.min = min; updateValues(); update(); } public float getCurrentValue() { return currentValue; } public void setCurrentValue(float value) { this.currentValue = value; updateValues(); update(); } //endregion public void update() { if (barWidth > 0f) { float currentPercent = indicatorX / barWidth; currentValue = currentPercent * (max - min) + min; updateBubbleWidth(); } postInvalidate(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); updateValues(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { updateValues(); super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(calculatedHieght, MeasureSpec.EXACTLY)); } private void updateBubbleWidth() { this.bubble.width = calculateBubbleTextWidth() + BUBBLE_PADDING_HORIZONTAL * 2f; this.bubble.width = Math.max(BUBBLE_MIN_WITH, this.bubble.width); } private void updateValues() { if (currentValue < min) { currentValue = min; } settings.paddingCorners = settings.barHeight; barWidth = getWidth() - this.settings.paddingCorners * 2; updateBubbleWidth(); this.bubble.height = dpToPx(settings.textSizeBubble) + BUBBLE_PADDING_VERTICAL * 2f; this.barY = 0; if(settings.displayMinMax) { barY += DISTANCE_TEXT_BAR; float topTextHeight = 0; final String tmpTextLeft = formatRegionValue(0, 0); final String tmpTextRight = formatRegionValue(1, 0); topTextHeight = Math.max(topTextHeight, calculateTextMultilineHeight(tmpTextLeft, settings.paintTextTop)); topTextHeight = Math.max(topTextHeight, calculateTextMultilineHeight(tmpTextRight, settings.paintTextTop)); this.barY += topTextHeight + 3; } else { barY = 15; } this.barCenterY = barY + settings.barHeight / 2f; this.bubble.y = barCenterY - bubble.height / 2f; indicatorX = (currentValue - min) / (max - min) * barWidth; calculatedHieght = (int) (barCenterY + settings.barHeight); calculatedHieght += 10; //padding bottom } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); { final float paddingLeft = settings.paddingCorners; final float paddingRight = settings.paddingCorners; final float radiusCorner = settings.barHeight / 2f; final float indicatorCenterX = indicatorX + paddingLeft; { //background final float centerCircleLeft = paddingLeft; final float centerCircleRight = getWidth() - paddingRight; //grey background settings.paintBar.setColor(settings.colorBackground); canvas.drawCircle(centerCircleLeft, barCenterY, radiusCorner, settings.paintBar); canvas.drawCircle(centerCircleRight, barCenterY, radiusCorner, settings.paintBar); canvas.drawRect(centerCircleLeft, barY, centerCircleRight, barY + settings.barHeight, settings.paintBar); //color before indicator settings.paintBar.setColor(settings.foregroundColor); canvas.drawCircle(centerCircleLeft, barCenterY, radiusCorner, settings.paintBar); canvas.drawRect(centerCircleLeft, barY, indicatorCenterX, barY + settings.barHeight, settings.paintBar); } if (settings.displayMinMax) { //texts top (values) final float textY = barY - DISTANCE_TEXT_BAR; drawIndicatorsTextAbove(canvas, formatValue(min), settings.paintTextTop, 0 + paddingLeft, textY, Layout.Alignment.ALIGN_CENTER); drawIndicatorsTextAbove(canvas, formatValue(max), settings.paintTextTop, canvas.getWidth(), textY, Layout.Alignment.ALIGN_CENTER); } //bubble { float bubbleCenterX = indicatorCenterX; float trangleCenterX; bubble.x = bubbleCenterX - bubble.width / 2f; if (bubbleCenterX > canvas.getWidth() - bubble.width / 2f) { bubbleCenterX = canvas.getWidth() - bubble.width / 2f; } else if (bubbleCenterX - bubble.width / 2f < 0) { bubbleCenterX = bubble.width / 2f; } trangleCenterX = (bubbleCenterX + indicatorCenterX) / 2f; drawBubble(canvas, bubbleCenterX, trangleCenterX, bubble.getY()); } } canvas.restore(); } private String formatValue(float value) { return textFormatter.format(value); } private String formatRegionValue(int region, float value) { if (regionTextFormatter != null) { return regionTextFormatter.format(region, value); } else { return formatValue(value); } } private void drawText(Canvas canvas, String text, float x, float y, TextPaint paint, Layout.Alignment aligment) { canvas.save(); { canvas.translate(x, y); final StaticLayout staticLayout = new StaticLayout(text, paint, (int) paint.measureText(text), aligment, 1.0f, 0, false); staticLayout.draw(canvas); } canvas.restore(); } private void drawMultilineText(Canvas canvas, String text, float x, float y, TextPaint paint, Layout.Alignment aligment) { final float lineHeight = paint.getTextSize(); float lineY = y; for (CharSequence line : text.split("\n")) { canvas.save(); { final float lineWidth = (int) paint.measureText(line.toString()); float lineX = x; if (aligment == Layout.Alignment.ALIGN_CENTER) { lineX -= lineWidth / 2f; } if (lineX < 0) { lineX = 0; } final float right = lineX + lineWidth; if (right > canvas.getWidth()) { lineX = canvas.getWidth() - lineWidth - settings.paddingCorners; } canvas.translate(lineX, lineY); final StaticLayout staticLayout = new StaticLayout(line, paint, (int) lineWidth, aligment, 1.0f, 0, false); staticLayout.draw(canvas); lineY += lineHeight; } canvas.restore(); } } private void drawIndicatorsTextAbove(Canvas canvas, String text, TextPaint paintText, float x, float y, Layout.Alignment alignment) { final float textHeight = calculateTextMultilineHeight(text, paintText); y -= textHeight; final int width = (int) paintText.measureText(text); if (x >= getWidth() - settings.paddingCorners) { x = (getWidth() - width - settings.paddingCorners / 2f); } else if (x <= 0) { x = width / 2f; } else { x = (x - width / 2f); } if (x < 0) { x = 0; } if (x + width > getWidth()) { x = getWidth() - width; } drawText(canvas, text, x, y, paintText, alignment); } private float calculateTextMultilineHeight(String text, TextPaint textPaint) { return text.split("\n").length * textPaint.getTextSize(); } private float calculateBubbleTextWidth() { String bubbleText = formatValue(getCurrentValue()); return settings.paintTextBubble.measureText(bubbleText); } private void drawBubblePath(Canvas canvas, float triangleCenterX, float height, float width) { final Path path = new Path(); int padding = 3; final Rect rect = new Rect(padding, padding, (int) width - padding, (int) (height) - padding); final float roundRectHeight = (height) / 2; path.moveTo(rect.left + roundRectHeight, rect.top); path.lineTo(rect.right - roundRectHeight, rect.top); path.quadTo(rect.right, rect.top, rect.right, rect.top + roundRectHeight); path.lineTo(rect.right, rect.bottom - roundRectHeight); path.quadTo(rect.right, rect.bottom, rect.right - roundRectHeight, rect.bottom); path.lineTo(triangleCenterX, height - padding); path.lineTo(triangleCenterX, height - padding); path.lineTo(triangleCenterX, height - padding); path.lineTo(rect.left + roundRectHeight, rect.bottom); path.quadTo(rect.left, rect.bottom, rect.left, rect.bottom - roundRectHeight); path.lineTo(rect.left, rect.top + roundRectHeight); path.quadTo(rect.left, rect.top, rect.left + roundRectHeight, rect.top); path.close(); canvas.drawPath(path, settings.paintBubble); } private void drawBubble(Canvas canvas, float centerX, float triangleCenterX, float y) { final float width = this.bubble.width; final float height = this.bubble.height; canvas.save(); { canvas.translate(centerX - width / 2f, y); triangleCenterX -= (centerX - width / 2f); settings.paintBubble.setStyle(Paint.Style.FILL); settings.paintBubble.setColor(settings.foregroundColor); drawBubblePath(canvas, triangleCenterX, height, width); settings.paintBubble.setStyle(Paint.Style.FILL); } final String bubbleText = formatValue(getCurrentValue()); drawText(canvas, bubbleText, BUBBLE_PADDING_HORIZONTAL, bubble.getHeight() / 2f - settings.paintTextBubble.getTextSize() / 2f - BUBBLE_PADDING_VERTICAL, settings.paintTextBubble, Layout.Alignment.ALIGN_NORMAL); canvas.restore(); } public void setTextFormatter(TextFormatter textFormatter) { this.textFormatter = textFormatter; update(); } public void setRegionTextFormatter(RegionTextFormatter regionTextFormatter) { this.regionTextFormatter = regionTextFormatter; update(); } public Settings getSettings() { return settings; } public interface TextFormatter { String format(float value); } public interface RegionTextFormatter { String format(int region, float value); } public static class Settings { private Sushi slidr; private Paint paintBar; private TextPaint paintTextTop; private TextPaint paintTextBubble; private Paint paintBubble; private int colorBackground = Color.parseColor("#cccccc"); private int textColor = Color.parseColor("#6E6E6E"); private int textSize = 12; private int textSizeBubble = 16; private float barHeight = 35; private float paddingCorners; private int foregroundColor = Color.parseColor("#007E90"); private boolean displayMinMax = true; public Settings(Sushi slidr) { this.slidr = slidr; paintBar = new Paint(); paintBar.setAntiAlias(true); paintBar.setStrokeWidth(2); paintBar.setColor(colorBackground); paintTextTop = new TextPaint(); paintTextTop.setAntiAlias(true); paintTextTop.setStyle(Paint.Style.FILL); paintTextTop.setColor(textColor); paintTextTop.setTextSize(dpToPx(textSize)); paintTextBubble = new TextPaint(); paintTextBubble.setAntiAlias(true); paintTextBubble.setStyle(Paint.Style.FILL); paintTextBubble.setColor(Color.WHITE); paintTextBubble.setStrokeWidth(2); paintTextBubble.setTextSize(dpToPx(textSizeBubble)); paintBubble = new Paint(); paintBubble.setAntiAlias(true); paintBubble.setStrokeWidth(3); } private void init(Context context, AttributeSet attrs) { if (attrs != null) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Sushi); setColorBackground(a.getColor(R.styleable.Sushi_sushi_backgroundColor, colorBackground)); this.barHeight = a.getDimensionPixelOffset(R.styleable.Sushi_sushi_barHeight, (int) barHeight); this.foregroundColor = a.getColor(R.styleable.Sushi_sushi_foregroundColor, foregroundColor); this.textSize = a.getDimensionPixelOffset(R.styleable.Sushi_sushi_textSize, (int) dpToPx(textSize)); this.paintTextTop.setTextSize(textSize); this.textSizeBubble = a.getDimensionPixelOffset(R.styleable.Sushi_sushi_bubbleTextSize, (int) dpToPx(textSizeBubble)); this.paintTextBubble.setTextSize(textSizeBubble); this.displayMinMax = a.getBoolean(R.styleable.Sushi_sushi_displayMinMax, displayMinMax); a.recycle(); } } public void setBarHeight(int barHeight) { this.barHeight = barHeight; slidr.updateValues(); slidr.update(); } public void setForegroundColor(int foregroundColor) { this.foregroundColor = foregroundColor; slidr.update(); } public void setColorBackground(int colorBackground) { this.colorBackground = colorBackground; slidr.update(); } public void setTextSize(int textSize) { this.textSize = textSize; this.paintTextTop.setTextSize(textSize); slidr.updateValues(); slidr.update(); } public void setBubbleTextSize(int textSizeBubble) { this.textSizeBubble = textSizeBubble; this.paintTextBubble.setTextSize(textSizeBubble); slidr.updateValues(); slidr.update(); } private float dpToPx(int size) { return size * slidr.getResources().getDisplayMetrics().density; } public void setDisplayMinMax(boolean displayMinMax) { this.displayMinMax = displayMinMax; slidr.updateValues(); slidr.update(); slidr.requestLayout(); } } private class Bubble { private float height; private float width; private float x; private float y; public boolean clicked(MotionEvent e) { return e.getX() >= x && e.getX() <= x + width && e.getY() >= y && e.getY() < y + height; } public float getHeight() { return height; } public float getX() { return Math.max(x, 0); } public float getY() { return Math.max(y, 0); } } public class EurosTextFormatter implements TextFormatter { @Override public String format(float value) { return String.format("%d €", (int) value); } } }