1

I have a custom made Canvas object in Android (thermometer) that unfortunately has a padding around it that I want to get rid of as it makes it quite difficult to position it within the layout. Here is how it looks like:

enter image description here

Here is the code of the Thermoeter Java class:

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 Thermometer extends View {
    private Paint mInnerCirclePaint;
    private int mInnerRadius;
    private int mThermometerColor = Color.RED;
    private Bitmap bitmap;
    private int left;
    private int top;
    private int innerCircleCenter;
    private int circleHeight;
    private int lineEndY;
    private int lineStartY;
    double positionOfTemperatureBar = 0.2;

    //0.378= 20°C, 0.2 = 21 °C, 0.022 = 22°C, 0.41 = lower limit, -0.03 = upper limit
    final double value_positionOfTemperatureBar_20Degrees = 0.378;
    final double value_positionOfTemperatureBar_22Degrees = 0.022;
    final double value_positionOfTemperatureBar_upperLimit = -0.03 ;
    final double value_positionOfTemperatureBar_lowerLimit = 0.41;




    public Thermometer(Context context) {
        this(context, null);
    }

    public Thermometer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Thermometer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        if (attrs != null) {

            final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Thermometer, defStyle, 0);

            mThermometerColor = a.getColor(R.styleable.Thermometer_therm_color, mThermometerColor);

            a.recycle();
        }

        init();
    }

    private void init() {
        mInnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mInnerCirclePaint.setColor(mThermometerColor);
        mInnerCirclePaint.setStyle(Paint.Style.FILL);

        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thermometer_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.90);
            scaledWidth = scaledHeight * bitmap.getWidth() / bitmap.getHeight();
        } else {
            scaledWidth = (int) (width * 0.90);
            scaledHeight = scaledWidth * bitmap.getHeight() / bitmap.getWidth();
        }

        bitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true);

        mInnerRadius = bitmap.getWidth() / 8;

        mInnerCirclePaint.setStrokeWidth((int)(bitmap.getWidth() / 10));
        left = (getWidth() - bitmap.getWidth()) / 2;
        top = (getHeight() - bitmap.getHeight()) / 2;
        innerCircleCenter = (left + left + bitmap.getWidth() + (Math.min(width, height) / 72)) / 2;
        circleHeight = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4.6f);


        lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (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.drawCircle(innerCircleCenter, circleHeight, mInnerRadius, mInnerCirclePaint);
        canvas.drawLine(innerCircleCenter, lineStartY, innerCircleCenter, lineEndY, mInnerCirclePaint);
        canvas.drawBitmap(bitmap, left, top, new Paint());
    }

    public void setThermometerColor(int thermometerColor) {
        this.mThermometerColor = thermometerColor;
        mInnerCirclePaint.setColor(mThermometerColor);
        invalidate();
    }

    public void changeTemperature( double percentageChangeOfTheWholeBar) {
        double appliedPercentageChangeOfTheWholeBar = percentageChangeOfTheWholeBar / 100;
        if (appliedPercentageChangeOfTheWholeBar>1) {
            appliedPercentageChangeOfTheWholeBar = 1;
        }
        if (appliedPercentageChangeOfTheWholeBar <-1) {
            appliedPercentageChangeOfTheWholeBar = -1;
        }

        double absolutValueSpanForTheWholeBar = value_positionOfTemperatureBar_22Degrees - value_positionOfTemperatureBar_20Degrees;

        positionOfTemperatureBar = positionOfTemperatureBar + appliedPercentageChangeOfTheWholeBar * absolutValueSpanForTheWholeBar;
        if (positionOfTemperatureBar < value_positionOfTemperatureBar_upperLimit) {
            positionOfTemperatureBar = value_positionOfTemperatureBar_upperLimit;
        }

        if(positionOfTemperatureBar > value_positionOfTemperatureBar_lowerLimit) {
            positionOfTemperatureBar = value_positionOfTemperatureBar_lowerLimit;
        }


        lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f);
    }

    public void setTemperature( double setTemperatureDegreesCelsius) {
        double appliedSetTemperature = setTemperatureDegreesCelsius;
        if (appliedSetTemperature < 20) {
            appliedSetTemperature = 20;
        }
        if (appliedSetTemperature >22) {
            appliedSetTemperature = 22;
        }

        double absolutValueSpanForTheWholeBar = value_positionOfTemperatureBar_22Degrees - value_positionOfTemperatureBar_20Degrees;

        positionOfTemperatureBar = value_positionOfTemperatureBar_20Degrees + ((setTemperatureDegreesCelsius - 20 )/2)  * absolutValueSpanForTheWholeBar;



        lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f);
    }


}

And here is the code of the XML layout file that contains the thermometer object:

<?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.Thermometer
        android:id="@+id/thermometer"
        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.795"
        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>

Following the answer to this question Self-made canvas view in Android is not correctly displayed (any more) I tried to change the onSizeChanged and onMeasure method accordingly which looked like this:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    // init bitmap

    int width = getWidth();
    int height = getHeight();

    mInnerCirclePaint.setStrokeWidth( (float) (width * 0.9));

    bitmap = Bitmap.createScaledBitmap(bitmap, width, height, 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) (positionOfTemperatureBar * bitmap.getHeight());
    lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 7f);

}

@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;

    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 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;
    }

    //Measure Width
    if (widthMode == MeasureSpec.EXACTLY) {
        //Must be this size
        width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        width = heightSize * desiredWidth / desiredHeight;
    } else {
        //Be whatever you want
        width = desiredWidth;
    }

    setMeasuredDimension(width, height);
}

But the result looked like this: enter image description here

This is obviously not what I want because the temperature bar is way to big (altough the padding disappeared). For me it is extremely difficult to design such self-made canvas objects in Android as I don't know what to adjust in order to make them look properly. Do you have any idea what I can do in order to have a proper temperature bar in the self-made canvas object while having no padding? How do you approach such kind of problems?

Update: Here is the R.drawable.thermometer_container enter image description here

And here is the R.styleable.Thermometer

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Thermometer">
        <attr name="therm_color" format="color" />
    </declare-styleable>
</resources>

Update: I tried the approach suggested by Rob but unfortunately there are 3 problems with it as you can see in the screenshot: enter image description here

  1. I can't place the view precisely as the blue area aroung the view wraps horizontally to the boundaries which is not what I want
  2. Layout Editor: The red circle inside the thermometer is too small and the whole red bar is too much on the right and not in the center.
  3. In the Emulator the custom view looks extremely bad as you can see on the screenshotenter image description here

Here is the updated code of the Thermometer.class

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 Thermometer extends View {
    private Paint mInnerCirclePaint;
    private int mInnerRadius;
    private int mThermometerColor = Color.RED;
    private Bitmap bitmap;
    private int left;
    private int top;
    private int innerCircleCenter;
    private int circleHeight;
    private int lineEndY;
    private int lineStartY;
    double positionOfTemperatureBar = 0.2;

    //0.378= 20°C, 0.2 = 21 °C, 0.022 = 22°C, 0.41 = lower limit, -0.03 = upper limit
    final double value_positionOfTemperatureBar_20Degrees = 0.378;
    final double value_positionOfTemperatureBar_22Degrees = 0.022;
    final double value_positionOfTemperatureBar_upperLimit = -0.03 ;
    final double value_positionOfTemperatureBar_lowerLimit = 0.41;




    public Thermometer(Context context) {
        this(context, null);
    }

    public Thermometer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Thermometer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        if (attrs != null) {

            final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Thermometer, defStyle, 0);

            mThermometerColor = a.getColor(R.styleable.Thermometer_therm_color, mThermometerColor);

            a.recycle();
        }

        init();
    }

    private void init() {
        mInnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mInnerCirclePaint.setColor(mThermometerColor);
        mInnerCirclePaint.setStyle(Paint.Style.FILL);

        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thermometer_container);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        bitmap = Bitmap.createScaledBitmap(bitmap,  getWidth(), getHeight(), true);

        mInnerRadius = bitmap.getWidth() / 5;
        innerCircleCenter = getWidth()/ 2;
        circleHeight = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 5f);

        mInnerCirclePaint.setStrokeWidth((int)(bitmap.getWidth() / 7));
        lineStartY = ((int)(bitmap.getHeight() / 6.4f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 2f);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        drawThermometer(canvas);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int desiredWidth = 558;
        int desiredHeight = 730;

        //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(desiredWidth, desiredHeight);
        //setMeasuredDimension(width, height);
    }

    private void drawThermometer(Canvas canvas) {
        canvas.drawCircle(innerCircleCenter, circleHeight, mInnerRadius, mInnerCirclePaint);
        canvas.drawLine(innerCircleCenter, lineStartY, innerCircleCenter, lineEndY, mInnerCirclePaint);
        canvas.drawBitmap(bitmap, left, top, new Paint());
    }

    public void setThermometerColor(int thermometerColor) {
        this.mThermometerColor = thermometerColor;
        mInnerCirclePaint.setColor(mThermometerColor);
        invalidate();
    }

    public void changeTemperature( double percentageChangeOfTheWholeBar) {
        double appliedPercentageChangeOfTheWholeBar = percentageChangeOfTheWholeBar / 100;
        if (appliedPercentageChangeOfTheWholeBar>1) {
            appliedPercentageChangeOfTheWholeBar = 1;
        }
        if (appliedPercentageChangeOfTheWholeBar <-1) {
            appliedPercentageChangeOfTheWholeBar = -1;
        }

        double absolutValueSpanForTheWholeBar = value_positionOfTemperatureBar_22Degrees - value_positionOfTemperatureBar_20Degrees;

        positionOfTemperatureBar = positionOfTemperatureBar + appliedPercentageChangeOfTheWholeBar * absolutValueSpanForTheWholeBar;
        if (positionOfTemperatureBar < value_positionOfTemperatureBar_upperLimit) {
            positionOfTemperatureBar = value_positionOfTemperatureBar_upperLimit;
        }

        if(positionOfTemperatureBar > value_positionOfTemperatureBar_lowerLimit) {
            positionOfTemperatureBar = value_positionOfTemperatureBar_lowerLimit;
        }


        lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f);
    }

    public void setTemperature( double setTemperatureDegreesCelsius) {
        double appliedSetTemperature = setTemperatureDegreesCelsius;
        if (appliedSetTemperature < 20) {
            appliedSetTemperature = 20;
        }
        if (appliedSetTemperature >22) {
            appliedSetTemperature = 22;
        }

        double absolutValueSpanForTheWholeBar = value_positionOfTemperatureBar_22Degrees - value_positionOfTemperatureBar_20Degrees;

        positionOfTemperatureBar = value_positionOfTemperatureBar_20Degrees + ((setTemperatureDegreesCelsius - 20 )/2)  * absolutValueSpanForTheWholeBar;



        lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f);
    }


}

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">

    <View
        android:id="@+id/imageView_TargetRectangle"
        android:layout_width="0dp"
        android:layout_height="0dp"


        android:background="@drawable/rectangle"
        app:layout_constraintBottom_toBottomOf="parent"

        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent="0.16"
        app:layout_constraintHorizontal_bias="0.535"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.01"
        app:layout_constraintWidth_percent="0.10" />


    <com.example.game.Thermometer
        android:id="@+id/thermometer"
        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.795"/>

    <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.343"
        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>

Do you know what to do in order to solve these issues?

VanessaF
  • 515
  • 11
  • 36
  • Can you please share your `R.styleable.Thermometer`, `R.styleable.Thermometer_therm_color`, and `R.drawable.thermometer_container`? – Rob Apr 29 '23 at 19:06
  • @Rob: Thanks for your comment. I edited my answer (altough I don't know what the difference between the `R.styleable.Thermometer, R.styleable.Thermometer_therm_colo` are. Hope it helps – VanessaF Apr 30 '23 at 06:27

1 Answers1

1

I'll try to explain my thought process here, so if you encounter a situation like this again you'll have a reference on some steps you can take to solve the problem.

This is very similar to your hot water tank question, but with some important differences. The major difference is the image itself. This is your thermometer image (opened in Photoshop to clearly see the background)- notice the padding around the thermometer:

Original thermometer image

Because this padding is part of the image itself, we can't reduce the padding in Android Studio beyond the edges of the image (actually we can, but it's easier to just modify the image). So that would be the first step: crop the image to get rid of the excess padding. I have done that here:

enter image description here

Note that there is still some space on the right side so that the thermometer body is directly in the center of the image.

Next, the issues with the thermometer class itself. You were on the right track modifying the onSizeChanged and the onMeasure methods, but they can't be exactly the same as they were for the water tank - we have different end goals here. In the water tank class, we were just trying to create a rectangle to represent the water inside the tank, but here we're trying to create a circle and a rectangle to represent the fluid inside the thermometer. Let's go through your original onSizeChanged method in the thermometer class, starting with // init bitmap:

        // init bitmap
        int scaledHeight;
        int scaledWidth;
        int width = getWidth();
        int height = getHeight();
        if (width > height) {
            scaledHeight = (int) (height * 0.90);
            scaledWidth = scaledHeight * bitmap.getWidth() / bitmap.getHeight();
        } else {
            scaledWidth = (int) (width * 0.90);
            scaledHeight = scaledWidth * bitmap.getHeight() / bitmap.getWidth();
        }

        bitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true);

It's hard for me to understand the point of this code. It seems like we're getting the width and the height of the bitmap, then multiplying each side by 0.9 and setting the bitmap's dimensions to the result. Let's just not modify the dimensions for now, and we can come back to it later if we need to. This code can be replaced by the single line:

bitmap = Bitmap.createScaledBitmap(bitmap,  getWidth(), getHeight(), true);

Next, we have the code:

        mInnerRadius = bitmap.getWidth() / 8;

        mInnerCirclePaint.setStrokeWidth((int)(bitmap.getWidth() / 10));
        left = (getWidth() - bitmap.getWidth()) / 2;
        top = (getHeight() - bitmap.getHeight()) / 2;
        innerCircleCenter = (left + left + bitmap.getWidth() + (Math.min(width, height) / 72)) / 2;
        circleHeight = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4.6f);


        lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f);

Line by line:

mInnerRadius = bitmap.getWidth() / 8; sets the radius of the red circle at the bottom of the thermometer. The radius will be the width of the bitmap divided by 8. Note that because we're going to use the cropped thermometer, we're going to have to play around with this number to find a radius that looks good with our new image.

mInnerCirclePaint.setStrokeWidth((int)(bitmap.getWidth() / 10)); sets the stroke width of the rectangular part of the thermometer's fluid. Same as above, we have to modify this to fit our new image.

The next three lines, assigning values to left, top, and innerCircleCenter, seem to have no effect on the generation of the image, so let's just get rid of them. Again, we can come back to these later if we find out something breaks.

circleHeight = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4.6f); determines where on the bitmap to draw the red circle. Like above, we'll have to modify this value.

The last two lines assigning values to lineStartY and lineEndY determine the height at which the red rectangle will start and end. We'll modify these as well.

So, here is our new onSizeChanged method, with new values to fit our newly cropped thermometer image. Feel free to play around with the values to see how modifying them changes the size and position of the red rectangle/circle.

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        bitmap = Bitmap.createScaledBitmap(bitmap,  getWidth(), getHeight(), true);

        mInnerRadius = bitmap.getWidth() / 6;
        innerCircleCenter = getWidth()/ 2;
        circleHeight = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 5f);

        mInnerCirclePaint.setStrokeWidth((int)(bitmap.getWidth() / 7));
        lineStartY = ((int)(bitmap.getHeight() / 6.4f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 2f);
    }

The onMeasure method is fine except for the first two lines. These need to match the aspect ratio of the image you're using. In your previous question, I used the dimensions of the hot water tank image, but the dimensions and the aspect ratio of the thermometer are different:

        int desiredWidth = 558;
        int desiredHeight = 730;

Finally, as mentioned in the previous post, you should not be assigning both a layout_constraintHeight_percent and a layout_constraintWidth_percent, because doing so will warp your image. Get rid of one of these constraints and instead set the layout_width or layout_height to wrap_content. Updated xml:

    <com.example.test2.Thermometer
        android:id="@+id/thermometer"
        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.795"/>

In sum: First, crop the excess padding in the image (or use the image I cropped above). Then, modify the onSizeChanged to account for the size differences in the new image. Finally, edit the dimensions in onMeasure to match the new image.

Result:

Final result

Here is the complete code for my Thermometer.java:

package com.example.test2;

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 Thermometer extends View {
    private Paint mInnerCirclePaint;
    private int mInnerRadius;
    private int mThermometerColor = Color.RED;
    private Bitmap bitmap;
    private int left;
    private int top;
    private int innerCircleCenter;
    private int circleHeight;
    private int lineEndY;
    private int lineStartY;
    double positionOfTemperatureBar = 0.2;

    //0.378= 20°C, 0.2 = 21 °C, 0.022 = 22°C, 0.41 = lower limit, -0.03 = upper limit
    final double value_positionOfTemperatureBar_20Degrees = 0.378;
    final double value_positionOfTemperatureBar_22Degrees = 0.022;
    final double value_positionOfTemperatureBar_upperLimit = -0.03 ;
    final double value_positionOfTemperatureBar_lowerLimit = 0.41;



    public Thermometer(Context context) {
        this(context, null);
    }

    public Thermometer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Thermometer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        if (attrs != null) {

            final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Thermometer, defStyle, 0);

            mThermometerColor = a.getColor(R.styleable.Thermometer_therm_color, mThermometerColor);

            a.recycle();
        }

        init();
    }

    private void init() {
        mInnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mInnerCirclePaint.setColor(mThermometerColor);
        mInnerCirclePaint.setStyle(Paint.Style.FILL);

        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.updated_therm);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        bitmap = Bitmap.createScaledBitmap(bitmap,  getWidth(), getHeight(), true);

        mInnerRadius = bitmap.getWidth() / 6;
        innerCircleCenter = getWidth() / 2;
        circleHeight = (bitmap.getHeight()) - (int)(bitmap.getHeight() / 5f);

        mInnerCirclePaint.setStrokeWidth((int)(bitmap.getWidth() / 7));
        lineStartY = ((int)(bitmap.getHeight() / 6.4f)) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (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) {
        int desiredWidth = 558;
        int desiredHeight = 730;
        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 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;
        }

        //Measure Width
        if (widthMode == MeasureSpec.EXACTLY) {
            //Must be this size
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            //Can't be bigger than...
            width = heightSize * desiredWidth / desiredHeight;
        } else {
            //Be whatever you want
            width = desiredWidth;
        }

        setMeasuredDimension(width, height);
    }

    private void drawThermometer(Canvas canvas) {
        canvas.drawCircle(innerCircleCenter, circleHeight, mInnerRadius, mInnerCirclePaint);
        canvas.drawLine(innerCircleCenter, lineStartY, innerCircleCenter, lineEndY, mInnerCirclePaint);
        canvas.drawBitmap(bitmap, left, top, new Paint());
    }

    public void setThermometerColor(int thermometerColor) {
        this.mThermometerColor = thermometerColor;
        mInnerCirclePaint.setColor(mThermometerColor);
        invalidate();
    }

    public void changeTemperature( double percentageChangeOfTheWholeBar) {
        double appliedPercentageChangeOfTheWholeBar = percentageChangeOfTheWholeBar / 100;
        if (appliedPercentageChangeOfTheWholeBar>1) {
            appliedPercentageChangeOfTheWholeBar = 1;
        }
        if (appliedPercentageChangeOfTheWholeBar <-1) {
            appliedPercentageChangeOfTheWholeBar = -1;
        }

        double absolutValueSpanForTheWholeBar = value_positionOfTemperatureBar_22Degrees - value_positionOfTemperatureBar_20Degrees;

        positionOfTemperatureBar = positionOfTemperatureBar + appliedPercentageChangeOfTheWholeBar * absolutValueSpanForTheWholeBar;
        if (positionOfTemperatureBar < value_positionOfTemperatureBar_upperLimit) {
            positionOfTemperatureBar = value_positionOfTemperatureBar_upperLimit;
        }

        if(positionOfTemperatureBar > value_positionOfTemperatureBar_lowerLimit) {
            positionOfTemperatureBar = value_positionOfTemperatureBar_lowerLimit;
        }


        lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f);
    }

    public void setTemperature( double setTemperatureDegreesCelsius) {
        double appliedSetTemperature = setTemperatureDegreesCelsius;
        if (appliedSetTemperature < 20) {
            appliedSetTemperature = 20;
        }
        if (appliedSetTemperature >22) {
            appliedSetTemperature = 22;
        }

        double absolutValueSpanForTheWholeBar = value_positionOfTemperatureBar_22Degrees - value_positionOfTemperatureBar_20Degrees;

        positionOfTemperatureBar = value_positionOfTemperatureBar_20Degrees + ((setTemperatureDegreesCelsius - 20 )/2)  * absolutValueSpanForTheWholeBar;



        lineStartY = ((int)(bitmap.getHeight() / 4.6f) + top) + (int) (positionOfTemperatureBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f);
    }
}
Rob
  • 518
  • 1
  • 3
  • 18
  • Thanks a lot Rob for your detailed answer. It definitely gives me some insights about how to approach something like this, altough I still think it's quite difficult to come up with something like this on my own. But thanks a lot for your effort and tremendous help. I really appreciate it (I upvoted and accepted your answer) – VanessaF May 06 '23 at 07:22
  • Hi Rob, it's me again. I have to admit I was a little bit to fast accepting your answer without having tried to implement it (I was on vacation this is why I just wanted to response which was a mistake). Actually trying out your suggested answer makes the problem way worse. The view looks quite bad in the Emulator (and it also looks wrong in the Layout Editor) as you can see in my updated question. Do you know why this is happening? I'll highly appreciate every further comment from you. – VanessaF Jun 04 '23 at 10:50
  • Hi Vanessa, it looks like your new `onMeasure` method has changed, it should be the same as in your original post (except the dimensions should be changed as described in my answer). Could you try changing that method back to how it was and let me know how it looks? – Rob Jun 05 '23 at 03:24
  • Thanks for your answer. If you have a closer look at my `onMeasure` method, you will see that it has not changed as you stated. It is exactly the same as before and I added the first two lines that you suggested ` int desiredWidth = 558; int desiredHeight = 730;` Then in the `setMeasuredDimension` method I used both the old argument `setMeasuredDimension(width, height);` and the new ones `setMeasuredDimension(desiredWidth, desiredHeight);`. In both cases the thermometer looks awful as you can see in the Emulator screenshots posted above – VanessaF Jun 08 '23 at 09:06
  • Ah I see, I meant the _second_ block of code's `onMeasure`, the one that has the `//Measure Height if (heightMode == MeasureSpec.EXACTLY) { //Must be this size` stuff. Try that `onMeasure` method but with the updated dimensions – Rob Jun 08 '23 at 18:22
  • Thanks for your comment and effort. I really appreciate it. I have to admit that I still don't know what to do. In my code there is no such thing as `if (heightMode == MeasureSpec.EXACTLY)`. Further, if I add that into the `onMeasure` method I get the error message `Cannot resolve symbol 'heightMode'` – VanessaF Jun 09 '23 at 15:12
  • I added the complete code for the thermometer class, give that a try and see if that fixes the sizing issue. – Rob Jun 10 '23 at 01:26
  • Thanks a lot Rob for your answer and effort. I used your code and the thermometer looks okay altough there are still 2 problems: 1) The red bar and cycle are a little bit tot much on the the right. How can I just slightly shift them a little too the left? 2) The temperature bar just does not go until the very top. It stops way before the 22°C mark. How can I change it such that it goes a little above the 22°C mark at its maximum? – VanessaF Jun 10 '23 at 09:10
  • To shift the circle and the bar left/right, change number in the line `innerCircleCenter = getWidth() / 2`. For example, changing it to `innerCircleCenter = (int) (getWidth() / 2.5)` shifts it to the left. Similarly, changing the numbers in `lineStartY` and `lineEndY` should adjust where the temperature bar starts and ends, you'll just have to play around with the numbers until you find something that works for you. – Rob Jun 10 '23 at 19:03
  • Thanks for your answer. Actually altering the `lineEndY` in the `onSizeChanged` method does not have any effect. I also altered it in the other methods but could not archieve what I want. Do you have any idea where and how to change it? Just me playing around with the values without understanding anything of it does not lead to the desired output as I have to admit I am just randomly guessing values. The bigger problem is that the Canvas class is extremely complex and so is the calculation of the lines `lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 4f);`. – VanessaF Jun 11 '23 at 09:20
  • How can I adjust it such that the end of the line goes a little bit more to the upper part of the thermometer? I have no clue what the equation of `lineEndY` actually does and how to alter it. The general better option is to come up with something more intuitive for creating self-made views but I don't know if there exists something like this in Android. The canvas approach is just too complex. But still, if you have any clue how and where to adjust the equation for `lineEndY `, I'll highly appreciate it if you can share it with me. – VanessaF Jun 11 '23 at 09:25
  • If you set `lineStartY = 0`, the line will start at the very top of the thermometer image. If you set `lineStartY = (int)bitmap.getHeight()`, the line will start at the very bottom of the thermometer image. If you set `lineStartY = (int)(bitmap.getHeight() / 2)`, the line will start halfway through the image. `lineStartY = (int)(bitmap.getHeight() / 6)` starts the line 1/6 of the way from the top of the image.. etc – Rob Jun 13 '23 at 07:22
  • I agree with you, there is almost certainly an easier way to do this – Rob Jun 13 '23 at 07:24
  • Thanks Rob for your comment. But how do I have to adjust `lineEndY` as this does not seem to have any effect in my code. I changed it in the `setTemperature`, `changeTemperature` and `onSizeChanged` method by adding 100 to each calculation but this did not change anything. Do you have any clue where and how to change it? Before the code modifications it was working properly but now with your suggested approach there is this strange problem, that the red bar does not got to the very top of the canvas. – VanessaF Jun 13 '23 at 17:14
  • Any comments to my last comment? I'll highly appreciate every further comment from you. – VanessaF Jun 15 '23 at 20:12
  • Yes, it looks like you'll have to adjust the `lineStartY` in those other functions as well. I'm not sure what you mean when you say that the red bar doesn't go to the top of the thermometer, because like I said in the previous comment, if you set `lineStartY = 0`, the line starts at the very top of the thermometer image. – Rob Jun 17 '23 at 18:48
  • Thanks for your comment. What I mean by "the red bar doesn't go to the top of the thermometer" is that the red bar of the thermometer does not reach the 22°C mark. It stays below no matter how much I increase it by calling the method `changeTemperature`. What I want is that the bar can go even above the mark for the 22°C temperature. So the red bar should - if the user calls the `changeTemperature` frequently by pressing the button - go over the stroke for the 22°C mark. In my old cold this was possible but with your suggested code it always stays below that stroke. Any idea how to do this. – VanessaF Jun 18 '23 at 15:25
  • Okay, but initially, does the bar meet the top of the image when you set `lineStartY = 0`? – Rob Jun 18 '23 at 19:03
  • Thanks Rob for your comment. When setting `lineStartY = 0` in all three methods `setTemperature`, `changeTemperature` and `onSizeChanged` the red bar is at the very top of the thermometer. It is actually even too much on the top such that it does not look good anymore as it is in the outside of the thermometer jpeg rim. – VanessaF Jun 21 '23 at 17:40
  • Any comments to my last comment? I'll highly appreciate every further comment from you. – VanessaF Jun 24 '23 at 08:19
  • So with that you're able to set the red bar height to the top of the image. With `lineStartY = (int)(bitmap.getHeight()`, you're able to set the bar height to the bottom of the image (and dividing the height by 2 puts it in the center of the image). You can play around with the different heights and see what lines up with your desired temperature readings. I'm not sure what else I can say about this without writing the methods myself. – Rob Jun 24 '23 at 19:35