Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spotify ListView header image effect

The Android version of Spotify has a unique ListView header effect when viewing an artist. Basically the header image appears to maintain it's own scrolling speed apart from the actual list. Does anyone know what I'm talking about? If so, can someone explain how to achieve this effect?

Here is a link to a video outlining the header image effect I'm referring to:

http://servestream.sourceforge.net/20130911_200347.mp4

like image 433
William Seemann Avatar asked Jul 29 '13 19:07

William Seemann


2 Answers

Thanks for posting the video. This is a parallax effect. The following library can help you achieve it:

ParallaxScrollView: A Parallax ScrollView which takes a background and foreground view, in the ParallexScrollView.

Link

So, I went ahead and modified the demo offered on the link. If this is what you are after, let me know and I'll add details on the modifications I did to get this working.

APK Link

How to get this:

If there was anything but a ListView in the scrollable part, the effect would have been easy to achieve. Because the container holding the ListView is an extended ScrollView, things get complicated. The following modifications were made:

In the activity, inflate the following layout:

<couk.jenxsol.parallaxscrollview.views.ParallaxScrollView xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".DemoActivity" >

<!-- Top Image: Here, the height is set to 300dp. You can set this in code -->
<!-- depending on screen dimensions -->

    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:gravity="center"
        android:scaleType="fitXY"
        android:src="@drawable/image_to_use" />

<!-- Foreground -->
<!-- You can place any of the items below as the foreground,  -->
<!-- but for most control, add the scroll view yourself. -->

<!-- This is the area that will hold the ListView -->
<!-- Also note that the LinearLayout's top margin will 
<!-- depend on ImageView's height. Here, there's an overalp of 100dp -->
<!-- between the ImageView and the LinearLayout -->
<!-- Reason: The first TextView(with a transparent background) inside the 
<!-- LinearLayout is displayed over the ImageView. -->
<!-- So, the overlapping height should be equal to first TextView's height -->
<!-- LinearLayout's top margin = ImageView's height - firstTextView's height -->

<!-- AnotherView is an extended LinearLayout that I added to the library -->

    <couk.jenxsol.parallaxscrollview.views.AnotherView
        android:id="@+id/anotherView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <LinearLayout
            android:id="@+id/llMainHolder"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="200dp"
            android:orientation="vertical" >

            <TextView
                android:id="@+id/tvTitle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:gravity="center"
                android:padding="@dimen/spacing"
                android:text="Parallax Effect"
                android:textColor="@android:color/white"
                android:textSize="21sp"
                tools:ignore="NewApi" />

            <!-- ListView -->

            <LinearLayout
                android:id="@+id/llMain"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" >

                <ListView
                    android:id="@+id/lvMain"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:divider="@android:color/black"
                    android:dividerHeight="2px" >

                </ListView>

            </LinearLayout>            

        </LinearLayout>

    </couk.jenxsol.parallaxscrollview.views.AnotherView>

</couk.jenxsol.parallaxscrollview.views.ParallaxScrollView>

Activity code:

public class DemoActivity extends Activity {

    private ParallaxScrollView mScrollView;
    private ListView lvMain;
    private LinearLayout llMain, llMainHolder;
    private AnotherView anotherView;
    private ImageView iv;
    private TextView tvTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);


        // Inflated layout
        View mContent = getLayoutInflater().inflate(R.layout.activity_demo, null);

        // Initialize components

        mScrollView = (ParallaxScrollView) mContent.findViewById(R.id.scroll_view);

        llMain = (LinearLayout) mContent.findViewById(R.id.llMain);

        llMainHolder = (LinearLayout) mContent.findViewById(R.id.llMainHolder);

        lvMain = (ListView) mContent.findViewById(R.id.lvMain);

        iv = (ImageView) mContent.findViewById(R.id.iv);

        tvTitle = (TextView) mContent.findViewById(R.id.tvTitle);

        anotherView = (AnotherView) mContent.findViewById(R.id.anotherView);

        String[] array = {"one", "two", "three", "four", "five", "six", "seven", "eight",
            "nine", "ten", "evelen", "twelve", "thirteen", "fourteen"};

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.text, array);

        lvMain.setAdapter(adapter);            

        // Set Content
        setContentView(mContent);

        lvMain.post(new Runnable() {

            @Override
            public void run() {

                // Adjusts llMain's height to match ListView's height
                setListViewHeight(lvMain, llMain);  

                // LayoutParams to set the top margin of LinearLayout holding
                // the content. 
                // topMargin = iv.getHeight() - tvTitle.getHeight()
                LinearLayout.LayoutParams p = 
                       (LinearLayout.LayoutParams)llMainHolder.getLayoutParams();
                p.topMargin = iv.getHeight() - tvTitle.getHeight(); 
                llMainHolder.setLayoutParams(p);
            }
        });
    }

    // Sets the ListView holder's height    
    public void setListViewHeight(ListView listView, LinearLayout llMain) {
        ListAdapter listAdapter = listView.getAdapter();
        if (listAdapter == null) {

            return;
        }

        int totalHeight = 0;
        int firstHeight = 0;
        int desiredWidth = MeasureSpec.makeMeasureSpec(
                             listView.getWidth(), MeasureSpec.AT_MOST);

        for (int i = 0; i < listAdapter.getCount(); i++) {

            if (i == 0) {
                View listItem = listAdapter.getView(i, null, listView);
                listItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
                firstHeight = listItem.getMeasuredHeight();
        }
            totalHeight += firstHeight; 
        }

        LinearLayout.LayoutParams params = 
                         (LinearLayout.LayoutParams)llMain.getLayoutParams();

        params.height = totalHeight + (listView.getDividerHeight() *
                                                (listAdapter.getCount() - 1));
        llMain.setLayoutParams(params);
        anotherView.requestLayout();    
    }
}

The view provided by the library that holds the content (ObservableScrollView) extends a ScrollView. this was causing problems with the ListView that you want to display. I addedAnotherView that extends a LinearLayout instead:

public class AnotherView extends LinearLayout {

private ScrollCallbacks mCallbacks;

    static interface ScrollCallbacks {
        public void onScrollChanged(int l, int t, int oldl, int oldt);
    }

    public void setCallbacks(ScrollCallbacks listener) {
        mCallbacks = listener;
    }

    public AnotherView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
    } 

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mCallbacks != null) {
            mCallbacks.onScrollChanged(l, t, oldl, oldt);
        }
    }

    @Override
    public int computeVerticalScrollRange() {
        return super.computeVerticalScrollRange();
    }              

}

At last: the library provides the parallax effect. The effect in your video is a reverse parallax effect. To get the desired result, a small change is required in ParallaxScrollView.onLayout(). In place of final int scrollYCenterOffset = -mScrollView.getScrollY(), use final int scrollYCenterOffset = mScrollView.getScrollY().

Modified library: Link.

Demo project: Link.

APK (revised / with ListView): Link.

like image 135
Vikram Avatar answered Oct 07 '22 17:10

Vikram


I made an open source library that dose just what you need - https://github.com/nirhart/ParallaxScroll you can see the example here - https://play.google.com/store/apps/details?id=com.nirhart.parallaxscrollexample

like image 37
Nir Hartmann Avatar answered Oct 07 '22 18:10

Nir Hartmann