I have a strange problem. I spent quite some time designing a self-made canvas view in Android which should be a water tank (that is the rim which insert via R.drawable.hot_water_tank_container
) and then there is also a blue bar inside the rim which depicts the water.
I have been using this for many months and everything worked correct. The blue bar was always being displayed inside the water tank rim (and even drawing operations to decrease or increase the size of the water bar were all within the boundaries).
Now all of a sudden (without knowingly having changed anything), the blue bar is not displayed anymore at the correct position. Further, the size of the water tank was (autmatically?) decreased somehow. I tested it on many emulators and on real devices. What makes it also strange is that on the layout editor, the self-made canvas view looks totally correct (the blue bar is within the rims and also the size is bigger).
Can anyone think about a reason as to why this is happening and how to fix it?
Here is the screenshot from the Layout Editor:
Here is the screenshot from the Emulator:
Here is the XML layout file of the displaying fragment:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.game.HotWaterTank
android:id="@+id/hotWaterTank"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintHorizontal_bias="0.529"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.895"
app:layout_constraintWidth_percent="0.15" />
<Button
android:id="@+id/button_action"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="Action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.102"
app:layout_constraintHorizontal_bias="0.373"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.745"
app:layout_constraintWidth_percent="0.13" />
</androidx.constraintlayout.widget.ConstraintLayout>
Here is the Java file of the self-made water tank canvas view:
package com.example.game;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
public class HotWaterTank extends View {
private Paint mInnerCirclePaint;
private int innerCircleCenter;
private int hotWaterColor = Color.parseColor("#327bff");
private Bitmap bitmap;
private int left;
private int top;
private int lineEndY;
private int lineStartY;
/*
Set the variables for the positions of the water bar
*/
double positionOfWaterBar = 0.5;
//0.607= empty, - 0.04 = full,
final double value_positionOfWaterBar_Empty = 0.607;
final double value_positionOfWaterBar_Full = -0.04;
final double value_positionOfWaterBar_upperLimit = -0.08;
final double value_positionOfWaterBar_lowerLimit = 0.61;
public HotWaterTank(Context context) {
this(context, null);
}
public HotWaterTank(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HotWaterTank(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (attrs != null) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Thermometer, defStyle, 0);
hotWaterColor = a.getColor(R.styleable.Thermometer_therm_color, hotWaterColor);
a.recycle();
}
init();
}
private void init() {
mInnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mInnerCirclePaint.setColor(hotWaterColor);
mInnerCirclePaint.setStyle(Paint.Style.FILL);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.hot_water_tank_container);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// init bitmap
int scaledHeight;
int scaledWidth;
int width = getWidth();
int height = getHeight();
if (width > height) {
scaledHeight = (int) (height * 0.5);
scaledWidth = scaledHeight * bitmap.getWidth() / bitmap.getHeight();
} else {
scaledWidth = (int) (width * 0.5);
scaledHeight = scaledWidth * bitmap.getHeight() / bitmap.getWidth();
}
mInnerCirclePaint.setStrokeWidth( (float) (scaledWidth * 0.9));
bitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true);
innerCircleCenter = (left + left + bitmap.getWidth() + (Math.min(width, height) / 72));
left = (getWidth() - bitmap.getWidth()) / 2;
top = (getHeight() - bitmap.getHeight()) / 2;
lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfWaterBar * bitmap.getHeight());
lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 7f);
//lineStartY = (int)(4.4*((int)(bitmap.getHeight() / 4.6f) + top));
//lineEndY = (int)(1.0* ((top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f)));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawThermometer(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//takes care of paddingTop and paddingBottom
int paddingY = getPaddingBottom() + getPaddingTop();
//get height and width
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
height += paddingY;
setMeasuredDimension(width, height);
}
private void drawThermometer(Canvas canvas) {
canvas.drawLine(innerCircleCenter , lineStartY, innerCircleCenter, lineEndY, mInnerCirclePaint);
canvas.drawBitmap(bitmap, left, top, new Paint());
}
public void setThermometerColor(int thermometerColor) {
this.hotWaterColor = thermometerColor;
mInnerCirclePaint.setColor(hotWaterColor);
invalidate();
}
public void changeVolumeBar( double percentageChangeOfTheWholeBar) {
double appliedPercentageChangeOfTheWholeBar = percentageChangeOfTheWholeBar / 100;
if (appliedPercentageChangeOfTheWholeBar>1) {
appliedPercentageChangeOfTheWholeBar = 1;
}
if (appliedPercentageChangeOfTheWholeBar <-1) {
appliedPercentageChangeOfTheWholeBar = -1;
}
double absolutValueSpanForTheWholeBar = value_positionOfWaterBar_Full - value_positionOfWaterBar_Empty;
positionOfWaterBar = positionOfWaterBar + appliedPercentageChangeOfTheWholeBar * absolutValueSpanForTheWholeBar;
if (positionOfWaterBar < value_positionOfWaterBar_upperLimit) {
positionOfWaterBar = value_positionOfWaterBar_upperLimit;
}
if(positionOfWaterBar > value_positionOfWaterBar_lowerLimit) {
positionOfWaterBar = value_positionOfWaterBar_lowerLimit;
}
lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfWaterBar * bitmap.getHeight());
lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 7f);
}
public void setVolumeBarPosition( double percentageOfTheWholeVolume) {
double appliedVolume = percentageOfTheWholeVolume;
if (appliedVolume < 0) {
appliedVolume = 0;
}
if (appliedVolume >100) {
appliedVolume = 100;
}
double absolutValueSpanForTheWholeBar = value_positionOfWaterBar_Full - value_positionOfWaterBar_Empty;
positionOfWaterBar = value_positionOfWaterBar_Empty + (appliedVolume/100 ) * absolutValueSpanForTheWholeBar;
lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfWaterBar * bitmap.getHeight());
lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 7f);
}
}
Update:
I inserted as suggested in one answer:
innerCircleCenter = left + (bitmap.getWidth() / 2)
mInnerCirclePaint.setStrokeWidth((float) (scaledWidth * 0.5))
but the result is even worse as you can see on this sceenshot:
As requested here is the hot water tank container file:
Update 2: Here is the new Java class or the Hot Water Tank with the new onMeasure
method:
package com.example.game;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
public class HotWaterTank extends View {
private Paint mInnerCirclePaint;
private int innerCircleCenter;
private int hotWaterColor = Color.parseColor("#327bff");
private Bitmap bitmap;
private int left;
private int top;
private int lineEndY;
private int lineStartY;
/*
Set the variables for the positions of the water bar
*/
double positionOfWaterBar = 0.5;
//0.607= empty, - 0.04 = full,
final double value_positionOfWaterBar_Empty = 0.607;
final double value_positionOfWaterBar_Full = -0.04;
final double value_positionOfWaterBar_upperLimit = -0.08;
final double value_positionOfWaterBar_lowerLimit = 0.61;
public HotWaterTank(Context context) {
this(context, null);
}
public HotWaterTank(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HotWaterTank(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (attrs != null) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Thermometer, defStyle, 0);
hotWaterColor = a.getColor(R.styleable.Thermometer_therm_color, hotWaterColor);
a.recycle();
}
init();
}
private void init() {
mInnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mInnerCirclePaint.setColor(hotWaterColor);
mInnerCirclePaint.setStyle(Paint.Style.FILL);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.hot_water_tank_container);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// init bitmap
int scaledHeight;
int scaledWidth;
int width = getWidth();
int height = getHeight();
if (width > height) {
scaledHeight = (int) (height * 0.5);
scaledWidth = scaledHeight * bitmap.getWidth() / bitmap.getHeight();
} else {
scaledWidth = (int) (width * 0.5);
scaledHeight = scaledWidth * bitmap.getHeight() / bitmap.getWidth();
}
mInnerCirclePaint.setStrokeWidth( (float) (scaledWidth * 0.9));
bitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true);
//innerCircleCenter = (left + left + bitmap.getWidth() + (Math.min(width, height) / 72));
innerCircleCenter = getWidth() / 2;
left = (getWidth() - bitmap.getWidth()) / 2;
top = (getHeight() - bitmap.getHeight()) / 2;
lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfWaterBar * bitmap.getHeight());
lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 7f);
//lineStartY = (int)(4.4*((int)(bitmap.getHeight() / 4.6f) + top));
//lineEndY = (int)(1.0* ((top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f)));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawThermometer(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// the actual dimensions of your water tank image
// these are just here to set the aspect ratio and the 'max' dimensions (we will make it smaller in the xml)
int desiredWidth = 421;
int desiredHeight = 693;
// takes care of paddingTop and paddingBottom
int paddingY = getPaddingBottom() + getPaddingTop();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
//Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
//Must be this size
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
width = Math.min(desiredWidth, widthSize);
} else {
//Be whatever you want
width = desiredWidth;
}
//Measure Height
if (heightMode == MeasureSpec.EXACTLY) {
//Must be this size
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
height = Math.min(desiredHeight, heightSize);
} else {
//Be whatever you want
height = desiredHeight;
}
height += paddingY;
setMeasuredDimension(width, height);
}
private void drawThermometer(Canvas canvas) {
canvas.drawLine(innerCircleCenter , lineStartY, innerCircleCenter, lineEndY, mInnerCirclePaint);
canvas.drawBitmap(bitmap, left, top, new Paint());
}
public void setThermometerColor(int thermometerColor) {
this.hotWaterColor = thermometerColor;
mInnerCirclePaint.setColor(hotWaterColor);
invalidate();
}
public void changeVolumeBar( double percentageChangeOfTheWholeBar) {
double appliedPercentageChangeOfTheWholeBar = percentageChangeOfTheWholeBar / 100;
if (appliedPercentageChangeOfTheWholeBar>1) {
appliedPercentageChangeOfTheWholeBar = 1;
}
if (appliedPercentageChangeOfTheWholeBar <-1) {
appliedPercentageChangeOfTheWholeBar = -1;
}
double absolutValueSpanForTheWholeBar = value_positionOfWaterBar_Full - value_positionOfWaterBar_Empty;
positionOfWaterBar = positionOfWaterBar + appliedPercentageChangeOfTheWholeBar * absolutValueSpanForTheWholeBar;
if (positionOfWaterBar < value_positionOfWaterBar_upperLimit) {
positionOfWaterBar = value_positionOfWaterBar_upperLimit;
}
if(positionOfWaterBar > value_positionOfWaterBar_lowerLimit) {
positionOfWaterBar = value_positionOfWaterBar_lowerLimit;
}
lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfWaterBar * bitmap.getHeight());
lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 7f);
}
public void setVolumeBarPosition( double percentageOfTheWholeVolume) {
double appliedVolume = percentageOfTheWholeVolume;
if (appliedVolume < 0) {
appliedVolume = 0;
}
if (appliedVolume >100) {
appliedVolume = 100;
}
double absolutValueSpanForTheWholeBar = value_positionOfWaterBar_Full - value_positionOfWaterBar_Empty;
positionOfWaterBar = value_positionOfWaterBar_Empty + (appliedVolume/100 ) * absolutValueSpanForTheWholeBar;
lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfWaterBar * bitmap.getHeight());
lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 7f);
}
}
And here the XML layout file:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.game.HotWaterTank
android:id="@+id/hotWaterTank"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintHorizontal_bias="0.529"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.895"
/>
<Button
android:id="@+id/button_action"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="Action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.102"
app:layout_constraintHorizontal_bias="0.373"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.745"
app:layout_constraintWidth_percent="0.13" />
</androidx.constraintlayout.widget.ConstraintLayout>
The problem is that once you set android:layout_width="wrap_content"
the layout of the views stratches to the boundaries of the display such that you can't position the View element precicesily any more, as you can see on this screenshot: