Created
August 6, 2018 15:33
-
-
Save softlion/27d3548decd1a138e81d1f68de578e8f to your computer and use it in GitHub Desktop.
A Parallax header for your RecyclerView using a CoordinatorLayour Behavior (Android Development)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<android.support.constraint.ConstraintLayout | |
xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:local="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
> | |
<!-- ... --> | |
<android.support.design.widget.CoordinatorLayout | |
android:layout_width="match_parent" | |
android:layout_height="0dp" | |
app:layout_constraintTop_toTopOf="parent" | |
app:layout_constraintBottom_toBottomOf="parent" | |
android:background="@color/accent" | |
> | |
<android.support.v4.widget.SwipeRefreshLayout | |
android:id="@+id/swipeRefresh" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:clipToPadding="false" | |
> | |
<android.support.v7.widget.RecyclerView | |
android:clipToPadding="false" | |
android:id="@+id/list" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:scrollbars="none" | |
android:divider="@color/accent" | |
android:animateLayoutChanges="false" | |
/> | |
</android.support.v4.widget.SwipeRefreshLayout> | |
<include | |
app:layout_behavior="vapolia.ParallaxHeaderBehavior" | |
android:id="@+id/collapsibleHeader" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
app:layout_anchor="@+id/swipeRefresh" | |
app:layout_anchorGravity="top" | |
android:layout_gravity="top" | |
layout="@layout/your_header" /> | |
</android.support.design.widget.CoordinatorLayout> | |
<!-- ... --> | |
</android.support.constraint.ConstraintLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using Android.Content; | |
using Android.Runtime; | |
using Android.Support.Design.Widget; | |
using Android.Support.V4.View; | |
using Android.Support.V4.Widget; | |
using Android.Support.V7.Widget; | |
using Android.Util; | |
using Android.Views; | |
using Object = Java.Lang.Object; | |
using R = YourAndroidAppNamespace.Resource; | |
namespace Vapolia.Lib.Ui | |
{ | |
/// <summary> | |
/// Thanks to https://medium.com/@zoha131/coordinatorlayout-behavior-basic-fd9c10d3c6e3 | |
/// </summary> | |
[Android.Runtime.Preserve] | |
[Register("vapolia.ParallaxHeaderBehavior")] | |
public class ParallaxHeaderBehavior : CoordinatorLayout.Behavior | |
{ | |
private float friction = 0.4f; | |
public ParallaxHeaderBehavior() | |
{ | |
} | |
[Android.Runtime.Preserve] | |
public ParallaxHeaderBehavior(Context context, IAttributeSet attrs) : base(context, attrs) | |
{ | |
} | |
[Android.Runtime.Preserve] | |
protected ParallaxHeaderBehavior(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) | |
{ | |
} | |
/// <summary> | |
/// Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll. | |
/// Any Behavior associated with any direct child of the CoordinatorLayout may respond to this event and return true to indicate that the CoordinatorLayout should act as a nested scrolling parent for this scroll. | |
/// Only Behaviors that return true from this method will receive subsequent nested scroll events. | |
/// </summary> | |
/// <param name="coordinatorLayout">the CoordinatorLayout parent of the view this Behavior is associated with</param> | |
/// <param name="child">the child view of the CoordinatorLayout this Behavior is associated with</param> | |
/// <param name="directTargetChild">the child view of the CoordinatorLayout that either is or contains the target of the nested scroll operation</param> | |
/// <param name="target">the descendant view of the CoordinatorLayout initiating the nested scroll</param> | |
/// <param name="axes">the axes that this nested scroll applies to. See SCROLL_AXIS_HORIZONTAL (=1), SCROLL_AXIS_VERTICAL (=2)</param> | |
/// <param name="type">the type of input which cause this scroll event</param> | |
/// <returns>true to receive further scroll events in that direction.</returns> | |
/// <remarks> | |
/// We need only vertical axis scroll notifications | |
/// </remarks> | |
public override bool OnStartNestedScroll(CoordinatorLayout coordinatorLayout, Object child, View directTargetChild, View target, int axes, int type) | |
{ | |
return (axes & ViewCompat.ScrollAxisVertical) != 0; | |
} | |
/// <summary> | |
/// onNestedPreScroll is called each time the nested scroll is updated by the nested scrolling child, before the nested scrolling child has consumed the scroll distance itself. | |
/// Each Behavior responding to the nested scroll will receive the same values. | |
/// The CoordinatorLayout will report as consumed the maximum number of pixels in either direction that any Behavior responding to the nested scroll reported as consumed. | |
/// </summary> | |
/// <param name="coordinatorLayout">the CoordinatorLayout parent of the view this Behavior is associated with</param> | |
/// <param name="child">the child view of the CoordinatorLayout this Behavior is associated with</param> | |
/// <param name="target">the descendant view of the CoordinatorLayout performing the nested scroll</param> | |
/// <param name="dx">the raw horizontal number of pixels that the user attempted to scroll</param> | |
/// <param name="dy">the raw vertical number of pixels that the user attempted to scroll</param> | |
/// <param name="consumed">out parameter. consumed[0] should be set to the distance of dx that was consumed, consumed[1] should be set to the distance of dy that was consumed</param> | |
/// <param name="type">the type of input which cause this scroll event</param> | |
/// <remarks> | |
/// when scrolling up: scroll ourself before the list + parallax effect | |
/// when scrolling down: scroll ourself after the list + parallax effect | |
/// </remarks> | |
public override void OnNestedPreScroll(CoordinatorLayout coordinatorLayout, Object child, View target, int dx, int dy, int[] consumed, int type) | |
{ | |
var childView = (View) child; | |
var currentHeightRemoved = (int)-childView.TranslationY; | |
var maxHeightRemoved = childView.Height - childView.MinimumHeight; | |
var minHeightRemoved = 0; | |
dy = (int)Math.Ceiling(friction * dy); | |
if (dy > 0) | |
{ | |
//scrolling up | |
//Scroll before the list + parallax effect | |
//If view is at min height, no scroll | |
if(currentHeightRemoved >= maxHeightRemoved) | |
return; | |
//Otherwise scroll but with max at min height | |
var newHeightRemoved = Math.Min(maxHeightRemoved, currentHeightRemoved+dy); | |
if(newHeightRemoved == currentHeightRemoved) | |
return; | |
consumed[1] = newHeightRemoved - currentHeightRemoved; | |
childView.TranslationY = -newHeightRemoved; | |
} | |
else //dy<=0 | |
{ | |
//scrolling down | |
//Scroll after the list + parallax effect | |
var recyclerView = (RecyclerView)coordinatorLayout.FindViewById(R.Id.list); | |
var listTop = recyclerView.ComputeVerticalScrollOffset(); | |
if (listTop > childView.Height/friction) | |
return; | |
//Si au max height pas de scroll | |
if(currentHeightRemoved <= minHeightRemoved) | |
return; | |
//Sinon scroll mais max au max height | |
var newHeightRemoved = Math.Max(minHeightRemoved, currentHeightRemoved+dy); | |
if(newHeightRemoved == currentHeightRemoved) | |
return; | |
consumed[1] = currentHeightRemoved - newHeightRemoved; | |
childView.TranslationY = -newHeightRemoved; | |
} | |
} | |
#region Fling handling | |
private bool hasFling; | |
public override bool OnNestedFling(CoordinatorLayout coordinatorLayout, Object child, View target, float velocityX, float velocityY, bool consumed) | |
{ | |
hasFling = true; | |
return false; | |
} | |
public override void OnNestedScroll(CoordinatorLayout coordinatorLayout, Object child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) | |
{ | |
if (dyUnconsumed < 0 && hasFling) | |
{ | |
//When flinging up, OnNestedPreScroll is not called (why?). At the end of fling, OnNestedScroll is called with some dyUnconsumed. | |
//If the scrolling view is scrolling but not consuming, then consume some of it until we're fed. | |
//In this case, target is the RecyclerView, not the SwipeRefreshLayout | |
var consumed = new int[2]; | |
OnNestedPreScroll(coordinatorLayout, child, target, dxUnconsumed, dyUnconsumed, consumed, type); | |
if (consumed[1] == 0) | |
hasFling = false; //We're fed | |
} | |
else | |
hasFling = false; | |
} | |
#endregion | |
/// <summary> | |
/// As layout_anchorGravity is ignored (why?), set top padding on the recyclerView to leave space for the parallax header. | |
/// </summary> | |
/// <param name="parent"></param> | |
/// <param name="child"></param> | |
/// <param name="layoutDirection"></param> | |
/// <returns>true if child has been layout</returns> | |
public override bool OnLayoutChild(CoordinatorLayout parent, Object child, int layoutDirection) | |
{ | |
if (((View)child).Id != R.Id.collapsibleHeader) | |
return false; | |
parent.OnLayoutChild((View)child, layoutDirection); | |
var headerHeight = ((View)child).MeasuredHeight; | |
var recyclerView = (RecyclerView)parent.FindViewById(R.Id.list); | |
if (recyclerView.PaddingTop != headerHeight) | |
{ | |
var yOffsetOld = recyclerView.ComputeVerticalScrollOffset(); | |
recyclerView.SetPadding(recyclerView.PaddingLeft, headerHeight, recyclerView.PaddingRight, recyclerView.PaddingBottom); | |
var yOffsetNew = recyclerView.ComputeVerticalScrollOffset(); | |
recyclerView.OffsetChildrenVertical(yOffsetNew-yOffsetOld); | |
//The swipeRefresh's loading animation is displayed behind the header, as it don't know about the recyclerView's paddingTop. Move it down a little. | |
var swipeRefresh = (SwipeRefreshLayout)parent.FindViewById(R.Id.swipeRefresh); | |
swipeRefresh.SetProgressViewOffset(true, headerHeight+(int)DpPxUtils.DpToPx(0), headerHeight+(int)DpPxUtils.DpToPx(40)); | |
} | |
return true; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment