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 fro
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
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.
It is simply done like this, assuming you have a scrollview containing an imageview that you both have references to:
scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
int top = scrollView.getScrollY(); // Increases when scrolling up ^
int newTop = (int) (top * .5f);
imageFrame.setTranslationY(newTop < 0 ? 0 : newTop);
}
});
This will scroll the imageview upwards at half speed compared to the rest of the scrollview, and also checks that it never scrolls down more than it should (past 0)
You could try to use FadingActionBar to replicate the way Google Play Music handles it's artist headers