Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Headers onClick #38

Open
amirlatifi opened this issue Jun 5, 2016 · 9 comments
Open

Headers onClick #38

amirlatifi opened this issue Jun 5, 2016 · 9 comments

Comments

@amirlatifi
Copy link

How can I add onClick to views in the header? The findHeaderViewUnder seems doesn't work most of the time even in your sample project. Specially after scrolling and having headers overlapped.

@ebarrenechea
Copy link
Owner

@amirlatifi The code in the sample is very proof-of-concept and only to show the approach you need to implement in order to detect click on the header views. Implementing the RecyclerView.OnItemTouchListener is not a trivial task and you can have a better look at what you need by checking out the twoway-view library.

Please, feel free to contribute a better implementation or even a different solution if you'd like. I can guarantee it will be most welcome. :)

@rafipanoyan
Copy link

Is it for the onClick purpose that you translate the header view where it will be drawn ? (https://github.com/edubarr/header-decor/blob/master/lib/src/main/java/ca/barrenechea/widget/recyclerview/decoration/StickyHeaderDecoration.java#L155)

I can see the library working without this translation, what's his purpose ?

Thanks you

@ebarrenechea
Copy link
Owner

@rafoufoun yes, translation is only there to support onClick on the header view. Without the translation, the views would be drawn at the correct place, but the actual view boundaries used for click detection would be somewhere else.

@rafipanoyan
Copy link

I tried to get a button as a header the way your library does but the onclick listener was never called. It seems to me that using an item decorator is more appropriate for only drawing than getting views with interactions

@IlyaMyasoedov
Copy link

IlyaMyasoedov commented Feb 7, 2017

I have found the solution. You have to add touchlistener like this.

...
decoration = new StickyHeaderDecoration(adapter);
recyclerView.addOnItemTouchListener(new StickyHeadersTouchListener(recyclerView, decoration))
...

code for StickyHeadersTouchListener:

public class StickyHeadersTouchListener implements RecyclerView.OnItemTouchListener{
    private final GestureDetector mTapDetector;
    private final RecyclerView mRecyclerView;
    private final StickyHeaderDecoration mDecor;
    private OnHeaderClickListener mOnHeaderClickListener;

    public interface OnHeaderClickListener {
        void onHeaderClick(View header, int position, long headerId);
    }

    public StickyHeadersTouchListener(final RecyclerView recyclerView,
                                      final StickyHeaderDecoration decor) {
        mTapDetector = new GestureDetector(recyclerView.getContext(), new SingleTapDetector());
        mRecyclerView = recyclerView;
        mDecor = decor;
    }

    public StickyHeaderAdapter getAdapter() {
        if (mRecyclerView.getAdapter() instanceof StickyHeaderAdapter) {
            return (StickyHeaderAdapter) mRecyclerView.getAdapter();
        } else {
            throw new IllegalStateException("A RecyclerView with " +
                    StickyHeadersTouchListener.class.getSimpleName() +
                    " requires a " + StickyHeaderAdapter.class.getSimpleName());
        }
    }


    public void setOnHeaderClickListener(OnHeaderClickListener listener) {
        mOnHeaderClickListener = listener;
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
        boolean tapDetectorResponse = this.mTapDetector.onTouchEvent(e);
        if (tapDetectorResponse) {
            // Don't return false if a single tap is detected
            return true;
        }
        if (e.getAction() == MotionEvent.ACTION_DOWN) {
            Pair<Integer, View> headerPos = mDecor.findHeaderPositionUnder((int)e.getX(), (int)e.getY());
            int position = headerPos.first;
            return position != -1;
        }

        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView view, MotionEvent e) {  }

    @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        // do nothing
    }

    private class SingleTapDetector extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Pair<Integer, View> headerPos = mDecor.findHeaderPositionUnder((int)e.getX(), (int)e.getY());
            int position = headerPos.first;
            if (position != -1) {
                View headerView = headerPos.second;
                performClick(headerView, e);
                long headerId = getAdapter().getHeaderId(position);
                if (mOnHeaderClickListener != null) mOnHeaderClickListener.onHeaderClick(headerView, position, headerId);
                mRecyclerView.playSoundEffect(SoundEffectConstants.CLICK);
                headerView.onTouchEvent(e);
                return true;
            }
            return false;
        }

        private void performClick(View view, MotionEvent e) {
            if (view instanceof ViewGroup) {
                ViewGroup viewGroup = (ViewGroup) view;
                for (int i = 0; i < viewGroup.getChildCount(); i++) {
                    View child = viewGroup.getChildAt(i);
                    performClick(child, e);
                }
            }

            containsBounds(view, e);
        }

        private View containsBounds(View view, MotionEvent e) {
            int x = (int) e.getX();
            int y = (int) e.getY();
            Rect rect = new Rect();
            view.getHitRect(rect);
            if (view.getVisibility() == View.VISIBLE
                    && view.dispatchTouchEvent(e)
                    && rect.left < rect.right && rect.top < rect.bottom && x >= rect.left && x < rect.right && y >= rect.top) {
                view.performClick();
                return view;
            }
            return null;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            return true;
        }
    }
}

@fengdexunmi
Copy link

@IlyaMyasoedov hi,where can I find the method findHeaderPositionUnder

@IlyaMyasoedov
Copy link

IlyaMyasoedov commented Nov 15, 2017

`public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
    public static final long NO_HEADER_ID = -1L;

    private Map<Long, RecyclerView.ViewHolder> mHeaderCache;

    private StickyHeaderAdapter mAdapter;

    private boolean mRenderInline;

    private int headersMargin = 0;


    /**
     * @param adapter
     *         the sticky header adapter to use
     */
    public StickyHeaderDecoration(StickyHeaderAdapter adapter) {
        this(adapter, false);
    }


    /**
     * @param adapter
     *         the sticky header adapter to use
     */
    public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline) {
        mAdapter = adapter;
        mHeaderCache = new LinkedHashMap<>();
        mRenderInline = renderInline;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view);
        int headerHeight = 0;

        if (position != RecyclerView.NO_POSITION
                && hasHeader(position)
                && showHeaderAboveItem(position)) {

            View header = getHeader(parent, position).itemView;
            headerHeight = getHeaderHeightForLayout(header);
        }

        outRect.set(0, headerHeight, 0, 0);
    }

    private boolean showHeaderAboveItem(int itemAdapterPosition) {
        if (itemAdapterPosition == 0) {
            return true;
        }
        return mAdapter.getHeaderId(itemAdapterPosition - 1) != mAdapter.getHeaderId(itemAdapterPosition);
    }

    public void setHeadersMargin(int headersMargin){
        this.headersMargin = headersMargin;
    }

    /**
     * Clears the header view cache. Headers will be recreated and
     * rebound on list scroll after this method has been called.
     */
    public void clearHeaderCache() {
        mHeaderCache.clear();
    }


    /**
     * Gets the position of the header under the specified (x, y) coordinates.
     *
     * @param x x-coordinate
     * @param y y-coordinate
     * @return position of header, or -1 if not found
     */
    public Pair<Integer, View> findHeaderPositionUnder(float x, float y){
        for (RecyclerView.ViewHolder holder : mHeaderCache.values()) {
            final View child = holder.itemView;
            final float translationX = ViewCompat.getTranslationX(child);
            final float translationY = ViewCompat.getTranslationY(child);

            if (x >= child.getLeft() + translationX &&
                    x <= child.getRight() + translationX &&
                    y >= child.getTop() + translationY &&
                    y <= child.getBottom() + translationY) {
                List<RecyclerView.ViewHolder> items = new ArrayList<>(mHeaderCache.values());
                return new Pair<>(items.indexOf(holder), child);
            }
        }

        return new Pair<>(-1, null);
    }


    private boolean hasHeader(int position) {
        return mAdapter.getHeaderId(position) != NO_HEADER_ID;
    }

    public RecyclerView.ViewHolder getHeader(RecyclerView parent, int position) {
        final long key = mAdapter.getHeaderId(position);

        if (mHeaderCache.containsKey(key)) {
            return mHeaderCache.get(key);
        } else {
            final RecyclerView.ViewHolder holder = mAdapter.onCreateHeaderViewHolder(parent);
            final View header = holder.itemView;

            //noinspection unchecked
            mAdapter.onBindHeaderViewHolder(holder, position);

            int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getMeasuredWidth(), View.MeasureSpec.EXACTLY);
            int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getMeasuredHeight(), View.MeasureSpec.UNSPECIFIED);

            int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
                    parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
            int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
                    parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);

            header.measure(childWidth, childHeight);
            header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());

            mHeaderCache.put(key, holder);

            return holder;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        final int count = parent.getChildCount();
        long previousHeaderId = -1;

        for (int layoutPos = 0; layoutPos < count; layoutPos++) {
            final View child = parent.getChildAt(layoutPos);
            final int adapterPos = parent.getChildAdapterPosition(child);

            if (adapterPos != RecyclerView.NO_POSITION && hasHeader(adapterPos)) {
                long headerId = mAdapter.getHeaderId(adapterPos);

                if (headerId != previousHeaderId) {
                    previousHeaderId = headerId;
                    View header = getHeader(parent, adapterPos).itemView;
                    canvas.save();

                    final int left = child.getLeft();
                    final int top = getHeaderTop(parent, child, header, adapterPos, layoutPos);
                    canvas.translate(left, top);

                    header.setTranslationX(left);
                    header.setTranslationY(top);
                    header.draw(canvas);
                    canvas.restore();
                }
            }
        }
    }

    private int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
        int headerHeight = getHeaderHeightForLayout(header);
        int top = ((int) child.getY()) - headerHeight;
        if (layoutPos == 0) {
            final int count = parent.getChildCount();
            final long currentId = mAdapter.getHeaderId(adapterPos);
            // find next view with header and compute the offscreen push if needed
            for (int i = 1; i < count; i++) {
                int adapterPosHere = parent.getChildAdapterPosition(parent.getChildAt(i));
                if (adapterPosHere != RecyclerView.NO_POSITION) {
                    long nextId = mAdapter.getHeaderId(adapterPosHere);
                    if (nextId != currentId) {
                        final View next = parent.getChildAt(i);
                        final int offset = ((int) next.getY()) - (headerHeight + getHeightWithMargin(getHeader(parent, adapterPosHere).itemView) + headersMargin);
                        if (offset < 0) {
                            return offset;
                        } else {
                            break;
                        }
                    }
                }
            }

            top = Math.max(0, top);
        }

        return top;
    }


    private int getHeaderHeightForLayout(View header) {
        return mRenderInline ? 0 : getHeightWithMargin(header);
    }

    private int getHeightWithMargin(View view){
        ViewGroup.MarginLayoutParams vlp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        return view.getHeight() + vlp.topMargin + vlp.bottomMargin;
    }
}`

@IlyaMyasoedov
Copy link

@fengdexunmi

@native-mobile
Copy link

IlyaMyasoedov do you have a Kotlin version, I get errors trying to call

mAdapter.onBindHeaderViewHolder(holder, position)

screen shot 2018-08-25 at 6 04 29 am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants