I am using my custom fragment instead of the RowsFragment in my Android TV app which implements the leanback library. The custom fragment consists of two equally sized horizontal LinearLayouts (upper and lower halves of the screen) in a FrameLayout. The lower part displays a grid of cards using a VerticalGridPresenter.
What I want to accomplish is to update the views on the upper part of the screen when the user navigates between the cards. I can do so by using the methods below. However, I would like to know whether it would be a better/cleaner approach to update the upper part of the fragment (screen) using another presenter. (ActualShowInformationPresenter) How can I add a second presenter to the ArrayObjectAdapter instance? Would that be the correct approach? Thanks in advance.
private void gridOnItemSelected(int position) {
if (position != mSelectedPosition) {
mSelectedPosition = position;
Channel c = (Channel) mAdapter.get(position);
//ActualShowInformationPresenter actualShowInformationPresenter = new ActualShowInformationPresenter();
Context context = getMainFragmentAdapter().getFragment().getContext();
if (c.getProgramme()!= null)
tvActualShowTitle.setText(c.getProgramme().get(0).getTitle().get(0));
}
}
}
...
private OnItemViewSelectedListener mOnItemViewSelectedListener = new OnItemViewSelectedListener() {
#Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
int position = mGridViewHolder.getGridView().getSelectedPosition();
Log.v(TAG, "grid selected position " + position);
gridOnItemSelected(position);
}
};
...
public void setGridPresenter(VerticalGridPresenter gridPresenter) {
if (gridPresenter == null) {
throw new IllegalArgumentException("Grid presenter may not be null");
}
mGridPresenter = gridPresenter;
mGridPresenter.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
if (mOnItemViewClickedListener != null) {
mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
}
}
...
private void setupAdapter() {
VerticalGridPresenter presenter = new VerticalGridPresenter(ZOOM_FACTOR);
presenter.setNumberOfColumns(COLUMNS);
setGridPresenter(presenter);
CardPresenter cardPresenter = new CardPresenter();
mAdapter = new ArrayObjectAdapter(cardPresenter);
setAdapter(mAdapter);
}
Related
I am using new ListAdapter, which automatically animates changes. I would like to disable animations or enable/disable it programmatically.
class UserAdapter extends ListAdapter<User, UserViewHolder> {
public UserAdapter() {
super(User.DIFF_CALLBACK);
}
#Override
public void onBindViewHolder(UserViewHolder holder, int position) {
holder.bindTo(getItem(position));
}
public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
new DiffUtil.ItemCallback<User>() {
#Override
public boolean areItemsTheSame(
#NonNull User oldUser, #NonNull User newUser) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldUser.getId() == newUser.getId();
}
#Override
public boolean areContentsTheSame(
#NonNull User oldUser, #NonNull User newUser) {
// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
return oldUser.equals(newUser);
}
}
}
Another solution is to simply remove the item animator altogether.
recyclerView.itemAnimator = null
You could try to disable or enable animations with setSupportsChangeAnimations on RecyclerView item animator:
SimpleItemAnimator itemAnimator = (SimpleItemAnimator) recyclerView.getItemAnimator();
itemAnimator.setSupportsChangeAnimations(false);
Solution which bypasses DiffUtil
Setting itemAnimator to null and calling submitList() still runs DiffUtil.ItemCallback in a background thread and doesn't submit the list on the same frame! If you want the same behaviour as calling notifyDataSetChanged(), you can do this:
adapter.submitList(null)
adapter.submitList(newItems)
This is useful if you know the contents are indeed completely different and don't care if you loose the scroll position. Or if you have multiple RecyclerViews that all have to update on the same frame (to reduce screen flashing).
Why does this work?
Looking at the source code of AsyncListDiffer.submitList():
// fast simple remove all
if (newList == null) {
//noinspection ConstantConditions
int countRemoved = mList.size();
mList = null;
mReadOnlyList = Collections.emptyList();
// notify last, after list is updated
mUpdateCallback.onRemoved(0, countRemoved);
onCurrentListChanged(previousList, commitCallback);
return;
}
// fast simple first insert
if (mList == null) {
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList);
// notify last, after list is updated
mUpdateCallback.onInserted(0, newList.size());
onCurrentListChanged(previousList, commitCallback);
return;
}
When you call submitList() the first time, all items are immediately removed. The second time they are immediately inserted, never calling DiffUtil or starting a background thread computation.
Putting it all together
if(animations) {
adapter.submitList(newItems)
} else {
recyclerView.itemAnimator = null
adapter.submitList(null)
adapter.submitList(newItems) {
recyclerView.post {
//Restore the default item animator
recyclerView.itemAnimator = DefaultItemAnimator()
}
}
}
So, I'm making a screen with Leanback's Browse Fragment and CardPresenter.
Inside my fragment that extends BrowseFragment, I have a method for drawing the UI:
private void loadCardRows() {
mRowsAdapter = new CustomArrayObjectAdapter(new ListRowPresenter());
final List<UiType> uiTypeList = new ArrayList<>(uiTypes);
for (UiType uiType : uiTypeList) {
HeaderItem cardPresenterHeader = new HeaderItem(0, uiType.getName());
List<TypeReportItem> items = performUiTypeFiltering(uiType.getEndpointType());
CardPresenter cardPresenter = new CardPresenter(attributesHelper);
CustomArrayObjectAdapter cardRowAdapter = new CustomArrayObjectAdapter(cardPresenter);
for (TypeReportItem item : items) {
cardRowAdapter.add(item);
}
mRowsAdapter.add(new ListRow(cardPresenterHeader, cardRowAdapter));
}
setAdapter(mRowsAdapter);
}
Now I'm having a service that loads some data every few seconds. That data is reachable through attributesHelper that I'm passing to CardPresenter.. How am I supposed to reload that data without causing the screen to blink every few seconds?
mRowAdapter.notifyArrayItemRangeChanged(startingIndex, mRowAdapter.size());
Starting index is the one from which position your are updating data.Dont give starting index something like 0,which will result in screen blink from index 0
Maybe this anwser will be helpful
for (int i = 0; i < mAdapter.size(); i++) {
ListRow listRow = ((ListRow) mAdapter.get(i));
ArrayObjectAdapter listRowAdapter = ((ArrayObjectAdapter) listRow.getAdapter());
if (listRowAdapter.size() > 0) {
listRowAdapter.notifyArrayItemRangeChanged(0, listRowAdapter.size());
}
}
android tv -Reloading adapter data
I have used this code in my project. No blink happened.
notifyArrayItemRangeChanged
caused blinking, so try this Adapter
class RefreshableArrayObjectAdapter(presenterSelector: PresenterSelector) :
ArrayObjectAdapter(presenterSelector) {
fun refresh() {
notifyChanged()
}
}
Could you help me to find a proper way without leak to animate a RecylcerView's item's sub item?
I want to create a two level drawer menu, where the menu items are in a RecyclerView created by an adapter. Some of the menu items contain sub items, and this items can open with an arrow at the end to add sub items to the list below the parent item.
I want to animate those arrows to rotate 180 degree when the adapter detects parent item have to open/close and show/remove sub items.
The whole thing is working but I'm not happy with the solution of the animation. :(
What the tricky part is that I'm using databinding to fill those item's with data.
Here's my class what contains the data:
public class MenuItem extends BaseObservable {
public ObservableField<String> name = new ObservableField<>();
public ObservableBoolean selected = new ObservableBoolean(false);
public ObservableBoolean isSubItem = new ObservableBoolean(false);
public MenuItem parent;
public List<MenuItem> subItems = new ArrayList<>(0);
private DrawerAdapter.OnDrawerItemClickListener listener;
/* It's a potencial leak */
public View arrow;
.....
public void animateArrow(boolean isOpen) {
if(arrow == null)
return;
float degrees = isOpen ? 180f : 0f;
arrow.animate().roatate(degrees).setDuration(300).start();
}
}
Here's my ViewHolder:
public class DrawerItemHolder extends RecyclerView.ViewHolder {
public DrawerItemBinder ui;
public DrawerItemHolder(View itemView) {
super(itemView);
ui = DataBindingUtil.bind(itemView);
}
}
Here's how I bind to ViewHolder:
#Override
public void onBindViewHolder(DrawerItemHolder holder, int position) {
MenuItem item = items.get(position);
if(!item.subItems.isEmpty())
item.arrow = holder.ui.arrow;
holder.ui.setData(item);
}
I attach the arrow view only those items what has sub item.
And when the adapter decides to open an item it calls this method:
private void openCategories(MenuItem item) {
...
/* Animate to close state the already opened category's arrow */
if(openedCategory != null)
openedCategory.animateOpener(false);
openedCategory = item;
/* Animate to opened state the newly opened category */
openedCategory.animateOpener(true);
notifyItemRangeInserted(...);
}
The openedCategory is also a MenuItem, a reference to the parent item what is actually open.
And when it want to close an item calls this:
private void closeCategories() {
if (openedCategory == null)
return;
...
notifyItemRangeRemoved(...);
/* Animate to closed state the already opened category's arrow */
openedCategory.animateOpener(false);
openedCategory = null;
}
So my problem is this. How can I animate a subview of parent items without leaking those arrow views by holding reference about them in the MenuItem class?
Best Regards!
My application is returning the latest data from firebase to the buttom of the ListView. But I want it to be on the top! I have thought about it and I think there is only two possible ways to do it.
1. Invert the Listview.
I think that this way is how it should be done but I couldn't figure it out. I have searched a lot on the web but no suitable solution for my case
This is my adapter code
public void onStart() {
super.onStart();
// Setup our view and list adapter. Ensure it scrolls to the bottom as data changes
final ListView listView = getListView();
// Tell our list adapter that we only want 50 messages at a time
mChatListAdapter = new ChatListAdapter(mFirebaseRef.limit(50), this, R.layout.chat_message, mUsername);
listView.setAdapter(mChatListAdapter);
}
And this is the code for the ChatListAdapter constructor for a custom list class ChatListAdapter which extends special list adapter class FirebaseListAdapter:
public ChatListAdapter(Query ref, Activity activity, int layout, String mUsername) {
super(ref, Chat.class, layout, activity);
this.mUsername = mUsername;
}
[Edit] This is some of the code for FirebaseListAdapter which extends BaseAdapter class
public FirebaseListAdapter(Query mRef, Class<T> mModelClass, int mLayout, Activity activity) {
this.mRef = mRef;
this.mModelClass = mModelClass;
this.mLayout = mLayout;
mInflater = activity.getLayoutInflater();
mModels = new ArrayList<T>();
mModelKeys = new HashMap<String, T>();
// Look for all child events. We will then map them to our own internal ArrayList, which backs ListView
mListener = this.mRef.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
T model = dataSnapshot.getValue(FirebaseListAdapter.this.mModelClass);
mModelKeys.put(dataSnapshot.getKey(), model);
// Insert into the correct location, based on previousChildName
if (previousChildName == null) {
mModels.add(0, model);
} else {
T previousModel = mModelKeys.get(previousChildName);
int previousIndex = mModels.indexOf(previousModel);
int nextIndex = previousIndex + 1;
if (nextIndex == mModels.size()) {
mModels.add(model);
} else {
mModels.add(nextIndex, model);
}
}
}
2. Descending query the data.
The second way seams impossible to me, because when I searched on Firebase API documentation and on the web, I couldn't find anyway to order retraived data on descending way.
My data on firebase look like the following:
glaring-fire-9714
chat
-Jdo7-l9_KBUjXF-U4_c
author: Ahmed
message: Hello World
-Jdo71zU5qsL5rcvBzRl
author: Osama
message: Hi!
Thank you.
A simple solution would be to manually move the newly added data to the top of the listview. As you rightly noticed, new data added to a listview will automatically be appended to the bottom of the list, but you may freely move entries once they are added. Something like the following would help you manually move the newest entry to the top of the list:
int iSwapCount = listView.getCount() - 1;
int iPosition = listView.getCount() - 1;
for (int j = 0; j < iSwapCount; j++)
{
Collections.swap(yourlistobject, iPosition, iPosition - 1);
iPosition = iPosition - 1;
}
The above code will begin by calculating the number of swaps that will be required to move last list entry to the top of the list, which is determined by the number of elements in the list - 1. The same is true for calculating the last position in the list. From there Collections.swap will be used to swap the last element in the list with the element before it; this will be repeated until the last element is now the first element, with the rest of the entries in the list remaining in the same order. This code would have to be called each time a new entry is added so that the overall order of the list is maintained.
I realize it has been a while since you asked but I had the same issue. It does not appear that there is a direct answer here.
Here's the change to the firebase adapter to get new items on the top of the list.
Notice the change from add(...) to add(0,...) and add(next...) to add(prev...)
Look for comments:
// prepend instead append
Example:
...
// Insert into the correct location, based on previousChildName
if (previousChildName == null) {
mModels.add(0, model);
mKeys.add(0, key);
} else {
int previousIndex = mKeys.indexOf(previousChildName);
int nextIndex = previousIndex + 1;
if (nextIndex == mModels.size()) {
//mModels.add(model);
//mKeys.add(key);
// prepend instead append
mModels.add(0,model);
mKeys.add(0,key);
} else {
//mModels.add(nextIndex, model);
//mKeys.add(nextIndex, key);
// prepend instead append
mModels.add(previousIndex, model);
mKeys.add(previousIndex, key);
}
}
...
Here is a simple way to invert a FirebaseUI list using a RecyclerView:
boolean reverseList = true;
LinearLayoutManager manager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, reverseList);
if (reverseList) {
manager.setStackFromEnd(true);
}
mRecyclerView.setLayoutManager(manager);
My project consists of a single Activity so far that loads a GridView that is populated by an extended BaseAdapter.
Typically the view is refreshed by calling BaseAdapter.notifyDataSetChanged() from one of my OnClickListener objects.
My problem is that I need to start a timer each time the view is refreshed. I only want to do this when the view has been completely reloaded.
I can't seem to find a listener or method that I can override in either the View or Adapter APIs to perform this, although I presume there is one.
The closest I've found is BaseAdapter.registerDataSetObserver although I'm not sure this is what I'm looking for either.
Can anyone advise please?
Thanks
DataSetObserver won't provide the feature you're looking for. In your adapter try looking at getView() or ViewBinder.setViewBinder() (for the Simple...Adapter classes) once the last view is filled with data you'll be able to know, roughly, when its done.
I'd say your best bet would be to create an anon inner class from this class and add your timer logic in an extension of GridView:
class AdapterDataSetObserver extends DataSetObserver {
#Override
public void onChanged() {
}
#Override
public void onInvalidated() {
}
}
(You can access this member variable as it is not priveate):
/**
* Should be used by subclasses to listen to changes in the dataset
*/
AdapterDataSetObserver mDataSetObserver;
This is the method that you should override (inside gridview) (You may need to make some modifications as some of these member variables may be private - mDataSetObserver is not, however:
#Override
public void setAdapter(ListAdapter adapter) {
if (null != mAdapter) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
resetList();
mRecycler.clear();
mAdapter = adapter;
mOldSelectedPosition = INVALID_POSITION;
mOldSelectedRowId = INVALID_ROW_ID;
if (mAdapter != null) {
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
mDataChanged = true;
checkFocus();
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
int position;
if (mStackFromBottom) {
position = lookForSelectablePosition(mItemCount - 1, false);
} else {
position = lookForSelectablePosition(0, true);
}
setSelectedPositionInt(position);
setNextSelectedPositionInt(position);
checkSelectionChanged();
} else {
checkFocus();
// Nothing selected
checkSelectionChanged();
}
requestLayout();
}
Look for these two lines in the method above and extend your class here:
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);