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

Update view from normal header to sticky header #393

Closed
NegreaVlad opened this issue Jun 22, 2017 · 20 comments
Closed

Update view from normal header to sticky header #393

NegreaVlad opened this issue Jun 22, 2017 · 20 comments
Milestone

Comments

@NegreaVlad
Copy link

In my app, the sticky headers have an extra view added to them over the basic header. How can I do this?
I tried forking the library and adding the sticky header position to the IFlexible onBindView method: bindViewHolder(FlexibleAdapter adapter, VH holder, int position, int stickyPosition, List payloads)

This way I can update the sticky header, but the changes are not reverted when the sticky header is changed, because the header is not bound again.

Congratulations for the library! It is very powerful, but the FlexibleAdapter class is huge. It's size should be reduced. Maybe more helper classes like StickyHeaderHelpercould be made.

Any help is welcome, thanks!

@davideas
Copy link
Owner

@NegreaVlad, Thanks for compliments! 👍
But can you explain better your use case?

You have access to the Adapter to know the current sticky posiiton and the ISectionable contains header already, why do you need to pass that position?
To update the sticky header you have notifyItemChanged or updateItem.

Regarding the size, it is not huge compared to RecyclerView. But, with RC3, I will remove all the deprecated methods, so 400~700 lines will be removed.

@NegreaVlad
Copy link
Author

@davideas I need to make changes to the header view when it becomes a sticky header. I use the list to show a calendar. The sticky header has a button that when clicked, the list scrolls to the current day. The other headers (which are not sticky) do not have this button.

How can I make this button appear on the sticky headers and not the regular headers?

@davideas
Copy link
Owner

davideas commented Jun 22, 2017

FlexibleAdapter.OnStickyHeaderChangeListener. You have the position of the new sticky header.
Unfortunately, not the old one.

@NegreaVlad
Copy link
Author

I found the FlexibleAdapter.OnStickyHeaderChangeListener, but without the position of the old header I cannot remove the button.

@davideas
Copy link
Owner

Well, you can save it at each callback. I will check if I can provide the old position.

@davideas
Copy link
Owner

Yes, I can provide it, but I have to refactor the method signature, it will be available from next version and snapshot.

@davideas davideas added this to the 5.0.0-rc3 milestone Jun 22, 2017
@NegreaVlad
Copy link
Author

NegreaVlad commented Jun 22, 2017

Thanks to your suggestions, I was able to achieve what I wanted, but it seems a bit hacky.
I cached the previous sticky header position:

(FlexibleAdapter.OnStickyHeaderChangeListener) index -> {
                    // If there is a previous sticky header or this is the first header that is converted to a 
                    //sticky header
                    if (mStickyHeaderPosition != -1 || index == 0) {
                        mAdapter.notifyItemChanged(mStickyHeaderPosition);
                    }

                    mStickyHeaderPosition = index;
                }

I have modified the AbstractFlexibleItem.bindViewHolder to provide the stickyPosition inside my header item implementation extending AbstractHeaderItem

@Override
    @SuppressWarnings("unchecked")
    public void bindViewHolder(FlexibleAdapter adapter, MeetingsHeaderViewHolder holder, int position, int stickyPosition, List payloads) {
        // Populate view holder with data

        if (position == stickyPosition) {
            // Show the button
        }
    }

I am open to suggestions.

@davideas
Copy link
Owner

davideas commented Jun 22, 2017

Good, but you can notifyItemChange with a payload, you don't need to override the method with the sticky position.

Another solution would be to check if that header is sticky so you can display your view.
I think this is better so when you recover the item by scrolling the list, since it is generated and bound out of the layout manager, it can work too.

Try both and check if any suite to your use case.

@NegreaVlad
Copy link
Author

NegreaVlad commented Jun 22, 2017

For the first solution, I should receive the instance of the old and new header items and update them so that they know if they are sticky or not and send them as payload to notifyItemChange .
The second solution I do not really understand, please elaborate.

@davideas
Copy link
Owner

The second solution, in bindViewHolder you have the position and adapter.getStickyPosition().

@NegreaVlad
Copy link
Author

@davideas Updating the UI inside the bindViewHolder works, but it is somehow delayed.

I have a view which I want to be shown only if the header is sticky, so I hide this view when adapter.getStickyPosition() != position. The problem is that when I do a fling, items that are no longer sticky, display the view for a short while.

Is there some sort of animation/delay when updating the headers view? I see a sort of alpha animation going on when I set the view's visibility to GONE.

@davideas
Copy link
Owner

@NegreaVlad, that alpha animation is for the container layout when sticky feature is enabled/disabled, not when they swap.
The sticky position is updated immediately when the scroll event brings a new item partially visible.

Would you make a short video, to have a better idea of what is displayed with normal scrolling and with fling? Also show me the binding method of the header.

@NegreaVlad
Copy link
Author

@davideas You can find the two screen recordings here: scrolling_videos.zip

The header's binding method:

    @Override
    public void bindViewHolder(FlexibleAdapter adapter, MeetingsHeaderViewHolder holder, int position, List payloads) {
        if (adapter.getStickyPosition() == position) {
            showTodayJump(holder);
        } else {
            hideTodayJump(holder);
        }

        holder.mDayTV.setText(mDayNumber);
        holder.mMonthTV.setText(mMonth);

        setupWeekday(holder);
        setupDateColor(holder);
    }

    private void showTodayJump(MeetingsHeaderViewHolder holder) {
        holder.mDayOfTheWeekTV.setTextColor(holder.mDayOfTheWeekTV.getContext().getResources().getColor(R.color.black));
        if (TimeUtils.isEventHappeningToday(mDate)) {
            holder.mTodayJumpTV.setVisibility(View.GONE);
        } else if (TimeUtils.isEventInThePast(mDate) && !TimeUtils.isEventHappeningToday(mDate)) {
            holder.mTodayJumpTV.setText(UIUtils.getString(R.string.today) + "↓");
            holder.mTodayJumpTV.setVisibility(View.VISIBLE);
        } else if (TimeUtils.isEventInTheFuture(mDate) && !TimeUtils.isEventHappeningToday(mDate)) {
            holder.mTodayJumpTV.setText(UIUtils.getString(R.string.today) + "↑");
            holder.mTodayJumpTV.setVisibility(View.VISIBLE);
        }
    }

    private void hideTodayJump(MeetingsHeaderViewHolder holder) {
        Log.d("Sticky header button", "showTodayJump: Setting button " + System.currentTimeMillis());
        holder.mTodayJumpTV.setVisibility(View.GONE);
    }

@NegreaVlad
Copy link
Author

I'm using a handler to notify of item changes in order to prevent errors:

.addListener(
                        (FlexibleAdapter.OnStickyHeaderChangeListener) index -> {
                            Log.d("Sticky header button", "initializeRecyclerView: Sticky header position changed");
                            mHandler.post(() -> {
                                // If there is a previous sticky header or this is the first header that is converted to a
                                // sticky header
                                if (mStickyHeaderPosition != -1 || index == 0) {
                                    Log.d("Sticky header button", "initializeRecyclerView: Notifying sticky position changed");
                                    mAdapter.notifyItemChanged(mStickyHeaderPosition);
                                }

                                mStickyHeaderPosition = index;
                            });
                        });

The delay is very small:

08-14 16:06:35.621 4385-4385/com.firstagenda.phone D/Sticky header button: initializeRecyclerView: Sticky header position changed
08-14 16:06:35.631 4385-4385/com.firstagenda.phone D/Sticky header button: initializeRecyclerView: Notifying sticky position changed

@davideas
Copy link
Owner

davideas commented Aug 14, 2017

@NegreaVlad, thanks for the videos and code. By the way, nice layout and user interface, thanks also to my adapter 😃

I tried myself in the demoApp, this delay appears also on my test and I can explain why.
Because I believe you use the DefaultItemAnimator, it has an alpha animation on change, and indeed we notice a slight flash on the full row. This seems to have an impact also to the inner views you are changing too.

You resolve in 2 ways:

  • by sending a payload in notifyItemChanged(mStickyHeaderPosition, payload); Then, in the binding method you check it, and finish the binding with return statement. In this way you update that view only!
  • Another solution is to set null to the itemAnimator, but you will loose also the add and remove animations. Not nice.

PS. Instead of using a Handler.post, you can use RecyclerView.post().

@NegreaVlad
Copy link
Author

@davideas Thanks for the help, you are 100% right. The alpha animation was caused by the DefaultItemAnimator.

Both of your suggestions fix the issue. I had gone with the first, using notifyItemChanged(mStickyHeaderPosition, payload); to keep the add/remove animations.

The final code:

    @Override
    public void bindViewHolder(FlexibleAdapter adapter, MeetingsHeaderViewHolder holder, int position, List payloads) {
        if (adapter.getStickyPosition() == position) {
            showTodayJump(holder);
        } else {
            hideTodayJump(holder);

            if (payloads.size() == 1 && payloads.get(0) instanceof UpdateHeaderEvent) {
                return;
            }
        }

        holder.mDayTV.setText(mDayNumber);
        holder.mMonthTV.setText(mMonth);

        setupWeekday(holder);
        setupDateColor(holder);
    }
                .addListener(
                        (FlexibleAdapter.OnStickyHeaderChangeListener) index -> {
//                            mHandler.post(() -> {
                                // If there is a previous sticky header or this is the first header that is converted to a
                                // sticky header
                                if (mStickyHeaderPosition != -1 || index == 0) {
                                    Log.d("StickyHeaderButton", "initializeRecyclerView: Notifying sticky position changed: " + mStickyHeaderPosition);
                                    mAdapter.notifyItemChanged(mStickyHeaderPosition, new UpdateHeaderEvent());
                                }

                                mStickyHeaderPosition = index;
//                            });
                        });

UpdateHeaderEvent is an empty POJO.

@davideas
Copy link
Owner

@NegreaVlad, watch out that you actually need the execution in post. LayoutManager is computing and you will receive a continuous warning in the logs. You can use RecyclerView to execute in post. no need of Handerl.

@NegreaVlad
Copy link
Author

@davideas There is however another issue that I found. Because I use a Handler to post notifyItemChanged events in order to prevent the IllegalStateException warning, sometimes, mStickyHeaderPosition no longer has the correct position of the previous sticky header because I also use infinite scrolling from the top and I suspect that new items are added to the list, thus making the index saved here mStickyHeaderPosition = index, invalid.

Basically, the wrong item receives the notifyItemChanged call.

I tried setting a RecyclerView.AdapterDataObserver like you did, to adjust the index, but I cannot extend your private one and you cannot have more than one RecyclerView.AdapterDataObserver per RecyclerView.

@davideas
Copy link
Owner

@NegreaVlad, you actually can register more observers as many you want.

@NegreaVlad
Copy link
Author

@davideas You are right, I didn't look close enough, you can add multiple observers.

I fixed the issue with the wrong header index by using a RecyclerView.AdapterDataObserver.

The code:
Where I configure my FlexibleAdapter

.addListener(
                        (FlexibleAdapter.OnStickyHeaderChangeListener) index -> mAdapter.notifyItemChangedWhileComputing(index, new UpdateHeaderEvent()));

Inside my custom FlexibleAdapter

    private int mStickyHeaderPosition = -1;

    public CustomAdapter() {
        // Call super() and all the rest ...
 
        registerAdapterDataObserver(new DataObserver());
    }

    public void notifyItemChangedWhileComputing(int index, Object payload) {
        if (getRecyclerView() == null)
            return;

        getRecyclerView().post(() -> {
            // If there is a previous sticky header or this is the first header that is converted to a
            // sticky header
            if (mStickyHeaderPosition != -1 || index == 0) {
                notifyItemChanged(mStickyHeaderPosition, new UpdateHeaderEvent());
            }

            mStickyHeaderPosition = index;
        });
    }

    private class DataObserver extends RecyclerView.AdapterDataObserver {

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            if (positionStart <= mStickyHeaderPosition) {
                mStickyHeaderPosition += itemCount;
            }
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            super.onItemRangeRemoved(positionStart, itemCount);
            if (positionStart <= mStickyHeaderPosition) {
                mStickyHeaderPosition -= itemCount;
            }
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            super.onItemRangeMoved(fromPosition, toPosition, itemCount);
        }
    }

Thanks again for the help and quick replies.

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

2 participants