Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RecyclerView with custom shaped items

I created a custom shaped imageview. It works fine if you use it in scrollview. But when i tried to use it in a recyclerview there's a strange behavior i observed. Images are not getting draw and show gap (see 1st image) unless you scroll down (see 2nd image). Same thing happens when you scroll up.

I want to know how to avoid these gaps. Could you please point me where i am doing wrong? Thanks for the help.

Initial state or after scrolling up:

Initial state

After scrolling down:

After scrolling down

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;

/**
 * Created by santalu on 7/4/17.
 */

public class DiagonalImageView extends AppCompatImageView {

    public static final int TOP = 0;
    public static final int MIDDLE = 1;
    public static final int BOTTOM = 2;

    private final Path mClipPath = new Path();
    private final Path mLinePath = new Path();

    private final Paint mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    private int mPosition;
    private int mOverlap;
    private int mLineColor;
    private int mLineSize;

    private boolean mMaskEnabled = true;

    public DiagonalImageView(Context context) {
        super(context);
        init(context, null);
    }

    public DiagonalImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        if (attrs == null) {
            return;
        }

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ShowCaseImageView);
        try {
            mPosition = a.getInt(R.styleable.DiagonalImageView_di_position, TOP);
            mOverlap = a.getDimensionPixelSize(R.styleable.DiagonalImageView_di_overlap, 0);
            mLineSize = a.getDimensionPixelSize(R.styleable.DiagonalImageView_di_lineSize, 0);
            mLineColor = a.getColor(R.styleable.DiagonalImageView_di_lineColor, Color.BLACK);

            mLinePaint.setColor(mLineColor);
            mLinePaint.setStyle(Style.STROKE);
            mLinePaint.setStrokeWidth(mLineSize);
        } finally {
            a.recycle();
        }
    }

    public void setPosition(int position, boolean maskEnabled) {
        mMaskEnabled = maskEnabled;
        setPosition(position);
    }

    public void setPosition(int position) {
        if (mPosition != position) {
            mClipPath.reset();
            mLinePath.reset();
        }
        mPosition = position;
    }

    @Override protected void onDraw(Canvas canvas) {
        int saveCount = canvas.getSaveCount();
        canvas.clipPath(mClipPath);
        super.onDraw(canvas);
        canvas.drawPath(mLinePath, mLinePaint);
        canvas.restoreToCount(saveCount);
    }

    @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (!changed) {
            return;
        }

        if (mMaskEnabled && mClipPath.isEmpty()) {
            int width = getMeasuredWidth();
            int height = getMeasuredHeight();

            if (width <= 0 || height <= 0) {
                return;
            }

            switch (mPosition) {
                case TOP:
                    mClipPath.moveTo(0, 0);
                    mClipPath.lineTo(width, 0);
                    mClipPath.lineTo(width, height - mOverlap);
                    mClipPath.lineTo(0, height);

                    mLinePath.moveTo(0, height);
                    mLinePath.lineTo(width, height - mOverlap);
                    break;
                case MIDDLE:
                    mClipPath.moveTo(0, mOverlap);
                    mClipPath.lineTo(width, 0);
                    mClipPath.lineTo(width, height - mOverlap);
                    mClipPath.lineTo(0, height);

                    mLinePath.moveTo(0, height);
                    mLinePath.lineTo(width, height - mOverlap);
                    break;
                case BOTTOM:
                    mClipPath.moveTo(0, mOverlap);
                    mClipPath.lineTo(width, 0);
                    mClipPath.lineTo(width, height);
                    mClipPath.lineTo(0, height);
                    break;
            }
            mClipPath.close();
            mLinePath.close();
        }
    }
}

I include the sample app here to demonstrate the issue if you are interested

import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.santalu.showcaseimageview.ShowCaseImageView;

public class MainActivity extends AppCompatActivity {

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        int overlap = getResources().getDimensionPixelSize(R.dimen.overlap_size);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setHasFixedSize(true);
        recyclerView.addItemDecoration(new OverlapItemDecoration(-overlap));
        recyclerView.setAdapter(new SampleAdapter(this));
    }

    static class SampleAdapter extends RecyclerView.Adapter<SampleAdapter.ViewHolder> {
        private final Context mContext;

        SampleAdapter(Context context) {
            mContext = context;
        }

        @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new ViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item, parent, false));
        }

        @Override public void onBindViewHolder(ViewHolder holder, int position) {
            holder.bind(position);
        }

        @Override public int getItemCount() {
            return 7;
        }

        class ViewHolder extends RecyclerView.ViewHolder {
            DiagonalImageView image;
            //int overlap;

            ViewHolder(View itemView) {
                super(itemView);
                image = (DiagonalImageView) itemView.findViewById(R.id.image);
                //overlap = -mContext.getResources().getDimensionPixelSize(R.dimen.overlap_size);
            }

            void bind(int position) {
                boolean maskEnabled = getItemCount() > 1;
                //MarginLayoutParams params = (MarginLayoutParams) image.getLayoutParams();
                if (position == 0) {
                    image.setPosition(ShowCaseImageView.TOP, maskEnabled);
                    //params.setMargins(0, 0, 0, 0);
                } else if (position == getItemCount() - 1) {
                    image.setPosition(ShowCaseImageView.BOTTOM, maskEnabled);
                    //params.setMargins(0, overlap, 0, 0);
                } else {
                    image.setPosition(ShowCaseImageView.MIDDLE, maskEnabled);
                    //params.setMargins(0, overlap, 0, 0);
                }
                //image.setLayoutParams(params);
            }
        }
    }

    static class OverlapItemDecoration extends RecyclerView.ItemDecoration {
        private int mOverlap;

        OverlapItemDecoration(int overlap) {
            mOverlap = overlap;
        }

        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            if (parent.getChildAdapterPosition(view) != 0) {
                outRect.top = mOverlap;
            }
        }
    }
}

item.xml

<?xml version="1.0" encoding="utf-8"?>
<com.santalu.diagonalimageview.DiagonalImageView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/image"
    android:layout_width="wrap_content"
    android:layout_height="@dimen/image_height"
    android:scaleType="centerCrop"
    android:src="@drawable/demo"
    app:csi_lineColor="@color/deep_orange"
    app:csi_lineSize="@dimen/line_size"
    app:csi_overlap="@dimen/overlap_size"/>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
like image 341
Fatih Santalu Avatar asked Jul 11 '17 07:07

Fatih Santalu


People also ask

What is ViewHolder in RecyclerView Android?

A ViewHolder describes an item view and metadata about its place within the RecyclerView. Adapter implementations should subclass ViewHolder and add fields for caching potentially expensive findViewById results. While LayoutParams belong to the LayoutManager , ViewHolders belong to the adapter.

What is Item decoration in RecyclerView?

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

How many times Oncreateviewholder called?

By default it have 5. you can increase as per your need. Save this answer.


1 Answers

After some research and try i figured out that Path values aren't correct and well designed especially for border. Some cases they are overlapping each other and i thought this causes images not drawn correctly.

I redesigned the view and made some improvements. For future readers here's final code:

/**
 * Created by santalu on 7/4/17.
 *
 * Note: if position set NONE mask won't be applied
 *
 * POSITION    DIRECTION
 *
 * TOP         LEFT |  RIGHT
 * BOTTOM      LEFT |  RIGHT
 * LEFT        TOP  |  BOTTOM
 * RIGHT       TOP  |  BOTTOM
 */

public class DiagonalImageView extends AppCompatImageView {

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

    public static final int NONE = 0;
    public static final int TOP = 1;
    public static final int RIGHT = 2;
    public static final int BOTTOM = 4;
    public static final int LEFT = 8;

    private final Path mClipPath = new Path();
    private final Path mBorderPath = new Path();

    private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    private int mPosition;
    private int mDirection;
    private int mOverlap;
    private int mBorderColor;
    private int mBorderSize;

    private boolean mBorderEnabled;

    public DiagonalImageView(Context context) {
        super(context);
        init(context, null);
    }

    public DiagonalImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        if (attrs == null) {
            return;
        }

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiagonalImageView);
        try {
            mPosition = a.getInteger(R.styleable.DiagonalImageView_di_position, NONE);
            mDirection = a.getInteger(R.styleable.DiagonalImageView_di_direction, RIGHT);
            mOverlap = a.getDimensionPixelSize(R.styleable.DiagonalImageView_di_overlap, 0);
            mBorderSize = a.getDimensionPixelSize(R.styleable.DiagonalImageView_di_borderSize, 0);
            mBorderColor = a.getColor(R.styleable.DiagonalImageView_di_borderColor, Color.BLACK);
            mBorderEnabled = a.getBoolean(R.styleable.DiagonalImageView_di_borderEnabled, false);

            mBorderPaint.setColor(mBorderColor);
            mBorderPaint.setStyle(Style.STROKE);
            mBorderPaint.setStrokeWidth(mBorderSize);
        } finally {
            a.recycle();
        }
    }

    public void set(int position, int direction) {
        if (mPosition != position || mDirection != direction) {
            mClipPath.reset();
            mBorderPath.reset();
        }
        mPosition = position;
        mDirection = direction;
        postInvalidate();
    }

    public void setPosition(int position) {
        if (mPosition != position) {
            mClipPath.reset();
            mBorderPath.reset();
        }
        mPosition = position;
        postInvalidate();
    }

    public void setDirection(int direction) {
        if (mDirection != direction) {
            mClipPath.reset();
            mBorderPath.reset();
        }
        mDirection = direction;
        postInvalidate();
    }

    public void setBorderEnabled(boolean enabled) {
        mBorderEnabled = enabled;
        postInvalidate();
    }

    @Override protected void onDraw(Canvas canvas) {
        if (mClipPath.isEmpty()) {
            super.onDraw(canvas);
            return;
        }

        int saveCount = canvas.save();
        canvas.clipPath(mClipPath);
        super.onDraw(canvas);
        if (!mBorderPath.isEmpty()) {
            canvas.drawPath(mBorderPath, mBorderPaint);
        }
        canvas.restoreToCount(saveCount);
    }

    @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (!changed) {
            return;
        }

        if (mClipPath.isEmpty()) {
            int width = getMeasuredWidth();
            int height = getMeasuredHeight();

            if (width <= 0 || height <= 0) {
                return;
            }

            mClipPath.reset();
            mBorderPath.reset();

            switch (mPosition) {
                case TOP:
                    if (mDirection == LEFT) {
                        mClipPath.moveTo(0, 0);
                        mClipPath.lineTo(width, mOverlap);
                        mClipPath.lineTo(width, height);
                        mClipPath.lineTo(0, height);

                        if (mBorderEnabled) {
                            mBorderPath.moveTo(0, 0);
                            mBorderPath.lineTo(width, mOverlap);
                        }
                    } else {
                        mClipPath.moveTo(0, mOverlap);
                        mClipPath.lineTo(width, 0);
                        mClipPath.lineTo(width, height);
                        mClipPath.lineTo(0, height);

                        if (mBorderEnabled) {
                            mBorderPath.moveTo(0, mOverlap);
                            mBorderPath.lineTo(width, 0);
                        }
                    }
                    break;
                case RIGHT:
                    if (mDirection == TOP) {
                        mClipPath.moveTo(0, 0);
                        mClipPath.lineTo(width, 0);
                        mClipPath.lineTo(width - mOverlap, height);
                        mClipPath.lineTo(0, height);

                        if (mBorderEnabled) {
                            mBorderPath.moveTo(width, 0);
                            mBorderPath.lineTo(width - mOverlap, height);
                        }
                    } else {
                        mClipPath.moveTo(0, 0);
                        mClipPath.lineTo(width - mOverlap, 0);
                        mClipPath.lineTo(width, height);
                        mClipPath.lineTo(0, height);

                        if (mBorderEnabled) {
                            mBorderPath.moveTo(width - mOverlap, 0);
                            mBorderPath.lineTo(width, height);
                        }
                    }
                    break;
                case BOTTOM:
                    if (mDirection == LEFT) {
                        mClipPath.moveTo(0, 0);
                        mClipPath.lineTo(width, 0);
                        mClipPath.lineTo(width, height - mOverlap);
                        mClipPath.lineTo(0, height);

                        if (mBorderEnabled) {
                            mBorderPath.moveTo(0, height);
                            mBorderPath.lineTo(width, height - mOverlap);
                        }
                    } else {
                        mClipPath.moveTo(0, 0);
                        mClipPath.lineTo(width, 0);
                        mClipPath.lineTo(width, height);
                        mClipPath.lineTo(0, height - mOverlap);

                        if (mBorderEnabled) {
                            mBorderPath.moveTo(0, height - mOverlap);
                            mBorderPath.lineTo(width, height);
                        }
                    }
                    break;
                case LEFT:
                    if (mDirection == TOP) {
                        mClipPath.moveTo(0, 0);
                        mClipPath.lineTo(width, 0);
                        mClipPath.lineTo(width, height);
                        mClipPath.lineTo(mOverlap, height);

                        if (mBorderEnabled) {
                            mBorderPath.moveTo(0, 0);
                            mBorderPath.lineTo(mOverlap, height);
                        }
                    } else {
                        mClipPath.moveTo(mOverlap, 0);
                        mClipPath.lineTo(width, 0);
                        mClipPath.lineTo(width, height);
                        mClipPath.lineTo(0, height);

                        if (mBorderEnabled) {
                            mBorderPath.moveTo(mOverlap, 0);
                            mBorderPath.lineTo(0, height);
                        }
                    }
                    break;
            }

            mClipPath.close();
            mBorderPath.close();
        }
    }
}

Update: I published this on Github as a library in case you need it Diagonal ImageView

like image 70
Fatih Santalu Avatar answered Nov 06 '22 02:11

Fatih Santalu