Open amirlatifi opened 8 years ago
@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. :)
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
@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.
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
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;
}
}
}
@IlyaMyasoedov hi,where can I find the method findHeaderPositionUnder ?
`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;
}
}`
@fengdexunmi
IlyaMyasoedov do you have a Kotlin version, I get errors trying to call
mAdapter.onBindHeaderViewHolder(holder, position)
How can I add
onClick
to views in the header? ThefindHeaderViewUnder
seems doesn't work most of the time even in your sample project. Specially after scrolling and having headers overlapped.