edu.ptu.recyclerviewdemo.stickheader.StickyHeaderHelper.java Source code

Java tutorial

Introduction

Here is the source code for edu.ptu.recyclerviewdemo.stickheader.StickyHeaderHelper.java

Source

/*
 * Copyright 2016 Martin Guillon & Davide Steduto (Hyper-Optimized for FlexibleAdapter project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package edu.ptu.recyclerviewdemo.stickheader;

import android.animation.Animator;
import android.support.annotation.IntRange;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;

import edu.ptu.recyclerviewdemo.R;
import edu.ptu.recyclerviewdemo.Utils;
import edu.ptucustomviewordrawable.LogUtils;

/**
 * A sticky header helper, to use only with {@link HeaderAdapter}.
 * <p>Header ViewHolders must be of type {@link HeaderViewHolder}.</p>
 * <p>
 * header {@link #ensureHeaderParent()}
 *
 * @since 25/03/2016 Created
 */
public class StickyHeaderHelper extends OnScrollListener {

    private static final String TAG = StickyHeaderHelper.class.getSimpleName();

    private HeaderAdapter mAdapter;
    private RecyclerView mRecyclerView;
    private ViewGroup mStickyHolderLayout;
    private HeaderViewHolder mStickyHeaderViewHolder;
    private OnStickyHeaderChangeListener mStickyHeaderChangeListener;
    private int mHeaderPosition = RecyclerView.NO_POSITION;
    private boolean displayWithAnimation = false;
    private float mElevation;

    public StickyHeaderHelper(HeaderAdapter adapter, OnStickyHeaderChangeListener stickyHeaderChangeListener,
            ViewGroup stickyHolderLayout) {
        mAdapter = adapter;
        mStickyHeaderChangeListener = stickyHeaderChangeListener;
        mStickyHolderLayout = stickyHolderLayout;
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        displayWithAnimation = mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
        updateOrClearHeader(false);
    }

    public void attachToRecyclerView(RecyclerView parent) {
        if (mRecyclerView != null) {
            mRecyclerView.removeOnScrollListener(this);
            clearHeader();
        }
        if (parent == null) {
            throw new IllegalStateException(
                    "Adapter is not attached to RecyclerView. Enable sticky headers after setting adapter to RecyclerView.");
        }
        mRecyclerView = parent;
        mRecyclerView.addOnScrollListener(this);
        initStickyHeadersHolder();
    }

    public void detachFromRecyclerView() {
        mRecyclerView.removeOnScrollListener(this);
        mRecyclerView = null;
        clearHeaderWithAnimation();
        //      if (FlexibleAdapter.DEBUG) Log.i(TAG, "StickyHolderLayout detached");
    }

    private FrameLayout createContainer(int width, int height) {
        FrameLayout frameLayout = new FrameLayout(mRecyclerView.getContext());
        frameLayout.setLayoutParams(new ViewGroup.LayoutParams(width, height));
        return frameLayout;
    }

    private ViewGroup getParent(View view) {
        return (ViewGroup) view.getParent();
    }

    private void initStickyHeadersHolder() {
        if (mStickyHolderLayout == null) {
            // Create stickyContainer for shadow elevation
            FrameLayout stickyContainer = createContainer(ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
            ViewGroup oldParentLayout = getParent(mRecyclerView);
            oldParentLayout.addView(stickyContainer);
            // Initialize Holder Layout
            mStickyHolderLayout = stickyContainer;
            LogUtils.logMainInfo("mStickyHolderLayout " + mStickyHolderLayout);
        }
        //      else if (FlexibleAdapter.DEBUG) {
        //         Log.i(TAG, "User defined StickyHolderLayout initialized");
        //      }
        // Show sticky header if exists already
        updateOrClearHeader(false);
    }

    public int getStickyPosition() {
        return mHeaderPosition;
    }

    private boolean hasStickyHeaderTranslated(int position) {
        RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(position);
        return vh != null && (vh.itemView.getX() < 0 || vh.itemView.getY() < 0);
    }

    private void onStickyHeaderChange(int sectionIndex) {
        if (mStickyHeaderChangeListener != null) {
            mStickyHeaderChangeListener.onStickyHeaderChange(sectionIndex);
        }
    }

    public void updateOrClearHeader(boolean updateHeaderContent) {
        if (mAdapter.getItemCount() == 0) {//||!mAdapter.areHeadersShown() || mAdapter.hasSearchText()
            clearHeaderWithAnimation();
            return;
        }
        int firstHeaderPosition = getStickyPosition(RecyclerView.NO_POSITION);
        System.out.println("===> firstHeaderPosition:" + firstHeaderPosition);
        if (firstHeaderPosition >= getHeaderCount()) {
            updateHeader(firstHeaderPosition, updateHeaderContent);
        } else {
            clearHeader();
        }
    }

    public int getHeaderCount() {
        //
        return 0;
    }

    private void updateHeader(int headerPosition, boolean updateHeaderContent) {
        // Check if there is a new header to be sticky
        System.out.println("===> mHeaderPosition" + mHeaderPosition + " headerPosition " + headerPosition);

        if (mHeaderPosition != headerPosition) {
            // #244 - Don't animate if header is already visible at the first layout position
            int firstVisibleItemPosition = Utils.findFirstVisibleItemPosition(mRecyclerView.getLayoutManager());
            // Animate if headers were hidden, but don't if configuration changed (rotation)
            if (displayWithAnimation && mHeaderPosition == RecyclerView.NO_POSITION
                    && headerPosition != firstVisibleItemPosition) {
                displayWithAnimation = false;
                mStickyHolderLayout.setAlpha(0);
                mStickyHolderLayout.animate().alpha(1).start();
                LogUtils.logMainInfo("setAlpha 1");
            } else {
                LogUtils.logViewInfo(mStickyHolderLayout);
                mStickyHolderLayout.setAlpha(1);
                LogUtils.logMainInfo("setAlpha 1");
            }
            mHeaderPosition = headerPosition;
            HeaderViewHolder holder = getHeaderViewHolder(headerPosition);
            System.out.println("===> holder" + holder + " headerPosition " + headerPosition);

            //         if (FlexibleAdapter.DEBUG)
            //            Log.d(TAG, "swapHeader newHeaderPosition=" + mHeaderPosition);
            swapHeader(holder);
        } else if (updateHeaderContent) {
            // #299 - ClassCastException after click on expanded sticky header when AutoCollapse is enabled
            //         mStickyHeaderViewHolder = getHeaderViewHolder(headerPosition);
            //         mStickyHeaderViewHolder.setBackupPosition(headerPosition);
            //         mAdapter.onBindViewHolder(mStickyHeaderViewHolder, headerPosition);
            //         ensureHeaderParent();
            //FIXME: update viewHolder when sticky
        }
        translateHeader();
    }

    private void configureLayoutElevation() {
        // 1. Take elevation from header item layout (most important)
        mElevation = ViewCompat.getElevation(mStickyHeaderViewHolder.getContentView());
        if (mElevation == 0f) {
            // 2. Take elevation settings
            mElevation = mAdapter.getStickyHeaderElevation();
        }
        if (mElevation > 0) {
            // Needed to elevate the view
            ViewCompat.setBackground(mStickyHolderLayout, mStickyHeaderViewHolder.getContentView().getBackground());
        }
    }

    private void translateHeader() {
        // Sticky at zero offset (no translation)
        int headerOffsetX = 0, headerOffsetY = 0;
        // Get calculated elevation
        float elevation = mElevation;

        // Search for the position where the next header item is found and translate the new offset
        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
            final View nextChild = mRecyclerView.getChildAt(i);
            if (nextChild != null) {
                int adapterPos = mRecyclerView.getChildAdapterPosition(nextChild);
                int nextHeaderPosition = getStickyPosition(adapterPos);
                if (mHeaderPosition != nextHeaderPosition) {
                    if (Utils.getOrientation(mRecyclerView.getLayoutManager()) == OrientationHelper.HORIZONTAL) {
                        if (nextChild.getLeft() > 0) {
                            int headerWidth = mStickyHolderLayout.getMeasuredWidth();
                            int nextHeaderOffsetX = nextChild.getLeft() - headerWidth;
                            headerOffsetX = Math.min(nextHeaderOffsetX, 0);
                            // Early remove the elevation/shadow to match with the next view
                            if (nextHeaderOffsetX < 5)
                                elevation = 0f;
                            if (headerOffsetX < 0)
                                break;
                        }
                    } else {
                        if (nextChild.getTop() > 0) {
                            int headerHeight = mStickyHolderLayout.getMeasuredHeight();
                            int nextHeaderOffsetY = nextChild.getTop() - headerHeight + 10;// 
                            headerOffsetY = Math.min(nextHeaderOffsetY, 0);
                            // Early remove the elevation/shadow to match with the next view
                            if (nextHeaderOffsetY < 5)
                                elevation = 0f;
                            if (headerOffsetY < 0)
                                break;
                        }
                    }
                }
            }
        }
        // Apply the user elevation to the sticky container
        ViewCompat.setElevation(mStickyHolderLayout, elevation);
        // Apply translation (pushed up by another header)
        mStickyHolderLayout.setTranslationX(headerOffsetX);
        mStickyHolderLayout.setTranslationY(headerOffsetY);
        //Log.v(TAG, "TranslationX=" + headerOffsetX + " TranslationY=" + headerOffsetY);
    }

    private void swapHeader(HeaderViewHolder newHeader) {
        if (mStickyHeaderViewHolder != null) {
            resetHeader(mStickyHeaderViewHolder);
        }
        mStickyHeaderViewHolder = newHeader;
        if (mStickyHeaderViewHolder != null) {
            mStickyHeaderViewHolder.setIsRecyclable(false);
            ensureHeaderParent();
        }
        onStickyHeaderChange(mHeaderPosition);
    }

    private void ensureHeaderParent() {
        final View view = mStickyHeaderViewHolder.getContentView();
        // #121 - Make sure the measured height (width for horizontal layout) is kept if
        // WRAP_CONTENT has been set for the Header View
        mStickyHeaderViewHolder.itemView.getLayoutParams().width = view.getMeasuredWidth();
        mStickyHeaderViewHolder.itemView.getLayoutParams().height = view.getMeasuredHeight();
        // Ensure the itemView is hidden to avoid double background
        mStickyHeaderViewHolder.itemView.setVisibility(View.INVISIBLE);
        // #139 - Copy xml params instead of Measured params
        ViewGroup.LayoutParams params = mStickyHolderLayout.getLayoutParams();
        params.width = view.getLayoutParams().width;
        params.height = view.getLayoutParams().height - 100;
        removeViewFromParent(view);
        mStickyHolderLayout.addView(view);
        if (mStickyHolderLayout.getChildAt(0) != null) {
            ((ViewGroup) mStickyHolderLayout.getChildAt(0)).getChildAt(1).setVisibility(View.VISIBLE);//FIXME 
            mStickyHolderLayout.invalidate();
        }
        configureLayoutElevation();
    }

    /**
     * On swing and on fast scroll some header items might still be invisible. We need
     * to identify them and restore visibility.
     */
    @SuppressWarnings("unchecked")
    private void restoreHeaderItemVisibility() {
        if (mRecyclerView == null)
            return;
        // Restore every header item visibility
        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
            View oldHeader = mRecyclerView.getChildAt(i);
            int headerPos = mRecyclerView.getChildAdapterPosition(oldHeader) - getHeaderCount();
            if (headerPos < 0) {
                Log.e("restoreH", headerPos + " ");
                continue;
            }
            if (headerPos >= mAdapter.getItemCount()) {
                Log.e("restoreH", headerPos + "");
                return;
            }
            try {
                if (mAdapter.isHeader(mAdapter.getItem(headerPos))) {
                    oldHeader.setVisibility(View.VISIBLE);
                }
            } catch (Exception e) {
                Log.e("restoreH", e.getMessage().toString());
            }
        }
    }

    private void resetHeader(HeaderViewHolder header) {
        restoreHeaderItemVisibility();
        // Clean the header container
        final View view = header.getContentView();
        removeViewFromParent(view);
        // Reset translation on removed header
        view.setTranslationX(0);
        view.setTranslationY(0);
        if (!header.itemView.equals(view))
            ((ViewGroup) header.itemView).addView(view);
        header.setIsRecyclable(true);
        // #294 - Expandable header is not resized / redrawn on automatic configuration change when sticky headers are enabled
        header.itemView.getLayoutParams().width = view.getLayoutParams().width;
        header.itemView.getLayoutParams().height = view.getLayoutParams().height;
    }

    private void clearHeader() {
        if (mStickyHeaderViewHolder != null) {
            //         if (FlexibleAdapter.DEBUG) Log.d(TAG, "clearHeader");
            resetHeader(mStickyHeaderViewHolder);
            mStickyHolderLayout.setAlpha(0);
            mStickyHolderLayout.animate().cancel();
            mStickyHolderLayout.animate().setListener(null);
            mStickyHeaderViewHolder = null;
            restoreHeaderItemVisibility();
            mHeaderPosition = RecyclerView.NO_POSITION;
            onStickyHeaderChange(mHeaderPosition);
        }
    }

    public void clearHeaderWithAnimation() {
        if (mStickyHeaderViewHolder != null && mHeaderPosition != RecyclerView.NO_POSITION) {
            mStickyHolderLayout.animate().setListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
                    mHeaderPosition = RecyclerView.NO_POSITION;
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    displayWithAnimation = true; //This helps after clearing filter
                    mStickyHolderLayout.setAlpha(0);
                    clearHeader();
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                }

                @Override
                public void onAnimationRepeat(Animator animation) {
                }
            });
            mStickyHolderLayout.animate().alpha(0).start();
        }
    }

    private static void removeViewFromParent(final View view) {
        final ViewParent parent = view.getParent();
        if (parent instanceof ViewGroup) {
            ((ViewGroup) parent).removeView(view);
        }
    }

    @SuppressWarnings("unchecked")
    private int getStickyPosition(int adapterPosHere) {

        if (adapterPosHere == RecyclerView.NO_POSITION) {
            adapterPosHere = Utils.findFirstVisibleItemPosition(mRecyclerView.getLayoutManager());
            System.out.println("===> " + adapterPosHere);
            if (adapterPosHere == getHeaderCount() && !hasStickyHeaderTranslated(getHeaderCount())) {
                System.out.println("===> RecyclerView.NO_POSITION");
                return RecyclerView.NO_POSITION;
            }
        }
        IHeader header = mAdapter.getSectionHeader(adapterPosHere - getHeaderCount());
        // Header cannot be sticky if it's also an Expandable in collapsed status, RV will raise an exception
        if (header == null || mAdapter.isExpandable(header) && !mAdapter.isExpanded(header)) {
            return RecyclerView.NO_POSITION;
        }
        return mAdapter.getGlobalPositionOf(header) + getHeaderCount();
    }

    /**
     * Gets the header view for the associated header position. If it doesn't exist yet, it will
     * be created, measured, and laid out.
     *
     * @param position the adapter position to get the header view
     * @return ViewHolder of type ViewHolder of the associated header position
     */
    @SuppressWarnings("unchecked")
    private HeaderViewHolder getHeaderViewHolder(int position) {
        // Find existing ViewHolder
        HeaderViewHolder holder = (HeaderViewHolder) mRecyclerView.findViewHolderForAdapterPosition(position);
        if (holder == null) {
            // Create and binds a new ViewHolder
            holder = (HeaderViewHolder) mRecyclerView.getAdapter().createViewHolder(mRecyclerView,
                    mAdapter.getItemViewType(position - getHeaderCount()));
            mRecyclerView.getAdapter().bindViewHolder(holder, position);
            // Restore the Adapter position
            holder.setBackupPosition(position);

            // Calculate width and height
            int widthSpec;
            int heightSpec;
            if (Utils.getOrientation(mRecyclerView.getLayoutManager()) == OrientationHelper.VERTICAL) {
                widthSpec = View.MeasureSpec.makeMeasureSpec(mRecyclerView.getWidth(), View.MeasureSpec.EXACTLY);
                heightSpec = View.MeasureSpec.makeMeasureSpec(mRecyclerView.getHeight(),
                        View.MeasureSpec.UNSPECIFIED);
            } else {
                widthSpec = View.MeasureSpec.makeMeasureSpec(mRecyclerView.getWidth(),
                        View.MeasureSpec.UNSPECIFIED);
                heightSpec = View.MeasureSpec.makeMeasureSpec(mRecyclerView.getHeight(), View.MeasureSpec.EXACTLY);
            }

            // Measure and Layout the stickyView
            final View headerView = holder.getContentView();
            int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
                    mRecyclerView.getPaddingLeft() + mRecyclerView.getPaddingRight(),
                    headerView.getLayoutParams().width);
            int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
                    mRecyclerView.getPaddingTop() + mRecyclerView.getPaddingBottom(),
                    headerView.getLayoutParams().height);

            headerView.measure(childWidth, childHeight);
            headerView.layout(0, 0, headerView.getMeasuredWidth(), headerView.getMeasuredHeight());
        }
        System.out.println("===> position " + position);
        mRecyclerView.getAdapter().bindViewHolder(holder, position);
        return holder;
    }

    /**
     * @since 05/03/2016
     */
    public interface OnStickyHeaderChangeListener {
        /**
         * Called when the current sticky header changed.
         *
         * @param sectionIndex the position of header, -1 if no header is sticky
         * @since 5.0.0-b1
         */
        void onStickyHeaderChange(int sectionIndex);
    }

    public static abstract class HeaderAdapter extends RecyclerView.Adapter {

        private RecyclerView mRecyclerView;
        private boolean headersShown = true;

        public HeaderAdapter() {
            //            registerAdapterDataObserver(new AdapterDataObserver()); //????
        }

        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            mRecyclerView = recyclerView;
        }

        public float getStickyHeaderElevation() {
            return 0;
        }

        /**
         * @param item recyclerview ?
         * @return
         */
        public abstract boolean isHeader(Object item);

        public abstract Object getItem(int headerPos);

        public IHeader getSectionHeader(int position) {
            // When headers are visible and sticky, get the previous header
            for (int i = position; i >= 0; i--) {
                if (i >= getItemCount())//FIXME 
                    continue;
                Object item = getItem(i);
                if (isHeader(item))
                    return (IHeader) item;
            }
            return null;
        }

        public abstract boolean isExpandable(IHeader header);// is Group

        public abstract int getGlobalPositionOf(IHeader header);//get FlatList position

        public abstract boolean isExpanded(IHeader header);

        public abstract void colloOrExpandGroup(int groupposiziton);

        public RecyclerView getRecyclerView() {
            return mRecyclerView;
        }

        public boolean isExpanded(@IntRange(from = 0) int position) {
            return isExpanded((IHeader) getItem(position));
        }
    }

    public static interface IHeader {

    }

    public static abstract class HeaderViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        private RecyclerView recyclerview;
        private HeaderAdapter mAdapter;
        private int mBackupPosition = RecyclerView.NO_POSITION;
        private View contentView;

        public HeaderViewHolder(View view, StickyHeaderHelper.HeaderAdapter adapter, RecyclerView recyclerView) {
            // Since itemView is declared "final", the split is done before the View is initialized
            super(new FrameLayout(view.getContext()));
            this.mAdapter = adapter;
            itemView.setLayoutParams(recyclerView.getLayoutManager().generateLayoutParams(view.getLayoutParams()));
            ((FrameLayout) itemView).addView(view); //Add View after setLayoutParams
            float elevation = ViewCompat.getElevation(view);
            if (elevation > 0) {
                ViewCompat.setBackground(itemView, view.getBackground());
                ViewCompat.setElevation(itemView, elevation);
            }
            contentView = view;
            getContentView().setOnClickListener(this);
            this.recyclerview = recyclerView;
        }

        public HeaderViewHolder(View view, HeaderAdapter adapter) {
            // Since itemView is declared "final", the split is done before the View is initialized
            super(new FrameLayout(view.getContext()));
            this.mAdapter = adapter;
            itemView.setLayoutParams(
                    adapter.getRecyclerView().getLayoutManager().generateLayoutParams(view.getLayoutParams()));
            ((FrameLayout) itemView).addView(view); //Add View after setLayoutParams
            float elevation = ViewCompat.getElevation(view);
            if (elevation > 0) {
                ViewCompat.setBackground(itemView, view.getBackground());
                ViewCompat.setElevation(itemView, elevation);
            }
            contentView = view;
            getContentView().setOnClickListener(this);
        }

        public View getContentView() {
            return contentView;
        }

        public void setBackupPosition(int backupPosition) {
            this.mBackupPosition = backupPosition;
        }

        @Override
        public void onClick(View v) {
            int position = getFlexibleAdapterPosition();
            mAdapter.colloOrExpandGroup(position - getRecyclerHeaderCount());
        }

        public final int getFlexibleAdapterPosition() {
            int position = getAdapterPosition();
            if (position == RecyclerView.NO_POSITION) {
                position = mBackupPosition;
            }
            return position;
        }

        public int getRecyclerHeaderCount() {
            return 0;
        }
    }
}