Search code examples
androidandroid-layoutandroid-linearlayoutandroid-constraintlayout

How to center ImageView with TextView vertically, yet making sure the TextView has enough space to be shown?


Background

This is a very simple question.

I want to have an ImageView and TextView centered, so that the TextView (and optionally more views with it) is below the ImageView, yet should always be shown (ImageView can scale down if needed, just keep aspect ratio).

The problem

For some reason, all of the ways I've thought about don't work. There is always a scenario (usually easy to reproduce by changing orientation to have less space available) that the TextView isn't shown.

What I've tried

  1. Initially, what I did is as such:

ic_android_red.xml - sample image instead of the original one.

<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="400dp"
        android:tint="#FF0009" android:viewportHeight="24.0"
        android:viewportWidth="24.0" android:width="400dp">
    <path android:fillColor="#FF000000"
          android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"/>
</vector>

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:orientation="vertical"
              android:layout_width="match_parent" android:gravity="center"
              android:layout_height="match_parent"
              tools:context=".MainActivity">

    <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content"
               app:srcCompat="@drawable/ic_android_red"/>

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"/>

</LinearLayout>

On portrait, it worked fine:

enter image description here

but on landscape, it could avoid showing the TextView completely:

enter image description here

  1. I tried to wrap it with NestedScrollView with android:fillViewport="true", but other than at least scrolling to it, it doesn't help any more.

  2. I tried to have the LinearLayout split into 2 halves (using weights and height of 0px), so that the top will have the ImageView set to the bottom of it (I used a FrameLayout to contain it), and the bottom will have the rest with its content aligned to the top and centered horizontally. But then it looks weird too, maybe because they are not really together anymore.

  3. I tried to use ConstraintLayout, so that the views will be chained together using app:layout_constraintVertical_chainStyle="packed" , and that the bottom part (the TextView) would have app:layout_constraintHeight_min="wrap" . Didn't work either.

The question

Is there an easy way to overcome it? To show both of the Views in the center, but with priority to the TextView ?

Currently, the only solution that is close to be considered as working is the 2 halves solution.


Solution

  • This can be accomplished with ConstraintLayout using a vertical packed chain and an ImageView with match_constraints (0dp) for its width and wrap_content for its height. The image view will also have the following set to keep the height within constraints when the image is taller than it is wide. (See "Dimension Constraints")

    app:layout_constrainedHeight="true"
    

    The vertical packed chain will group the ImageView and TextView together and centered vertically within the layout.

    match_constraints on the ImageView will permit the image to expand and contract to match boundaries while permitting room for the TextView that is placed directly beneath the image. If you don't want the image to change size unless needed, then set the width of the ImageView to wrap_content instead of match_constraints and set app:layout_constrainedWidth="true".

    An earlier post was predicated on a 1:1 aspect ratio of the image. This solution does not place a restriction on the aspect ratio. Here are some images of the layout working:

    Portrait mode with image width > height enter image description here

    Landscape mode with image width > height enter image description here

    Portrait mode with image height > width enter image description here

    Landscape mode with image height > width enter image description here

    I also added some margins to better see the true extents of the views but the margins can be removed. The size of the text in the TextView was also increased to make it more visible but can be any size.

    <androidx.constraintlayout.widget.ConstraintLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
    <!-- ImageView width could also be android:layout_height="wrap_content" 
    with app:layout_constrainedWidth="true"-->
    
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            app:layout_constrainedHeight="true"
            app:layout_constraintBottom_toTopOf="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_chainStyle="packed"
            app:srcCompat="@drawable/vertical_image" />
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:text="Hello World!"
            android:textSize="36sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/imageView" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    The key aspect of the layout is the use of app:layout_constrainedHeight=”true" which limits the height of theImageView`. From the documentation for ConstraintLayout::

    WRAP_CONTENT : enforcing constraints (Added in 1.1)

    If a dimension is set to WRAP_CONTENT, in versions before 1.1 they will be treated as a literal dimension -- meaning, constraints will not limit the resulting dimension. While in general this is enough (and faster), in some situations, you might want to use WRAP_CONTENT, yet keep enforcing constraints to limit the resulting dimension. In that case, you can add one of the corresponding attribute:

       app:layout_constrainedWidth=”true|false”
       app:layout_constrainedHeight=”true|false”