Skip to content

Instantly share code, notes, and snippets.

@softlion
Created August 6, 2018 15:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save softlion/27d3548decd1a138e81d1f68de578e8f to your computer and use it in GitHub Desktop.
Save softlion/27d3548decd1a138e81d1f68de578e8f to your computer and use it in GitHub Desktop.
A Parallax header for your RecyclerView using a CoordinatorLayour Behavior (Android Development)
<?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>
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