Issue with CoordinatorLayout and ImageView that adjusts width while scrolling
L

1

11

I'm attempting to put an ImageView in a CollapsingToolbarLayout in which it takes up the full screen on load and as you scroll the content, the 16x9 resolution image width resizes until the image takes up the full width of the screen. At that point, I'd like the image to parallax with a app:layout_collapseParallaxMultiplier of 0.5

Using this XML Layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/img_hero"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:adjustViewBounds="true"
                android:scaleType="centerCrop"
                android:src="@drawable/lake"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.5"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="none"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_scrolling"/>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end"
        app:srcCompat="@android:drawable/ic_dialog_email"/>

</android.support.design.widget.CoordinatorLayout>

Accomplishes the following:

enter image description here

Which the following shows what the actual boundaries of the image are:

enter image description here

As I scroll, I would like more of the image width to show as the height of the image shrinks and results in the following:

enter image description here

Once I get to this point, this is where I would like the collapse parallax multiplier of 0.5 to take effect.

I've messed with many different scroll behaviors, tried all of the ImageView scrollTypes, to no avail. Does anybody know if this is possible and if so, can provide any pointers into what I'm either doing wrong or not doing.

Do I need to create my own custom CoordinatorLayout.Behavior to accomplish this?

Lakenyalaker answered 4/1, 2017 at 7:47 Comment(1)
Can you show the scrolling effect of the your actual xml (some frame or animated gif) ?Portsalut
T
20

You can achieve what you want by tracking vertical offset of AppBarLayout. It has beautiful method addOnOffsetChangedListener, so you can scale your image depending on offset of AppBarLayout.

So, there are three things that you have to do to get it working:

  1. You need to place your image into drawable-nodpi folder, to prevent Android from scaling it for different screen sizes.
  2. Change your ImageView's property scaleType to matrix - it's needed as we will change matrix of this ImageView by ourselves.
  3. Implement addOnOffsetChangedListener for you AppBarLayout by next way:

    final ImageView imageView = (ImageView) findViewById(R.id.img_hero);
    AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.app_bar);
    appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
        @Override
        public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
            Matrix matrix = new Matrix(imageView.getImageMatrix());
    
            //get image's width and height
            final int dwidth = imageView.getDrawable().getIntrinsicWidth();
            final int dheight = imageView.getDrawable().getIntrinsicHeight();
    
            //get view's width and height
            final int vwidth = imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight();
            int vheight = imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom();
    
            float scale;
            float dx = 0, dy = 0;
            float parallaxMultiplier = ((CollapsingToolbarLayout.LayoutParams) imageView.getLayoutParams()).getParallaxMultiplier();
    
            //maintain the image's aspect ratio depending on offset
            if (dwidth * vheight > vwidth * dheight) {
                vheight += (verticalOffset); //calculate view height depending on offset
                scale = (float) vheight / (float) dheight; //calculate scale
                dx = (vwidth - dwidth * scale) * 0.5f; //calculate x value of the center point of scaled drawable
                dy = -verticalOffset * (1 - parallaxMultiplier); //calculate y value by compensating parallaxMultiplier
            } else {
                scale = (float) vwidth / (float) dwidth;
                dy = (vheight - dheight * scale) * 0.5f;
            }
    
            int currentWidth = Math.round(scale * dwidth); //calculate current intrinsic width of the drawable
    
            if (vwidth <= currentWidth) { //compare view width and drawable width to decide, should we scale more or not
                matrix.setScale(scale, scale);
                matrix.postTranslate(Math.round(dx), Math.round(dy));
                imageView.setImageMatrix(matrix);
            }
        }
    });
    

What I did here is just get ImageView's source code to determine bounds when it has centerCrop scale type and then just calculate the scale and translation of matrix depending on verticalOffset. If scale value is less than 1.0f then we've just reached the point where our view's aspect ratio is equal to the drawable's aspect ratio, and we don't need to scale more.

Note:

  1. It would work as you wish, only with the image whose width > height, otherwise its behavior would be the same as centerCrop
  2. It would work only if your parallaxMultiplier is in between 0 and 1.

How it looks for me:

enter image description here

Transhumance answered 8/1, 2017 at 19:44 Comment(12)
Were there any other edits to the XML that I provided other than changing the scaleType to Matrix? I added the listener but it didn't make it look like your animation, just changed how the image looks and still does the previous parallaxing.Lakenyalaker
Hm, it's strange. Yes, my XML is completely equal to yours one, just with one change - android:scaleType="matrix". I've posted my XML as well with edit, check this. Maybe you forgot somethingTranshumance
What is your image resolution btw?Transhumance
@Lakenyalaker seems I got your problem - you need to place your lake.jpg image to drawable-nodpi folder to prevent Android from scaling it for different device densities. Then it would work as expected. Updated the answerTranshumance
@Gpack I guess it's needed for translating the drawable to the center of the view after scaling. So after you've calculated the width of the scaled drawable, 0.5f multiplier is needed to get the center of this scaled drawable to translate to that point after - to reach center_crop behavior. I repeat - got this code from ImageView configureBounds method for center_crop case.Transhumance
@rom4ek And the -verticalOffset * (1 - parallaxMultiplier) to compensate the effect of the app:layout_collapseParallaxMultiplier in the xml until the scale is the same. Very good job.Portsalut
@Portsalut yes, true. Will comment these lines in code.Transhumance
@rom4ek Thanks for that, I moved the local resource to the nodpi folder and it worked. Also, switching it out to use a dynamic image loaded via Glide works as well. There a few sizing issues I need to work out once it comes from Glide, but that's just minor details.Lakenyalaker
@rom4ek Not sure, it is only a my raw idea: to avoid the influence of the dpi/px translation, it could be possible check the intrinsic ratio (h/w) of the drawable against the in progress ratio of the view, instead of if (scale >= 1.0f).Portsalut
@rom4ek I am noticing that if the image you're loading is not as wide as the ImageView width, then it adds space to the left/right side once scrolled to where the whole image is visible. I'm going to play around with the calculations in a bit to see what can be done to limit the image from getting smaller if the image has reached the width of the view.Lakenyalaker
@Lakenyalaker I finally got it, check my updated code of function in the answer. We need to calculate current intrinsic width of the drawable, and then compare it with the view width to decide should we scale anymore. Then it would work as expected, and your issue with the image that has smaller width than view's width would be resolved.Transhumance
I dont want it to scroll up to top. I just want it to be scrollable 25 % of the image. Any way to do that ?Messenia

© 2022 - 2024 — McMap. All rights reserved.