I have a Multi-Selection RecyclerView. I am tring to handle button click inside to RecyclerView Item when It's selected/activated. But when an item selected/activated in RecyclerView I am not able to click the ChildView (Button) It handles RootView of content and reverts to the deactivated/notselected state.
Video
I read the documantation It says
Selection Hotspot
This is an optional feature identifying an area within a view that is
single-tap to select. Ordinarily a single tap on an item when there is
no existing selection will result in that item being activated. If the
tap occurs within the "selection hotspot" the item will instead be
selected.
See OnItemActivatedListener for details on handling item activation.
I tried to override inSelectionHotspot function
/**
* Areas are often included in a view that behave similar to checkboxes, such
* as the icon to the left of an email message. "selection
* hotspot" provides a mechanism to identify such regions, and for the
* library to directly translate taps in these regions into a change
* in selection state.
*
* #return true if the event is in an area of the item that should be
* directly interpreted as a user wishing to select the item. This
* is useful for checkboxes and other UI affordances focused on enabling
* selection.
*/
public boolean inSelectionHotspot(#NonNull MotionEvent e) {
return false;
}
The document's comment reffers to Gmail application, that example to selection by clicking e-mail's leftside ImageView and you can able to move e-mail's content by clicking to Title or Description area. That's exactly what I'm trying to do.
ItemDetailsLookup.ItemDetails Class
class PurchaseItemDetails(var binding: ListObjectLevelPurchaseBinding) : ItemDetailsLookup.ItemDetails<LevelModel>() {
var itemPosition: Int = 0
lateinit var item: LevelModel
override fun getSelectionKey(): LevelModel? {
return item
}
override fun getPosition(): Int {
return itemPosition
}
override fun inSelectionHotspot(e: MotionEvent): Boolean {
val locationOnScreen = IntArray(2)
binding.buttonLevelPurchaseInspect.getLocationOnScreen(locationOnScreen)
val (left) = locationOnScreen
val right = left + binding.buttonLevelPurchaseInspect.width
Log.e("InSelectionHotspot","${e.x.roundToInt() !in left..right}")
return e.x.roundToInt() !in left..right
}
}
I also tried to override onItemActivatedListener nothing changed.
tracker = SelectionTracker.Builder<LevelModel>(
"selection.levels",
binding.recyclerviewBundleDetailPurchaseLevels,
PurchaseItemKeyProvider(adapter.currentList),
PurchaseItemLookup(binding.recyclerviewBundleDetailPurchaseLevels),
StorageStrategy.createParcelableStorage(LevelModel::class.java)
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
Although this question is old, I think there always will be Android devs that are struggling with the RecyclerView's selection API. As always the Google's library is lacking details and some useful examples. That's why I'd like to share a solution for those who keep struggling for a working setup of selection hotspot. Sorry for the inconvenience, the code is in Java since I don't use Kotlin yet. But I think it is easy to convert it to Kotlin.
ItemDetailsLookup.ItemDetails Class
public static class PurchaseItemDetails extends ItemDetailsLookup.ItemDetails<Long> {
private long position;
private Rect hotSpot;
public PurchaseItemDetails() {}
#Override
public int getPosition() {
return (int) position;
}
#Nullable
#Override
public Long getSelectionKey() {
return position;
}
#Override
public boolean inSelectionHotspot(#NonNull MotionEvent e) {
if(hotSpot == null) {
Log.d(TAG, "inSelectionHotspot: null; Event X "+e.getX()+", Y "+e.getY());
return false;
}
// Here the coordinates of selection hotspot rect can be observed
Log.d(TAG, String.format("inSelectionHotspot: Hotspot %s; event X %f, Y " +
"%f", hotSpot.toShortString(), e.getX(), e.getY()));
return hotSpot.contains((int) e.getX(), (int) e.getY());
}
void setPosition(long position) {
this.position = position;
}
void setHotSpot(Rect hotSpot) {
this.hotSpot = hotSpot;
}
}
Now that we have implemented the ItemDetails class we have to have this class within the ViewHolder class in order to be able to define and set the selection hotspot.
SampleViewHolder Class
class SampleViewHolder extends RecyclerView.ViewHolder {
private final PurchaseItemDetails details;
//...
public SampleViewHolder(#NonNull View itemView) {
super(itemView);
details = new PurchaseItemDetails();
}
void bind() {
// Setup selection hotspot
Rect rect = new Rect();
// Attention here! We must get the rectangle after the viewholder object has created.
// Otherwise the getGlobalVisibleRect() will return unexpected values.
binding.buttonLevelPurchaseInspect.getGlobalVisibleRect(rect);
// Set the rectangle spot so that the Item details can check and recognize the spot.
details.setHotSpot(rect);
// Setup position
details.setPosition(getBindingAdapterPosition());
//...
}
//...
}
And finally somewhere in the adapter class where we bind the view holder:
#Override
public void onBindViewHolder(#NonNull SampleViewHolder holder, int position) {
holder.bind();
}
Warning!!
This is not a whole code at all. The implementers must adapt it to their codes and convenience.
Related
The PlaybackOverlayFragment of the sample app uses the PlaybackControlsGlue to set up playback controls based on the data model. This is the look when using the standard glue:
My problem is that I don't want the title/subtitle text to appear above the main player controls bar - we want them at the top left of the player screen instead. Therefore, to disable the showing of title/subtitle, I override createControlsRowAndPresenter() of the glue and use the empty-args constructor of PlaybackControlsRowPresenter instead:
#Override
public PlaybackControlsRowPresenter createControlsRowAndPresenter() {
PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
setControlsRow(controlsRow);
final View.OnKeyListener onKeyListener = this;
PlaybackControlsRowPresenter presenter = new PlaybackControlsRowPresenter() { // no AbstractDetailsDescriptionPresenter argument
#Override
protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
super.onBindRowViewHolder(vh, item);
vh.setOnKeyListener(onKeyListener);
}
#Override
protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
super.onUnbindRowViewHolder(vh);
vh.setOnKeyListener(null);
}
};
// secondaryActionsAdapter setup not shown
presenter.setOnActionClickedListener(new OnActionClickedListener() {
#Override
public void onActionClicked(Action action) {
dispatchAction(action);
}
});
return presenter;
}
The result? No title/subtitle show as expected but now there's more spacing between the primary controls bar and other rows:
What could I be doing wrong, or is it a bug with the leanback library?
Without those two rows of text, the playback controls are now at the top of that view. You can probably apply margins or padding to the playback controls to shift it to the expected location.
Turns out the playback controls need some view above it so they don't occupy the top of their container view (#Nick is right). But I wanted to share my solution in case anyone has a similar need.
PlaybackControlsRowPresenter can take in any presenter in its constructor, not just AbstractDetailsDescriptionPresenters. So createControlsRowAndPresenter() should look like this:
EmojiRowPresenter emojiRowPresenter = new EmojiRowPresenter() {
#Override
protected void onBindEmojiInfo(EmojiRowView rowView, EmojiInfo emojiInfo) {
rowView.setEmojiInfo(emojiInfo);
}
};
PlaybackControlsRowPresenter presenter = new PlaybackControlsRowPresenter(emojiRowPresenter) { // replace the default description presenter with custom presenter
...
}
// everything else stays as before
and EmojiRowPresenter is a subclass of Presenter that looks like this:
public abstract class EmojiRowPresenter extends Presenter {
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
EmojiRowView emojiRowView = new EmojiRowView(parent.getContext());
emojiRowView.setFocusable(true);
emojiRowView.setFocusableInTouchMode(true);
return new ViewHolder(emojiRowView);
}
#Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
EmojiRowView emojiRowView = (EmojiRowView) viewHolder.view;
PlaybackControlHelper glue = (PlaybackControlHelper) item;
EmojiInfo emojiInfo = glue.getEmojiInfo();
if (emojiInfo != null) {
onBindEmojiInfo(emojiRowView, emojiInfo);
}
}
#Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
// ...
}
protected abstract void onBindEmojiInfo(EmojiRowView rowView, EmojiInfo emojiInfo);
}
Of course, EmojiRowView creates the view from the layout that defines each item. Here's the end result:
I am trying to implement a Google Map marker with infoWindow that if someone clicks on this infoWindow, it plays a song and if clicks again, it stops. To visualize this, I write a custom infoWindow layout. There is, in infoWindow, you can see user and track info with a button. This button shows play icon if the track does not begin to play yet, and if it pressed (press on infoWindow, not the button), I hope it changes its icon from "play" to "stop". However, I cannot change my custom infoWindow's view depending on infoWindowClickListener activity. I tried to change infoWindowAdapter especially but I do not want to change view of all other infoWindows and also I want to see the change immediately. In this way, the infoWindow refreshes its view after I click on the marker again. In other words, it does not change the view simultaneously with my click action.
Here you can see what I am talking about. Stop status on left, play status on right:
Here is my futile effort for adapter:
public class OrangeInfoWindowAdapter implements GoogleMap.InfoWindowAdapter {
Context context;
ImageButton playButton;
boolean onPlay;
public OrangeInfoWindowAdapter(Context context, boolean onPlay) {
this.context = context;
this.onPlay = onPlay;
}
#Override
public View getInfoWindow(Marker arg0) {
LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.orange_infowindow, null);
v.setMinimumWidth(280);
v.setMinimumHeight(120);
TextView tvUsername = (TextView) v.findViewById(R.id.tv_username);
TextView tvTrack = (TextView) v.findViewById(R.id.tv_track);
int index = arg0.getTitle().indexOf("*");
try {
tvUsername.setText(arg0.getTitle().substring(0, index - 1) + "\n" + arg0.getTitle().substring(index + 2));
} catch (StringIndexOutOfBoundsException e) {
}
tvUsername.setTextSize(10);
tvUsername.setTextColor(Color.rgb(70, 70, 70));
index = arg0.getSnippet().indexOf("*");
try {
tvTrack.setText(arg0.getSnippet().substring(0, index - 1) + "\n" + arg0.getSnippet().substring(index + 2));
} catch (StringIndexOutOfBoundsException e) {
}
tvTrack.setTextSize(10);
tvTrack.setTextColor(Color.rgb(230, 92, 1));
playButton = (ImageButton) v.findViewById(R.id.playButton);
if (onPlay)
onPlay();
return v;
}
public void onPlay() {
playButton.setBackgroundResource(R.drawable.info_stop_button);
}
public void onStop() {
playButton.setBackgroundResource(R.drawable.info_play_button);
}
#Override
public View getInfoContents(Marker arg0) {
return null;
}
}
And this is my onInfoWindowClick():
#Override
public void onInfoWindowClick(Marker marker) {
if (!infoWindowPlayerActive) {
int index = findMarkerIndex(marker);
OrangeInfoWindowAdapter infoWindowAdapter2 = new OrangeInfoWindowAdapter(getActivity().getApplicationContext(), true);
googleMap.setInfoWindowAdapter(infoWindowAdapter2);
new InfoWindowPlayerTask(mainActivity).execute(activities.get(index).getTrackId());
infoWindowPlayerActive = true;
}
else {
// same thing...
infoWindowPlayerActive = false;
}
}
If you want more information to understand the problem clearly, please ask me.
The GoogleMap API v.2 does not support any interaction on InfoWindow, besides opening and closing it.
However, there is an amazing hack implemented in this answer, on how you should create an interactive View inside your InfoWindow. Keep in mind that the same technique applies for Fragments too.
From the official documentation:
Note: The info window that is drawn is not a live view. The view is rendered as an image (using View.draw(Canvas)) at the time it is returned. This means that any subsequent changes to the view will not be reflected by the info window on the map. To update the info window later (e.g., after an image has loaded), call showInfoWindow(). Furthermore, the info window will not respect any of the interactivity typical for a normal view such as touch or gesture events. However you can listen to a generic click event on the whole info window as described in the section below.
I found a sloppy but working solution:
#Override
public void onInfoWindowClick(Marker marker) {
if (!infoWindowPlayerActive) {
googleMap.setInfoWindowAdapter(infoWindowAdapterOnPlay);
marker.showInfoWindow();
newClickedInfoWindowIndex = findMarkerIndex(marker);
if (lastClickedInfoWindowIndex != newClickedInfoWindowIndex) {
new InfoWindowPlayerTask(mainActivity).execute(activities.get(newClickedInfoWindowIndex).getTrackId());
}
else {
mainActivity.getPlayerManager().clickPlayPause();
}
lastClickedInfoWindowIndex = newClickedInfoWindowIndex;
infoWindowPlayerActive = true;
}
else {
googleMap.setInfoWindowAdapter(infoWindowAdapter);
marker.showInfoWindow();
mainActivity.getPlayerManager().clickPlayPause();
infoWindowPlayerActive = false;
}
}
public int findMarkerIndex(Marker marker) {
for (int i = 0; i < markers.size(); i++) {
if (marker.getPosition().equals(markers.get(i).getPosition())) {
return i;
}
}
return -1;
}
Of course, assume that infoWindowPlayerActive, lastClickedInfoWindowIndex, newClickedInfoWindowIndex are defined in the class above.
I'm using Talkback to read the content of the views on my ViewPager, and it's reading the content of the current view and the content of the next view (not visible).
For example
View 1
TextView -> hi1
TextView -> bye1
View2
TextView -> hi2
TextView -> bye2
Talkback read hi1, hi2, bye1, bye2
I've tried to change the value of pager.setOffscreenPageLimit(), but it doesn't do anything, Talkback always read the current view and the next one, even if the value of OffScreenPageLimit is 4 (it should read the next 2 views).
The only info I've found is that: https://code.google.com/p/eyes-free/issues/detail?id=139
Any idea?
You can create event custom Accessibility event inside the OnChangePageView and save the string you want to read in a list and get position from interface between Activity/fragment and adapter.
ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrollStateChanged(int arg0) { }
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) { }
#Override
public void onPageSelected(int position) {
if(isTalkbackActive(getApplicationContext())) {
AccessibilityEvent event = AccessibilityEvent.obtain();
if (onPageChangeInterface != null) {
String text = onPageChangeInterface.getTextToRead(position);
event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
event.getText().add(text);
viewPagerWallets.requestFocus();
viewPagerWallets.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
accessibilityManager.sendAccessibilityEvent(event);
}
}
}
};
Then the same string that you saved you add it in ContentDescription of the relative that you have all the view only in the first position.
ArrayList readAccessibilityValues = new ArrayList<>();
String valor = valor + txtView1.getText()+",";
valor = valor + txtView2.getText()+",";
if(position == 0) {
relativeGeneral.setContentDescription(valor);
}
readAccessibilityValues.add(valor);
I think it's not the best solution but I have not found another
This could help you to get into the right direction:
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val event = AccessibilityEvent.obtain()
event.eventType = AccessibilityEvent.TYPE_ANNOUNCEMENT
event.text.add("Hello TalkBack!")
val accessibilityManager = requireContext().getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
accessibilityManager.sendAccessibilityEvent(event)
}
})
Simply replace "Hello Talkback!" by any string you would like to be read when changing a page in ViewPager.
I have a custom adapter that renders two (2) checkboxes, a picture and the name of the client. All the information needed for the adapter is fetched from an ArratList that contains the Client class.
Every row needs to have both checkboxes checked (selected) for the client in order to process the purchase order, in case that a particular client has one checkbox checked-off and the other checkbox not, that raises a flag as MISMATCH. To make a valid order both checkboxes need to be checked-off.
We are implementing a button for verification, which will find any mismatch in the adapter and then hightlight the mismatches.
EDITION: After pressing the verificationBtn I am able to identify if any row has mismatch on checkboxes, for example, if checkbox1 was checked and checkbox2 not. that will mark the row as mismatch. I am using the position of my checkboxes based on clientList that is an arraylist of List clientList.
QUESTION: How can I get the position that the viewHolder has in order compare against the clientList position? Is there any way I can force the viewHolder to store the position and get it back and make the comparison with cli.getClient_number() ?
So far I have tested two different ways with no luck:
Method 1:
viewHolder.clientName.setBackgroundColor((Interger.parseInt(cli.getClient_number()) ) == position ? Color.GREEN : Color.TRANSPARENT);
Method 2
viewHolder.clientName.setBackgroundColor(Color.GREEN);
here the code that I am implementing.
// This goes in my main Client Activity
Button verificationBtn = (Button) findViewById(R.id.verificationBtn);
verificationBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
buffer.setLength(0);
mismatchTv.setText("");
for (Client cli : clientList) {
if (cli.isCheckboxOneSelected() != cli.isCheckboxTwoSelected()) {
//We had defined above the following buffer = new StringBuffer();
buffer.append((ah.parseInt(cli.getClient_number(), 0) - 1) + ", ");
cli.setMismatch(true);
//We are passing here the ID that correspond to the client mismatch
list_adapter.setBackgroundColor(Integer.parseInt(cli.getClient_number()) - 1);
setListAdapter(list_adapter);
Log.w("cli.getClient_number() ", String.valueOf(Integer.parseInt(cli.getClient_number()) - 1));
}
}
// We display any mismatch on a TextView on top of the screen
if (buffer.length() != 0) {
//This is a TextView on top of the screen
mismatchTv.setText("Error en Client(s) "
+ buffer.toString());
}
// This goes inside of the ClientArrayAdapter
public void setBackgroundColor(int position) {
Log.w("inside of setBackgroundColor method", "True");
switchIndex = 1;
positionFetched = position;
}
// This goes inside of the ClientArrayAdapter
// and inside the body of public View getView(int position, View convertView, ViewGroup parent) {
switch (switchIndex) {
case 1:
viewHolder.cbone
.setButtonDrawable(R.drawable.btn_checkbox_selector);
viewHolder.cbtwo
.setButtonDrawable(R.drawable.btn_checkbox_selector);
Log.w("switch 1 was called ", "True");
for (Client cli : clientList) {
if (cli.isCheckboxOneSelected() != cli.isCheckboxTwoSelected()) {
Client cli = getItem(positionFetched);
if (cli.isMismatch()) {
cli.setColor(Color.BLACK);
Log.e("if (cli.isMismatch()) ", "");
//HERE WE ARE TRYING TO HIGHLIGHT THE ROW WITH MISMATCH
//WHY THIS LINE DOES NOT WORK?
//THE ISSUE THAT I AM GETTING IS THAT I CANNOT CONTROL WHAT ROW TO AFFECT
//IN THE VIEW HOLDER
viewHolder.clientName.setBackgroundColor(Color.GREEN);
}
}
}
break;
default:
viewHolder.cbone.setButtonDrawable(R.drawable.disabled_cb);
viewHolder.cbtwo.setButtonDrawable(R.drawable.disabled_cb);
break;
}
// This goes inside of the ClientArrayAdapter
static class ViewHolder {
public TextView clientName;
public TextView clientNumber;
public ImageView imageView;
public CheckBox cbtwo;
public CheckBox cbone;
public int position;
}
After three days trying to figure out what's wrong with this code, I finally found the solution moving the Method #1 just at the very end of the getView method. :-)
I'm building an interface similar to the Google Hangouts chat interface. New messages are added to the bottom of the list. Scrolling up to the top of the list will trigger a load of previous message history. When the history comes in from the network, those messages are added to the top of the list and should not trigger any kind of scroll from the position the user had stopped when the load was triggered. In other words, a "loading indicator" is shown at the top of the list:
Which is then replaced in-situ with any loaded history.
I have all of this working... except one thing that I've had to resort to reflection to accomplish. There are plenty of questions and answers involving merely saving and restoring a scroll position when adding items to the adapter attached to a ListView. My problem is that when I do something like the following (simplified but should be self-explanatory):
public void addNewItems(List<Item> items) {
final int positionToSave = listView.getFirstVisiblePosition();
adapter.addAll(items);
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
}
Then what the user will see is a quick flash to the top of the ListView, then a quick flash back to the right location. The problem is fairly obvious and discovered by many people: setSelection() is unhappy until after notifyDataSetChanged() and a redraw of ListView. So we have to post() to the view to give it a chance to draw. But that looks terrible.
I've "fixed" it by using reflection. I hate it. At its core, what I want to accomplish is reset the first position of the ListView without going through the rigamarole of the draw cycle until after I've set the position. To do that, there's a helpful field of ListView: mFirstPosition. By gawd, that's exactly what I need to adjust! Unfortunately, it's package-private. Also unfortunately, there doesn't appear to be any way to set it programmatically or influence it in any way that doesn't involve an invalidate cycle... yielding the ugly behavior.
So, reflection with a fallback on failure:
try {
Field field = AdapterView.class.getDeclaredField("mFirstPosition");
field.setAccessible(true);
field.setInt(listView, positionToSave);
}
catch (Exception e) { // CATCH ALL THE EXCEPTIONS </meme>
e.printStackTrace();
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
}
}
Does it work? Yes. Is it hideous? Yes. Will it work in the future? Who knows? Is there a better way? That's my question.
How do I accomplish this without reflection?
An answer might be "write your own ListView that can handle this." I'll merely ask whether you've seen the code for ListView.
EDIT: Working solution with no reflection based on Luksprog's comment/answer.
Luksprog recommended an OnPreDrawListener(). Fascinating! I've messed with ViewTreeObservers before, but never one of these. After some messing around, the following type of thing appears to work quite perfectly.
public void addNewItems(List<Item> items) {
final int positionToSave = listView.getFirstVisiblePosition();
adapter.addAll(items);
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
listView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
#Override
public boolean onPreDraw() {
if(listView.getFirstVisiblePosition() == positionToSave) {
listView.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
else {
return false;
}
}
});
}
Very cool.
As I said in my comment, a OnPreDrawlistener could be another option to solve the problem. The idea of using the listener is to skip showing the ListView between the two states(after adding the data and after setting the selection to the right position). In the OnPreDrawListener(set with listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);) you'll check the current visible position of the ListView and test it against the position which the ListView should show. If those don't match then make the listener's method return false to skip the frame and set the selection on the ListView to the right position. Setting the proper selection will trigger the draw listener again, this time the positions will match, in which case you'd unregister the OnPreDrawlistener and return true.
I was breaking up my head until I found a solution similar to this.
Before adding a set of items you have to save top distance of the firstVisible item and after adding the items do setSelectionFromTop().
Here is the code:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// for (Item item : items){
mListAdapter.add(item);
}
// restore index and top position
mList.setSelectionFromTop(index, top);
It works without any jump for me with a list of about 500 items :)
I took this code from this SO post: Retaining position in ListView after calling notifyDataSetChanged
The code suggested by the question author works, but it's dangerous.
For instance, this condition:
listView.getFirstVisiblePosition() == positionToSave
may always be true if no items were changed.
I had some problems with this aproach in a situation where any number of elements were added both above and below the current element. So I came up with a sligtly improved version:
/* This listener will block any listView redraws utils unlock() is called */
private class ListViewPredrawListener implements OnPreDrawListener {
private View view;
private boolean locked;
private ListViewPredrawListener(View view) {
this.view = view;
}
public void lock() {
if (!locked) {
locked = true;
view.getViewTreeObserver().addOnPreDrawListener(this);
}
}
public void unlock() {
if (locked) {
locked = false;
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
}
#Override
public boolean onPreDraw() {
return false;
}
}
/* Method inside our BaseAdapter */
private updateList(List<Item> newItems) {
int pos = listView.getFirstVisiblePosition();
View cell = listView.getChildAt(pos);
String savedId = adapter.getItemId(pos); // item the user is currently looking at
savedPositionOffset = cell == null ? 0 : cell.getTop(); // current item top offset
// Now we block listView drawing until after setSelectionFromTop() is called
final ListViewPredrawListener predrawListener = new ListViewPredrawListener(listView);
predrawListener.lock();
// We have no idea what changed between items and newItems, the only assumption
// that we make is that item with savedId is still in the newItems list
items = newItems;
notifyDataSetChanged();
// or for ArrayAdapter:
//clear();
//addAll(newItems);
listView.post(new Runnable() {
#Override
public void run() {
// Now we can finally unlock listView drawing
// Note that this code will always be executed
predrawListener.unlock();
int newPosition = ...; // Calculate new position based on the savedId
listView.setSelectionFromTop(newPosition, savedPositionOffset);
}
});
}