homj
2/14/2015 - 7:19 PM

A ItemDecoration to center the contents of a RecyclerView. This comes in handy when you want to build a responsive UI.

A ItemDecoration to center the contents of a RecyclerView. This comes in handy when you want to build a responsive UI.

/*
 * Copyright 2015 Johannes Homeier
 *
 * 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 de.twoid.ui.decoration;

import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;

/**
 * A {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} backed by a {@link android.support.v7.widget.StaggeredGridLayoutManager}
 */
public class StaggeredGridSpanSizeLookupHelper extends InsetItemDecoration.SpanSizeLookupHelper<StaggeredGridLayoutManager> {

    protected StaggeredGridSpanSizeLookupHelper(StaggeredGridLayoutManager layoutManager) {
        super(layoutManager);
    }

    @Override
    public int getSpanCount() {
        return layoutManager.getSpanCount();
    }

    @Override
    public int getSpanIndex(int position) {
        StaggeredGridLayoutManager.LayoutParams params = getLayoutParams(position);
        return params.getSpanIndex();
    }

    @Override
    public int getSpanSize(int position) {
        StaggeredGridLayoutManager.LayoutParams params = getLayoutParams(position);
        return params.isFullSpan() ? getSpanCount() : 1;
    }

    @Override
    public int getOrientation() {
        return layoutManager.getOrientation();
    }

    private StaggeredGridLayoutManager.LayoutParams getLayoutParams(int position){
        View view = layoutManager.getChildAt(position);
        return (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
    }
}
/*
 * Copyright 2015 Johannes Homeier
 *
 * 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 de.twoid.ui.decoration;

import android.support.v7.widget.LinearLayoutManager;

/**
 * A {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} backed by a {@link android.support.v7.widget.LinearLayoutManager}
 */
public class LinearSpanSizeLookupHelper extends InsetItemDecoration.SpanSizeLookupHelper<LinearLayoutManager> {

    public LinearSpanSizeLookupHelper(LinearLayoutManager layoutManager) {
        super(layoutManager);
    }

    @Override
    public int getSpanCount() {
        return 1;
    }

    /**
     * As the {@link android.support.v7.widget.LinearLayoutManager} only has one column, this method always returns 0;
     *
     * {@inheritDoc}
     */
    @Override
    public int getSpanIndex(int position) {
        return 0;
    }

    /**
     * As the {@link android.support.v7.widget.LinearLayoutManager} only has one column, this method always returns 1;
     *
     * {@inheritDoc}
     */
    @Override
    public int getSpanSize(int position) {
        return 1;
    }

    @Override
    public int getOrientation() {
        return layoutManager.getOrientation();
    }
}
/*
 * Copyright 2015 Johannes Homeier
 *
 * 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 de.twoid.ui.decoration;

import android.graphics.Rect;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
import android.widget.ImageView;

/**
 * An ItemDecoration to center the content of a RecyclerView.
 * This class comes in handy when you want to build a responsive UI while still receiving touch- and scroll-events on the "borders" of the {@link android.support.v7.widget.RecyclerView}
 */
public class InsetItemDecoration extends RecyclerView.ItemDecoration {
    protected SpanSizeLookupHelper spanSizeLookupHelper;
    private float maxContentSize;
    private float parentSize;
    private boolean setup = false;

    /**
     * Create a new InsetItemDecoration without specifying a LayoutManager;
     * The needed {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} will be generated during the first call of {@link #getItemOffsets(android.graphics.Rect, android.view.View, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State)}
     * @param maxContentSize the desired max-dimension of the content in pixel
     */
    public InsetItemDecoration(float maxContentSize) {
        this.maxContentSize = maxContentSize;
    }

    /**
     * Create a new InsetItemDecoration by creating a new {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} with the passed {@link android.support.v7.widget.GridLayoutManager}
     * @param layoutManager the associated GridLayoutManager
     * @param maxContentSize the desired max-dimension of the content in pixel
     */
    public InsetItemDecoration(GridLayoutManager layoutManager, float maxContentSize) {
        this(new GridSpanSizeLookupHelper(layoutManager), maxContentSize);
    }

    /**
     * Create a new InsetItemDecoration by creating a new {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} with the passed {@link android.support.v7.widget.LinearLayoutManager}
     * @param layoutManager the associated LinearLayoutManager
     * @param maxContentSize the desired max-dimension of the content in pixel
     */
    public InsetItemDecoration(LinearLayoutManager layoutManager, float maxContentSize) {
        this(new LinearSpanSizeLookupHelper(layoutManager), maxContentSize);
    }

    /**
     * Create a new InsetItemDecoration by creating a new {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} with the passed {@link android.support.v7.widget.StaggeredGridLayoutManager}
     * @param layoutManager the associated StaggeredGridLayoutManager
     * @param maxContentSize the desired max-dimension of the content in pixel
     */
    public InsetItemDecoration(StaggeredGridLayoutManager layoutManager, float maxContentSize) {
        this(new StaggeredGridSpanSizeLookupHelper(layoutManager), maxContentSize);
    }

    /**
     * Create a new InsetItemDecoration with the passed {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper}
     * @param spanSizeLookupHelper the SpanSizeLookupHelper to work with
     * @param maxContentSize the desired max-dimension of the content in pixel
     */
    public InsetItemDecoration(SpanSizeLookupHelper spanSizeLookupHelper, float maxContentSize) {
        this.spanSizeLookupHelper = spanSizeLookupHelper;
        this.maxContentSize = maxContentSize;
        setup = true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if(!setup){
            setup = generateSpanSizeLookupHelper(parent.getLayoutManager());

            if(!setup){
                return;
            }
        }

        int position = spanSizeLookupHelper.getPosition(view);
        int spanSize = spanSizeLookupHelper.getSpanSize(position);
        int spanIndex = spanSizeLookupHelper.getSpanIndex(position);

        if(spanSizeLookupHelper.getOrientation() == OrientationHelper.VERTICAL){
            parentSize = parent.getWidth();
        }else{
            parentSize = parent.getHeight();
        }

        if(parentSize <= maxContentSize){
            super.getItemOffsets(outRect, view, parent, state);
            return;
        }

        inset(outRect, spanIndex, spanSize);
    }

    /**
     * Generates a new {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} if the passed {@link android.support.v7.widget.RecyclerView.LayoutManager}
     * is either a {@link android.support.v7.widget.LinearLayoutManager}, a {@link android.support.v7.widget.GridLayoutManager}, or a {@link android.support.v7.widget.StaggeredGridLayoutManager}
     * @param layoutManager
     * @return allways true;
     */
    protected boolean generateSpanSizeLookupHelper(RecyclerView.LayoutManager layoutManager){
        if(layoutManager instanceof LinearLayoutManager){
            spanSizeLookupHelper = new LinearSpanSizeLookupHelper((LinearLayoutManager) layoutManager);
        }else if(layoutManager instanceof GridLayoutManager){
            spanSizeLookupHelper = new GridSpanSizeLookupHelper((GridLayoutManager) layoutManager);
        }else if(layoutManager instanceof StaggeredGridLayoutManager){
            spanSizeLookupHelper = new StaggeredGridSpanSizeLookupHelper((StaggeredGridLayoutManager) layoutManager);
        }else{
            throw new UnsupportedLayoutManagerException();
        }

        return true;
    }

    /**
     * Applies a inset to the passed Rect depending on the spanIndex and spanSize;
     * @param outRect the {@link android.graphics.Rect} to modify
     * @param spanIndex the span-index of a view
     * @param spanSize the span-size of a view
     */
    protected void inset(Rect outRect, int spanIndex, int spanSize) {
        final float totalSizeDifference = getTotalSizeDifference();
        final int baseInset = (int) (totalSizeDifference / 2f);

        if(spanSizeLookupHelper.getOrientation() == OrientationHelper.VERTICAL){
            outRect.set(
                    (int) (baseInset - spanSizeLookupHelper.getSpanPositionDifference(spanIndex, totalSizeDifference))
                    , 0
                    , (int) -(baseInset - spanSizeLookupHelper.getSpanPositionDifference(spanIndex + spanSize, totalSizeDifference))
                    , 0);
        }else{
            outRect.set(
                    0
                    , (int) (baseInset - spanSizeLookupHelper.getSpanPositionDifference(spanIndex, totalSizeDifference))
                    , 0
                    , (int) (baseInset - spanSizeLookupHelper.getSpanPositionDifference(spanIndex + spanSize, totalSizeDifference)));
        }
    }

    /**
     * Returns the sizeDifference between the size of the parent {@link android.support.v7.widget.RecyclerView} and the passed maxContentSize
     * @return 0 if maxContentSize is bigger than the size of the RecyclerView, else the difference of both sizes
     */
    protected float getTotalSizeDifference(){
        return Math.max(0, parentSize - maxContentSize);
    }

    /**
     * @return the max size of the RecyclerViews content in pixels
     */
    protected float getMaxContentSize() {
        return maxContentSize;
    }

    /**
     * @return the size of the RecyclerView
     */
    protected float getParentSize() {
        return parentSize;
    }

    /**
     * Indicates whether the {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} has been created or not
     * @return true if the SpanSizeLookupHelper got created
     */
    protected boolean isSetup(){
        return setup;
    }

    /**
     * Invalidate the current {@link android.support.v7.widget.RecyclerView.LayoutManager} (and with that the {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper})
     * and look far a new one in the next call of {@link #getItemOffsets(android.graphics.Rect, android.view.View, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State)}
     * This has to be called when you set a new {@link android.support.v7.widget.RecyclerView.LayoutManager} to the {@link android.support.v7.widget.RecyclerView}
     */
    public void invalidateLayoutManager(){
        setup = false;
    }

    public void swapSpanSizeLookupHeler(SpanSizeLookupHelper spanSizeLookupHelper){
        if(spanSizeLookupHelper == null){
            setup = false;
        }

        this.spanSizeLookupHelper = spanSizeLookupHelper;
    }

    /**
     * A helper-class to provide span-information to the {@link de.twoid.ui.decoration.InsetItemDecoration}
     * @param <E>
     */
    public static abstract class SpanSizeLookupHelper<E extends RecyclerView.LayoutManager>{
        protected E layoutManager;
        protected int spanCount;

        protected SpanSizeLookupHelper(E layoutManager, int spanCount) {
            this.layoutManager = layoutManager;
            this.spanCount = spanCount;
        }

        /**
         * @param view
         * @return the position of the view in the {@link android.support.v7.widget.RecyclerView.LayoutManager}
         */
        public int getPosition(View view){
            return layoutManager.getPosition(view);
        }

        /**
         * @return the span-count of the {@link android.support.v7.widget.RecyclerView.LayoutManager}
         */
        public abstract int getSpanCount();

        /**
         * @param position
         * @return the span-index for a view at the passed position
         */
        public abstract int getSpanIndex(int position);

        /**
         * @param position
         * @return the span-size for a view at the passed position
         */
        public abstract int getSpanSize(int position);

        /**
         * Generates a offset for a span-index;
         * This offset is used to inset and shrink a view according to its span-index and span-size
         * @param spanIndex
         * @param sizeDifference
         * @return the offset for the passed spanIndex in pixels
         */
        public float getSpanPositionDifference(int spanIndex, float sizeDifference){
            if(sizeDifference <= 0){
                return 0;
            }else{
                return spanIndex * sizeDifference / ((float) getSpanCount());
            }
        }

        /**
         * @return the orientation of the {@link android.support.v7.widget.RecyclerView.LayoutManager}
         */
        public abstract int getOrientation();
    }

    public static class UnsupportedLayoutManagerException extends ClassCastException{
        private static final String ADDITIONAL_MESSAGE =
                InsetItemDecoration.class.getName()+" only supports "
                        + LinearLayoutManager.class.getName()
                        + ", " + GridLayoutManager.class.getName()
                        + " and " + StaggeredGridLayoutManager.class.getName()
                        + ". If you want to use a custom LayoutManager, you should create a new SpanSizeLookupHelper and pass it in the constructor";

        public UnsupportedLayoutManagerException() {
            super(ADDITIONAL_MESSAGE);
        }
    }
}
/*
 * Copyright 2015 Johannes Homeier
 *
 * 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 de.twoid.ui.decoration;

import android.support.v7.widget.GridLayoutManager;

/**
 * A {@link de.twoid.ui.decoration.InsetItemDecoration.SpanSizeLookupHelper} backed by a {@link android.support.v7.widget.GridLayoutManager}
 */
public class GridSpanSizeLookupHelper extends InsetItemDecoration.SpanSizeLookupHelper<GridLayoutManager> {
    private GridLayoutManager.SpanSizeLookup spanSizeLookup;

    public GridSpanSizeLookupHelper(GridLayoutManager layoutManager) {
        super(layoutManager);
        spanSizeLookup = layoutManager.getSpanSizeLookup();
        spanSizeLookup.setSpanIndexCacheEnabled(true);
    }

    @Override
    public int getSpanCount() {
        return layoutManager.getSpanCount();
    }

    @Override
    public int getSpanIndex(int position){
        return spanSizeLookup.getSpanIndex(position, getSpanCount());
    }

    @Override
    public int getSpanSize(int position) {
        return spanSizeLookup.getSpanSize(position);
    }

    @Override
    public int getOrientation() {
        return layoutManager.getOrientation();
    }
}