0

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: enter image description here

Here is the screenshot from the Emulator: enter image description here

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: enter image description here

As requested here is the hot water tank container file: enter image description here

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:

enter image description here

VanessaF
  • 515
  • 11
  • 36
  • Can you share `hot_water_tank_container` file? also can you provide some other states of your tank? example: full, half state? – beigirad Apr 09 '23 at 07:25
  • @beigirad: Thanks for your comment. I added the hot water tank container file. What do you mean by "provide" some states? Full means that the hot water tank should be full. Thus the blue bar should cover the whole hot water tank (but it should be made sure, that the blue water bar does not exceed the rims). Half means, that the hot water tank should be filled until the middle with water (vertically). – VanessaF Apr 09 '23 at 07:40

2 Answers2

1

There are two issues here:

  1. The calculation of innerCircleCenter is incorrect. innerCircleCenter should be the X coordinate of the center of the water tank, but the current calculation seems to be set up for a specific screen size or a specific image width. The calculation should simply be innerCircleCenter = getWidth() / 2;.

  2. The lines app:layout_constraintWidth_percent="0.15" and app:layout_constraintHeight_percent="0.3" set the width and height of the HotWaterTank depending on the aspect ratio of the screen, which explains why it displays one way in your layout editor and another way on your emulator/actual device. In the example below, notice the width of the HotWaterTank is different depending on the aspect ratio of the screen. This is because width is set to 15 percent of the screen, so the wider the screen, the wider the box will be for the HotWaterTank (and the taller the screen, the taller the HotWaterTank will be). One of these constraints should be set to wrap_content so your image's aspect ratio is preserved (more on this below).

The top image is on a Google Pixel 1, bottom is on a Google Pixel 4:

Google Pixel

Google Pixle 4

This also might explain why it was working for months and then suddenly stopped working. Perhaps you recently upgraded your phone? :)

The custom view you have built is not currently set up to handle size changes like wrap_content. Here is an updated onSizeChanged and onMeasure that will enable you use wrap_content to the width (you'll have to make some changes if you want to wrap_content the height instead):

    @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) (positionOfWaterBar * bitmap.getHeight());
        lineEndY = (top + bitmap.getHeight()) - (int)(bitmap.getHeight() / 7f);
    }

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

And the updated xml for HotWaterTank:

    <com.example.test.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.25"
        app:layout_constraintHorizontal_bias="0.529"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.85" />

Result: Updated view

Rob
  • 518
  • 1
  • 3
  • 18
  • 1
    Thanks a lot Rob for your answer. I tried what you suggested but the result does not look as desired (see my updated question with a screenshot). Any idea as to why this is happening and how to fix it? The strange thing is that everything was working properly over many months with this code and now all of a sudden it looks so bad, altough I have not changed anything (most probably Android Studio has changed some libraries) – VanessaF Apr 09 '23 at 05:51
  • Hey Vanessa, sorry for the confusion earlier. I think I figured it out and updated my answer. – Rob Apr 10 '23 at 02:56
  • Thanks for your answer. While I understand your first point and how to fix it, I don't know how to fix the second point about `app:layout_constraintWidth_percent="0.15"`. I would like to have an UI that scales with the device and thus is device indepandant. So I choose `app:layout_constraintWidth_percent="0.15"` because I thought that this will make it flexible and fitting for multiple screen sizes. I have 2 questions on that: 1) Is your solution working for every screen size? 2) What would you advice me to do in order to have a layout for different device sizes? – VanessaF Apr 10 '23 at 23:27
  • Yes, my solution works for all screen sizes. Having the constraint set to 15% is okay, but it's a bad idea to have BOTH the width and the height constrained, because it modifies the aspect ratio of your image depending on the size of the screen (and thus moved the bar outside in this case). You should have just one (height OR width) constrained, and the other should be `wrap_content `. – Rob Apr 11 '23 at 01:15
  • Thanks for your comment and effort. I really appreciate it. The problem when usin g `wrap_content ` in the height layout is, that the area of the hot water tank in the Layout Editor wraps the whole vertical screen size and thus I can't place it at a special position in the screen. Just try your suggested approach out by getting rid of `app:layout_constraintHeight_percent="0.3"`. You will see that you can't control the UI element any more in its vertical position. – VanessaF Apr 11 '23 at 04:03
  • That surprised me because `wrap_content` usually shrinks the box to match the other constraints. I looked it up and found that with a custom view, you have to add some more logic in the `onMeasure` function to allow for size changes. See this answer for a good overview: https://stackoverflow.com/a/42430834/19224040 I'll also update my answer with some new code that's working for me. – Rob Apr 11 '23 at 07:38
  • 1
    Thanks a lot for your help ane effort Rob. Altough I still need to try out how to deal with the size of custom canvas, your answer is quite valuable. I upvoted it, accepted it and awarded the bouty to you. – VanessaF Apr 12 '23 at 11:16
  • I tried you solution with the scaling and the new `onMeasure` method but it still works quite bad and the problem mentioned above still persists. In the layout editor, when using your approach with `wrap_content` area of the hot water tank in the Layout Editor wraps the whole vertical screen size and thus I can't place it at a special position in the screen. For me it was obvious that changing the `onMeasure` method will not solve the problem in the XML code. – VanessaF Apr 20 '23 at 07:58
  • Can you share your updated code for both the HotWaterTank and the xml? It is working as expected on my machine. – Rob Apr 20 '23 at 08:55
  • Thanks Rob for your comment and effort. I really appreciate it. I just posted an update (Update 2) with the code you were asking for. – VanessaF Apr 21 '23 at 06:22
  • Very weird. I copied and pasted your updated code and it still works on my machine: https://i.imgur.com/xLLn9RT.png . The only thing I could think of is that we're using different versions of dependencies. Can you double check that your dependencies are set to the latest versions? – Rob Apr 22 '23 at 19:52
  • Thanks for your comment. I have 2 questions: 1)First of all I think in the screenshot of you the custom UI element also does not look that good for positioning, as there is a large white area around it within the blue line boundaries so you can only position it in an inprecise way. Is there a method how you can position the UI element without the white area around it. 2) You wrote "Can you double check that your dependencies are set to the latest versions? "--> How can I check if all dependencies are on the latest versions? – VanessaF Apr 23 '23 at 07:36
  • Yes, there are some changes you have to make in your `HotWaterTank` class to get rid of the padding (white area). I will update my answer with the changes. As for the dependencies, they are in the `build.gradle` file under `dependencies`. For example, I have `implementation 'androidx.constraintlayout:constraintlayout:2.1.4'`. If your dependency is out of date, Android Studio will highlight the verison number. Make sure you have the `constraintLayout` dependency. – Rob Apr 23 '23 at 19:26
  • You were creating the white space in your `onSizeChanged` method in the `HotWatertank`, I updated this method in my edit above. Use the new `onSizeChanged` method AND the new `onMeasure` methods AND update your dependencies. My advice for fixing future issues like this is to just go into the methods yourself and play around with different values to see what works and what doesn't. That's how I figured this one out. – Rob Apr 23 '23 at 19:37
  • Hi Rob. Thanks for your answer.. I did what you suggested and it looks good now. The only question I have is HOW did you manage to do this? You wrote "My advice for fixing future issues like this is to just go into the methods yourself and play around with different values to see what works and what doesn't." --> Believe it or not, I have at least spent 10 hours only playing around with values but I was not successfull. The problem is that I don't know at all how I should do this. What approach shall I use when playing around with those values. There are not guidlines for something like this. – VanessaF Apr 26 '23 at 06:47
  • I suppose it just comes with experience - I've been playing around with Android for a few years now, so I know how certain things (eg constraint layout) _should_ work. To find the root of the padding issue, I first checked the xml file for any attributes that might be adding padding to the image. When I didn't find any, I checked the HotWaterTank class. I saw that some modifications were being made to the height/width (`scaledHeight = (int) (height * 0.5);`), so I changed 0.5 to a random number to see if the image changed. In this case, removing the line completely got rid of the padding. – Rob Apr 26 '23 at 19:59
  • Thanks a lot for your answer and effort Rob. I really appreciate it. Unfortuntely I still don't have the foggiest notion about how to approach such problems. For example I tried to use your `onSizeChanged` and `onMeasure` on a very similar self-made canvas object but the result does not look good. Here is the question in case you are interested: https://stackoverflow.com/questions/76118290/how-to-remove-unwanted-padding-from-self-made-canvas-view-in-android – VanessaF Apr 27 '23 at 08:27
  • Thanks for your answers Rob. Any comments about my last comment (maybe even looking at the new question)? Can you tell me how I should approach issues like this? I really spent quite much time but I could not find any helpful advice about to to generally tackle these kind of issues – VanessaF Apr 29 '23 at 05:02
  • 1
    I'll take a look at your other question and if I can solve it I'll try to explain my thought process – Rob Apr 29 '23 at 18:52
1

Because of your hot_water_tank_container is a large image, I used AppCompatImageView as parent. Also used hard-code android:layout_width in xml with setAdjustViewBounds(true) in java code to simplify measuring calculations.

<com.example.game.HotWaterTank
    android:id="@+id/tank"
    android:layout_width="100dp"
    android:layout_height="wrap_content" />

Here the HotWaterTank class

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;

import androidx.appcompat.widget.AppCompatImageView;

public class HotWaterTank extends AppCompatImageView {
    private Paint mPaint;
    private Rect mRect;
    private int mColor = Color.parseColor("#327bff");

    private float percentage = 0f;

    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() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(mColor);
        mPaint.setStyle(Paint.Style.FILL);

        mRect = new Rect();

        setAdjustViewBounds(true);
        setImageResource(R.drawable.hot_water_tank_container);
    }

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

        mRect.set(0, 0, w, h);
        mRect.inset((int) (0.085f * w), (int) (0.18f * h)); /* can be refine more */
    }

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

        float top = mRect.bottom - (mRect.height() * percentage);
        canvas.drawRect(mRect.left, top, mRect.right, mRect.bottom, mPaint);
    }

    public void changeVolumeBar(float percentage) {
        this.percentage = percentage;
        invalidate();
    }
}

Shot: enter image description here

beigirad
  • 4,986
  • 2
  • 29
  • 52
  • Thanks a lot beigirad for your answer. I dont understand your statement "Because of your hot_water_tank_container was a large image, to simplify the calculations, I used AppCompatImageView as parent with hard-code android:layout_width in xml with setAdjustViewBounds(true) in java code.". I don't want to use hard-coded widths becasue the layout should be scalable for different devices. So I have to use `app:layout_constraintWidth_percent="0.15"` in the XML layout file. Further, you got rid of my method `setVolumeBarPosition`. Why did you do that? – VanessaF Apr 09 '23 at 23:43
  • You can use `app:layout_constraintWidth_percent="0.15"`. Why you need `setVolumeBarPosition`? What you need that didn't show in screen shot? – beigirad Apr 10 '23 at 04:18
  • Thanks beigirad for your answer. I used the answer posted by Rob above. Still, your answer is also valuable. So I upvoted it. Thanks. – VanessaF Apr 12 '23 at 11:16