What is the best way to inspect and assert that a listview is the expected size with android espresso?
I wrote this matcher, but don't quite know how to integrate it into the test.
public static Matcher<View> withListSize (final int size) {
return new TypeSafeMatcher<View> () {
#Override public boolean matchesSafely (final View view) {
return ((ListView) view).getChildCount () == size;
}
#Override public void describeTo (final Description description) {
description.appendText ("ListView should have " + size + " items");
}
};
}
Figured this out.
class Matchers {
public static Matcher<View> withListSize (final int size) {
return new TypeSafeMatcher<View> () {
#Override public boolean matchesSafely (final View view) {
return ((ListView) view).getCount () == size;
}
#Override public void describeTo (final Description description) {
description.appendText ("ListView should have " + size + " items");
}
};
}
}
If expecting one item in the list, put this in the actual test script.
onView (withId (android.R.id.list)).check (ViewAssertions.matches (Matchers.withListSize (1)));
There are two different approaches of getting items count in a list with espresso:
First one is as #CoryRoy mentioned above - using TypeSafeMatcher, the other one is to use BoundedMatcher.
And because #CoryRoy already showed how to assert it, here I'd like to tell how to get(return) the number using different matchers.
public class CountHelper {
private static int count;
public static int getCountFromListUsingTypeSafeMatcher(#IdRes int listViewId) {
count = 0;
Matcher matcher = new TypeSafeMatcher<View>() {
#Override
protected boolean matchesSafely(View item) {
count = ((ListView) item).getCount();
return true;
}
#Override
public void describeTo(Description description) {
}
};
onView(withId(listViewId)).check(matches(matcher));
int result = count;
count = 0;
return result;
}
public static int getCountFromListUsingBoundedMatcher(#IdRes int listViewId) {
count = 0;
Matcher<Object> matcher = new BoundedMatcher<Object, String>(String.class) {
#Override
protected boolean matchesSafely(String item) {
count += 1;
return true;
}
#Override
public void describeTo(Description description) {
}
};
try {
// do a nonsense operation with no impact
// because ViewMatchers would only start matching when action is performed on DataInteraction
onData(matcher).inAdapterView(withId(listViewId)).perform(typeText(""));
} catch (Exception e) {
}
int result = count;
count = 0;
return result;
}
}
Also want to mention that you should use ListView#getCount() instead of ListView#getChildCount():
getCount() - number of data items owned by the Adapter, which may be larger than the number of visible views.
getChildCount() - number of children in the ViewGroup, which may be reused by the ViewGroup.
Related
How to go about checking whether RecyclerView items are displayed in the correct order using Espresso? I'm trying to test it checking it by the text for the title of each element.
When I try this piece of code it works to click the element but can't go on to instead of performing a click trying to Assert the text for the element
onView(withId(R.id.rv_metrics)).perform(actionOnItemAtPosition(0, click()));
When I try to use a custom matcher instead I keep getting the error
Error performing 'load adapter data' on view 'with id: mypackage_name:id/rv_metrics'
I know now onData doesn't work for RecyclerView but before that I was trying to use a custom matcher for this task.
public static Matcher<Object> hasTitle(final String inputString) {
return new BoundedMatcher<Object, Metric>(Metric.class) {
#Override
protected boolean matchesSafely(Metric metric) {
return inputString.equals(metric.getMetric());
}
#Override
public void describeTo(org.hamcrest.Description description) {
description.appendText("with title: ");
}
};
}
I also tried something like this but it obviously doesn't work due to the type given as parameter to the actionOnItemAtPosition method but would we have something similar to it that could maybe work?
onView(withId(R.id.rv_metrics)).check(actionOnItemAtPosition(0, ViewAssertions.matches(withText("Weight"))));
What am I missing here please?
Thanks a lot.
As it's been mentioned here, RecyclerView objects work differently than AdapterView objects, so onData() cannot be used to interact with them.
In order to find a view at specific position of a RecyclerView you need to implement a custom RecyclerViewMatcher like below:
public class RecyclerViewMatcher {
private final int recyclerViewId;
public RecyclerViewMatcher(int recyclerViewId) {
this.recyclerViewId = recyclerViewId;
}
public Matcher<View> atPosition(final int position) {
return atPositionOnView(position, -1);
}
public Matcher<View> atPositionOnView(final int position, final int targetViewId) {
return new TypeSafeMatcher<View>() {
Resources resources = null;
View childView;
public void describeTo(Description description) {
String idDescription = Integer.toString(recyclerViewId);
if (this.resources != null) {
try {
idDescription = this.resources.getResourceName(recyclerViewId);
} catch (Resources.NotFoundException var4) {
idDescription = String.format("%s (resource name not found)",
new Object[] { Integer.valueOf
(recyclerViewId) });
}
}
description.appendText("with id: " + idDescription);
}
public boolean matchesSafely(View view) {
this.resources = view.getResources();
if (childView == null) {
RecyclerView recyclerView =
(RecyclerView) view.getRootView().findViewById(recyclerViewId);
if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
}
else {
return false;
}
}
if (targetViewId == -1) {
return view == childView;
} else {
View targetView = childView.findViewById(targetViewId);
return view == targetView;
}
}
};
}
}
And then use it in your test case in this way:
#Test
void testCase() {
onView(new RecyclerViewMatcher(R.id.rv_metrics)
.atPositionOnView(0, R.id.txt_title))
.check(matches(withText("Weight")))
.perform(click());
onView(new RecyclerViewMatcher(R.id.rv_metrics)
.atPositionOnView(1, R.id.txt_title))
.check(matches(withText("Height")))
.perform(click());
}
If somebody is interested in the Kotlin version, here it is
fun hasItemAtPosition(position: Int, matcher: Matcher<View>) : Matcher<View> {
return object : BoundedMatcher<View, RecyclerView>(RecyclerView::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("has item at position $position : ")
matcher.describeTo(description)
}
override fun matchesSafely(item: RecyclerView?): Boolean {
val viewHolder = item?.findViewHolderForAdapterPosition(position)
return matcher.matches(viewHolder?.itemView)
}
}
}
I simplified a bit Mosius answer:
public static Matcher<View> hasItemAtPosition(final Matcher<View> matcher, final int position) {
return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {
#Override
public void describeTo(Description description) {
description.appendText("has item at position " + position + ": ");
matcher.describeTo(description);
}
#Override
protected boolean matchesSafely(RecyclerView recyclerView) {
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position);
return matcher.matches(viewHolder.itemView);
}
};
}
We pass Matcher to the function so we can provide further conditions. Example usage:
onView(hasItemAtPosition(hasDescendant(withText("Item 1")), 0)).check(matches(isDisplayed()));
onView(hasItemAtPosition(hasDescendant(withText("Item 2")), 1)).check(matches(isDisplayed()));
The original problem has been solved but am posting an answer here as found the Barista library solves this problem in one single line of code.
assertDisplayedAtPosition(R.id.rv_metrics, 0, R.id.tv_title, "weight");
It's made on top of Espresso and the documentation for it can be found here
Hope this may be helpful to someone. :)
If you want to match a matcher on a position in RecyclerView, then you can try to create a custom Matcher<View>:
public static Matcher<View> hasItemAtPosition(int position, Matcher<View> matcher) {
return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {
#Override public void describeTo(Description description) {
description.appendText("has item: ");
matcher.describeTo(description);
description.appendText(" at position: " + position);
}
#Override protected boolean matchesSafely(RecyclerView view) {
RecyclerView.Adapter adapter = view.getAdapter();
int type = adapter.getItemViewType(position);
RecyclerView.ViewHolder holder = adapter.createViewHolder(view, type);
adapter.onBindViewHolder(holder, position);
return matcher.matches(holder.itemView);
}
};
}
And you can use it for example:
onView(withId(R.id.rv_metrics)).check(matches(0, hasDescendant(withText("Weight")))))
I am using parse-server to develop the app which uses RecyclerView to display image items.
but the problem is that the items displayed in the view changed every time I scrolled up and down.
I want to know what is the problem on my code.
if you see below images, you can find the items are changing their position.
I tried to make holder image become null before call the holder again. but it's not working. I guess that the item's position number is changed when I call the item again.but I can't find the cause of the situation
enter image description here
enter image description here
RecyclerParseAdapter.java
public class MyTimelineAdapter extends RecyclerParseAdapter {
private interface OnQueryLoadListener<ParseObject> {
public void onLoading();
public void onLoaded(List<ParseObject> objects, Exception e);
}
private static ParseQueryAdapter.QueryFactory<ParseObject> queryFactory;
private static List<OnQueryLoadListener<ParseObject>> onQueryLoadListeners;
private static List<List<ParseObject>> objectPages;
private static ArrayList<ParseObject> items;
private static int currentPage;
private static RequestManager requestManager;
public MyTimelineAdapter(Context context, RequestManager requestManager) {
super(context);
this.requestManager = requestManager;
this.onQueryLoadListeners = new ArrayList<>();
this.currentPage = 0;
this.objectPages = new ArrayList<>();
this.items = new ArrayList<>();
this.queryFactory = new ParseQueryAdapter.QueryFactory<ParseObject>() {
#Override
public ParseQuery<ParseObject> create() {
ParseQuery<ParseObject> query = ParseQuery.getQuery("ImageClassName");
query.setCachePolicy(ParseQuery.CachePolicy.CACHE_THEN_NETWORK);
query.whereEqualTo("status", true);
query.orderByDescending("createdAt");
return query;
}
};
loadObjects(currentPage);
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View timelineView;
timelineView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_timeline_item2, parent, false);
TimelineItemViewHolder timelineItemViewHolder = new TimelineItemViewHolder(timelineView);
return timelineItemViewHolder;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final ParseObject timelineOb = getItem(position);
FunctionPost functionPost = new FunctionPost(context);
functionPost.TimelineArtistPostAdapterBuilder( timelineOb, holder, requestManager);
//기능 추가
}
#Override
public int getItemCount() {
return items.size();
}
#Override
public ParseObject getItem(int position) {
return items.get(position);
}
#Override
public void loadObjects(final int page) {
final ParseQuery<ParseObject> query = this.queryFactory.create();
if (this.objectsPerPage > 0 && this.paginationEnabled) {
this.setPageOnQuery(page, query);
}
this.notifyOnLoadingListeners();
if (page >= objectPages.size()) {
objectPages.add(page, new ArrayList<ParseObject>());
}
query.findInBackground(new FindCallback<ParseObject>() {
#Override
public void done(List<ParseObject> foundObjects, ParseException e) {
if ((e != null) && ((e.getCode() == ParseException.CONNECTION_FAILED) || (e.getCode() != ParseException.CACHE_MISS))) {
hasNextPage = true;
} else if (foundObjects != null) {
// Only advance the page, this prevents second call back from CACHE_THEN_NETWORK to
// reset the page.
if (page >= currentPage) {
currentPage = page;
// since we set limit == objectsPerPage + 1
hasNextPage = (foundObjects.size() > objectsPerPage);
}
if (paginationEnabled && foundObjects.size() > objectsPerPage) {
// Remove the last object, fetched in order to tell us whether there was a "next page"
foundObjects.remove(objectsPerPage);
}
List<ParseObject> currentPage = objectPages.get(page);
currentPage.clear();
currentPage.addAll(foundObjects);
syncObjectsWithPages(items, objectPages);
// executes on the UI thread
notifyDataSetChanged();
}
notifyOnLoadedListeners(foundObjects, e);
}
});
}
public void loadNextPage() {
if (items.size() == 0) {
loadObjects(0);
} else {
loadObjects(currentPage + 1);
}
}
public void syncObjectsWithPages(ArrayList<ParseObject> items, List<List<ParseObject>> objectPages) {
items.clear();
for (List<ParseObject> pageOfObjects : objectPages) {
items.addAll(pageOfObjects);
}
}
protected void setPageOnQuery(int page, ParseQuery<ParseObject> query) {
query.setLimit(this.objectsPerPage + 1);
query.setSkip(page * this.objectsPerPage);
}
public void addOnQueryLoadListener(OnQueryLoadListener<ParseObject> listener) {
this.onQueryLoadListeners.add(listener);
}
public void removeOnQueryLoadListener(OnQueryLoadListener<ParseObject> listener) {
this.onQueryLoadListeners.remove(listener);
}
public void notifyOnLoadingListeners() {
for (OnQueryLoadListener<ParseObject> listener : this.onQueryLoadListeners) {
listener.onLoading();
}
}
public void notifyOnLoadedListeners(List<ParseObject> objects, Exception e) {
for (OnQueryLoadListener<ParseObject> listener : this.onQueryLoadListeners) {
listener.onLoaded(objects, e);
}
}
}
I did find the problem
I add overide method in the adapter then It works find.
#Override
public int getItemViewType(int position) {
return position;
}
I am not sure why it happens now. any one help me to know the cause of problem?
I has a similar problem the other day see this post. onBindViewHolder needs to know how to display the row when it's called. I returned two different view types depending on the need in getItemViewType, inflated the view type conditionally in onCreateViewHolder, then I was able to set the data on the ViewHolder as needed.
Using Espresso and Hamcrest,
How can I count items number available in a recyclerView?
Exemple: I would like check if 5 items are displaying in a specific RecyclerView (scrolling if necessary).
Here an example ViewAssertion to check RecyclerView item count
public class RecyclerViewItemCountAssertion implements ViewAssertion {
private final int expectedCount;
public RecyclerViewItemCountAssertion(int expectedCount) {
this.expectedCount = expectedCount;
}
#Override
public void check(View view, NoMatchingViewException noViewFoundException) {
if (noViewFoundException != null) {
throw noViewFoundException;
}
RecyclerView recyclerView = (RecyclerView) view;
RecyclerView.Adapter adapter = recyclerView.getAdapter();
assertThat(adapter.getItemCount(), is(expectedCount));
}
}
and then use this assertion
onView(withId(R.id.recyclerView)).check(new RecyclerViewItemCountAssertion(5));
I have started to write an library which should make testing more simple with espresso and uiautomator. This includes tooling for RecyclerView action and assertions. https://github.com/nenick/espresso-macchiato See for example EspRecyclerView with the method assertItemCountIs(int)
Adding a bit of syntax sugar to the #Stephane's answer.
public class RecyclerViewItemCountAssertion implements ViewAssertion {
private final Matcher<Integer> matcher;
public static RecyclerViewItemCountAssertion withItemCount(int expectedCount) {
return withItemCount(is(expectedCount));
}
public static RecyclerViewItemCountAssertion withItemCount(Matcher<Integer> matcher) {
return new RecyclerViewItemCountAssertion(matcher);
}
private RecyclerViewItemCountAssertion(Matcher<Integer> matcher) {
this.matcher = matcher;
}
#Override
public void check(View view, NoMatchingViewException noViewFoundException) {
if (noViewFoundException != null) {
throw noViewFoundException;
}
RecyclerView recyclerView = (RecyclerView) view;
RecyclerView.Adapter adapter = recyclerView.getAdapter();
assertThat(adapter.getItemCount(), matcher);
}
}
Usage:
import static your.package.RecyclerViewItemCountAssertion.withItemCount;
onView(withId(R.id.recyclerView)).check(withItemCount(5));
onView(withId(R.id.recyclerView)).check(withItemCount(greaterThan(5)));
onView(withId(R.id.recyclerView)).check(withItemCount(lessThan(5)));
// ...
To complete nenick answer and provide and little bit more flexible solution to also test if item cout is greaterThan, lessThan ...
public class RecyclerViewItemCountAssertion implements ViewAssertion {
private final Matcher<Integer> matcher;
public RecyclerViewItemCountAssertion(int expectedCount) {
this.matcher = is(expectedCount);
}
public RecyclerViewItemCountAssertion(Matcher<Integer> matcher) {
this.matcher = matcher;
}
#Override
public void check(View view, NoMatchingViewException noViewFoundException) {
if (noViewFoundException != null) {
throw noViewFoundException;
}
RecyclerView recyclerView = (RecyclerView) view;
RecyclerView.Adapter adapter = recyclerView.getAdapter();
assertThat(adapter.getItemCount(), matcher);
}
}
Usage:
onView(withId(R.id.recyclerView)).check(new RecyclerViewItemCountAssertion(5));
onView(withId(R.id.recyclerView)).check(new RecyclerViewItemCountAssertion(greaterThan(5));
onView(withId(R.id.recyclerView)).check(new RecyclerViewItemCountAssertion(lessThan(5));
// ...
Validated answer works but we can solve this problem with one line and without adapter awareness :
onView(withId(R.id.your_recycler_view_id)).check(matches(hasChildCount(2)))
Replace your_recycler_view_id with your id and 2 with the number to assert.
Based on #Sivakumar Kamichetty answer:
Variable 'COUNT' is accessed from within inner class, needs to be declared final.
Unnecessarily line: COUNT = 0;
Transfer COUNT variable to one element array.
Variable result is unnecessary.
Not nice, but works:
public static int getCountFromRecyclerView(#IdRes int RecyclerViewId) {
final int[] COUNT = {0};
Matcher matcher = new TypeSafeMatcher<View>() {
#Override
protected boolean matchesSafely(View item) {
COUNT[0] = ((RecyclerView) item).getAdapter().getItemCount();
return true;
}
#Override
public void describeTo(Description description) {}
};
onView(allOf(withId(RecyclerViewId),isDisplayed())).check(matches(matcher));
return COUNT[0];
}
I used the below method to get the count of RecyclerView
public static int getCountFromRecyclerView(#IdRes int RecyclerViewId) {
int COUNT = 0;
Matcher matcher = new TypeSafeMatcher<View>() {
#Override
protected boolean matchesSafely(View item) {
COUNT = ((RecyclerView) item).getAdapter().getItemCount();
return true;
}
#Override
public void describeTo(Description description) {
}
};
onView(allOf(withId(RecyclerViewId),isDisplayed())).check(matches(matcher));
int result = COUNT;
COUNT = 0;
return result;
}
Usage -
int itemsCount = getCountFromRecyclerView(R.id.RecyclerViewId);
Then perform assertions to check if the itemsCount is as expected
You can create a custom BoundedMatcher:
object RecyclerViewMatchers {
#JvmStatic
fun hasItemCount(itemCount: Int): Matcher<View> {
return object : BoundedMatcher<View, RecyclerView>(
RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has $itemCount items")
}
override fun matchesSafely(view: RecyclerView): Boolean {
return view.adapter.itemCount == itemCount
}
}
}
}
And then use it like this:
onView(withId(R.id.recycler_view)).check(matches((hasItemCount(5))))
count with ActivityScenarioRule
#get: Rule
val activityScenarioRule = ActivityScenarioRule(ShowListActivity::class.java)
#Test
fun testItemCount(){
activityScenarioRule.scenario.onActivity { activityScenarioRule ->
val recyclerView = activityScenarioRule.findViewById<RecyclerView(R.id.movieListRecyclerView)
val itemCount = recyclerView.adapter?.itemCount?:0
....
}
}
I want to create a customized ListView (or similar) which will behave like a closed (circular) one:
scrolling down - after the last item was reached the first begins (.., n-1, n, 1, 2, ..)
scrolling upward - after the first item was reached the last begins (.., 2, 1, n, n-1, ..)
It sounds simple conceptually but, apparently, there is no straightforward approach to do this.
Can anyone point me to the right solution ?
Thank you !
I have already received an answer (from Streets Of Boston on Android-Developers google groups), but it sounds somehow ugly :) -
I did this by creating my own
list-adapter (subclassed from
BaseAdapter).
I coded my own list-adapter in such a
way that its getCount() method returns
a HUUUUGE number.
And if item 'x' is selected, then this
item corresponds to adapter
position='adapter.getCount()/2+x'
And for my adapter's method
getItem(int position), i look in my
array that backs up the adapter and
fetch the item on index:
(position-getCount()/2) % myDataItems.length
You need to do some more 'special'
stuff to make it all work correctly,
but you get the idea.
In principle, it is still possible to
reach the end or the beginning of the
list, but if you set getCount() to
around a million or so, this is hard
to do :-)
My colleague Joe, and I believe we have found a simpler way to solve the same problem. In our solution though instead of extending BaseAdapter we extend ArrayAdapter.
The code is as follows :
public class CircularArrayAdapter< T > extends ArrayAdapter< T >
{
public static final int HALF_MAX_VALUE = Integer.MAX_VALUE/2;
public final int MIDDLE;
private T[] objects;
public CircularArrayAdapter(Context context, int textViewResourceId, T[] objects)
{
super(context, textViewResourceId, objects);
this.objects = objects;
MIDDLE = HALF_MAX_VALUE - HALF_MAX_VALUE % objects.length;
}
#Override
public int getCount()
{
return Integer.MAX_VALUE;
}
#Override
public T getItem(int position)
{
return objects[position % objects.length];
}
}
So this creates a class called CircularArrayAdapter which take an object type T (which may be anything) and uses it to create an array list. T is commonly a string though may be anything.
The constructor is the same as is for ArrayAdapter though initializes a constant called middle. This is the middle of the list. No matter what the length of the array MIDDLE can be used to center the ListView in the mid of the list.
getCount() is overrides to return a huge value as is done above creating a huge list.
getItem() is overrides to return the fake position on the array. Thus when filling the list the list is filled with objects in a looping manner.
At this point CircularArrayAdapter simply replaces ArrayAdapter in the file creating the ListView.
To centre the ListView the fallowing line must be inserted in your file creating the ListView after the ListView object has been initialised:
listViewObject.setSelectionFromTop(nameOfAdapterObject.MIDDLE, 0);
and using the MIDDLE constant previously initialized for the list the view is centered with the top item of the list at the top of the screen.
: ) ~ Cheers, I hope this solution is useful.
The solution you mention is the one I told other developers to use in the past. In getCount(), simply return Integer.MAX_VALUE, it will give you about 2 billion items, which should be enough.
I have, or I think I have done it right, based on the answers above.
Hope this will help you.
private static class RecipeListAdapter extends BaseAdapter {
private static LayoutInflater mInflater;
private Integer[] mCouponImages;
private static ImageView viewHolder;
public RecipeListAdapter(Context c, Integer[] coupomImages) {
RecipeListAdapter.mInflater = LayoutInflater.from(c);
this.mCouponImages = coupomImages;
}
#Override
public int getCount() {
return Integer.MAX_VALUE;
}
#Override
public Object getItem(int position) {
// you can do your own tricks here. to let it display the right item in your array.
return position % mCouponImages.length;
}
#Override
public long getItemId(int position) {
return position;
// return position % mCouponImages.length;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.coupon_list_item, null);
viewHolder = (ImageView) convertView.findViewById(R.id.item_coupon);
convertView.setTag(viewHolder);
} else {
viewHolder = (ImageView) convertView.getTag();
}
viewHolder.setImageResource(this.mCouponImages[position % mCouponImages.length]);
return convertView;
}
}
And you would like to do this if you want to scroll down the list.
Commonly we can just scroll up and list then scroll down.
// see how many items we would like to sroll. in this case, Integer.MAX_VALUE
int listViewLength = adapter.getCount();
// see how many items a screen can dispaly, I use variable "span"
final int span = recipeListView.getLastVisiblePosition() - recipeListView.getFirstVisiblePosition();
// see how many pages we have
int howManySpans = listViewLength / span;
// see where do you want to be when start the listview. you dont have to do the "-3" stuff.
it is for my app to work right.
recipeListView.setSelection((span * (howManySpans / 2)) - 3);
I could see some good answers for this, One of my friend has tried to achieve this via a simple solution. Check the github project.
If using LoadersCallbacks I have created MyCircularCursor class which wraps the typical cursor like this:
#Override
public void onLoadFinished(Loader<Cursor> pCursorLoader, Cursor pCursor) {
mItemListAdapter.swapCursor(new MyCircularCursor(pCursor));
}
the decorator class code is here:
public class MyCircularCursor implements Cursor {
private Cursor mCursor;
public MyCircularCursor(Cursor pCursor) {
mCursor = pCursor;
}
#Override
public int getCount() {
return mCursor.getCount() == 0 ? 0 : Integer.MAX_VALUE;
}
#Override
public int getPosition() {
return mCursor.getPosition();
}
#Override
public boolean move(int pOffset) {
return mCursor.move(pOffset);
}
#Override
public boolean moveToPosition(int pPosition) {
int position = MathUtils.mod(pPosition, mCursor.getCount());
return mCursor.moveToPosition(position);
}
#Override
public boolean moveToFirst() {
return mCursor.moveToFirst();
}
#Override
public boolean moveToLast() {
return mCursor.moveToLast();
}
#Override
public boolean moveToNext() {
if (mCursor.isLast()) {
mCursor.moveToFirst();
return true;
} else {
return mCursor.moveToNext();
}
}
#Override
public boolean moveToPrevious() {
if (mCursor.isFirst()) {
mCursor.moveToLast();
return true;
} else {
return mCursor.moveToPrevious();
}
}
#Override
public boolean isFirst() {
return false;
}
#Override
public boolean isLast() {
return false;
}
#Override
public boolean isBeforeFirst() {
return false;
}
#Override
public boolean isAfterLast() {
return false;
}
#Override
public int getColumnIndex(String pColumnName) {
return mCursor.getColumnIndex(pColumnName);
}
#Override
public int getColumnIndexOrThrow(String pColumnName) throws IllegalArgumentException {
return mCursor.getColumnIndexOrThrow(pColumnName);
}
#Override
public String getColumnName(int pColumnIndex) {
return mCursor.getColumnName(pColumnIndex);
}
#Override
public String[] getColumnNames() {
return mCursor.getColumnNames();
}
#Override
public int getColumnCount() {
return mCursor.getColumnCount();
}
#Override
public byte[] getBlob(int pColumnIndex) {
return mCursor.getBlob(pColumnIndex);
}
#Override
public String getString(int pColumnIndex) {
return mCursor.getString(pColumnIndex);
}
#Override
public short getShort(int pColumnIndex) {
return mCursor.getShort(pColumnIndex);
}
#Override
public int getInt(int pColumnIndex) {
return mCursor.getInt(pColumnIndex);
}
#Override
public long getLong(int pColumnIndex) {
return mCursor.getLong(pColumnIndex);
}
#Override
public float getFloat(int pColumnIndex) {
return mCursor.getFloat(pColumnIndex);
}
#Override
public double getDouble(int pColumnIndex) {
return mCursor.getDouble(pColumnIndex);
}
#Override
public int getType(int pColumnIndex) {
return 0;
}
#Override
public boolean isNull(int pColumnIndex) {
return mCursor.isNull(pColumnIndex);
}
#Override
public void deactivate() {
mCursor.deactivate();
}
#Override
#Deprecated
public boolean requery() {
return mCursor.requery();
}
#Override
public void close() {
mCursor.close();
}
#Override
public boolean isClosed() {
return mCursor.isClosed();
}
#Override
public void registerContentObserver(ContentObserver pObserver) {
mCursor.registerContentObserver(pObserver);
}
#Override
public void unregisterContentObserver(ContentObserver pObserver) {
mCursor.unregisterContentObserver(pObserver);
}
#Override
public void registerDataSetObserver(DataSetObserver pObserver) {
mCursor.registerDataSetObserver(pObserver);
}
#Override
public void unregisterDataSetObserver(DataSetObserver pObserver) {
mCursor.unregisterDataSetObserver(pObserver);
}
#Override
public void setNotificationUri(ContentResolver pCr, Uri pUri) {
mCursor.setNotificationUri(pCr, pUri);
}
#Override
public boolean getWantsAllOnMoveCalls() {
return mCursor.getWantsAllOnMoveCalls();
}
#Override
public Bundle getExtras() {
return mCursor.getExtras();
}
#Override
public Bundle respond(Bundle pExtras) {
return mCursor.respond(pExtras);
}
#Override
public void copyStringToBuffer(int pColumnIndex, CharArrayBuffer pBuffer) {
mCursor.copyStringToBuffer(pColumnIndex, pBuffer);
}
}
I want to create a customized ListView (or similar) which will behave like a closed (circular) one:
scrolling down - after the last item was reached the first begins (.., n-1, n, 1, 2, ..)
scrolling upward - after the first item was reached the last begins (.., 2, 1, n, n-1, ..)
It sounds simple conceptually but, apparently, there is no straightforward approach to do this.
Can anyone point me to the right solution ?
Thank you !
I have already received an answer (from Streets Of Boston on Android-Developers google groups), but it sounds somehow ugly :) -
I did this by creating my own
list-adapter (subclassed from
BaseAdapter).
I coded my own list-adapter in such a
way that its getCount() method returns
a HUUUUGE number.
And if item 'x' is selected, then this
item corresponds to adapter
position='adapter.getCount()/2+x'
And for my adapter's method
getItem(int position), i look in my
array that backs up the adapter and
fetch the item on index:
(position-getCount()/2) % myDataItems.length
You need to do some more 'special'
stuff to make it all work correctly,
but you get the idea.
In principle, it is still possible to
reach the end or the beginning of the
list, but if you set getCount() to
around a million or so, this is hard
to do :-)
My colleague Joe, and I believe we have found a simpler way to solve the same problem. In our solution though instead of extending BaseAdapter we extend ArrayAdapter.
The code is as follows :
public class CircularArrayAdapter< T > extends ArrayAdapter< T >
{
public static final int HALF_MAX_VALUE = Integer.MAX_VALUE/2;
public final int MIDDLE;
private T[] objects;
public CircularArrayAdapter(Context context, int textViewResourceId, T[] objects)
{
super(context, textViewResourceId, objects);
this.objects = objects;
MIDDLE = HALF_MAX_VALUE - HALF_MAX_VALUE % objects.length;
}
#Override
public int getCount()
{
return Integer.MAX_VALUE;
}
#Override
public T getItem(int position)
{
return objects[position % objects.length];
}
}
So this creates a class called CircularArrayAdapter which take an object type T (which may be anything) and uses it to create an array list. T is commonly a string though may be anything.
The constructor is the same as is for ArrayAdapter though initializes a constant called middle. This is the middle of the list. No matter what the length of the array MIDDLE can be used to center the ListView in the mid of the list.
getCount() is overrides to return a huge value as is done above creating a huge list.
getItem() is overrides to return the fake position on the array. Thus when filling the list the list is filled with objects in a looping manner.
At this point CircularArrayAdapter simply replaces ArrayAdapter in the file creating the ListView.
To centre the ListView the fallowing line must be inserted in your file creating the ListView after the ListView object has been initialised:
listViewObject.setSelectionFromTop(nameOfAdapterObject.MIDDLE, 0);
and using the MIDDLE constant previously initialized for the list the view is centered with the top item of the list at the top of the screen.
: ) ~ Cheers, I hope this solution is useful.
The solution you mention is the one I told other developers to use in the past. In getCount(), simply return Integer.MAX_VALUE, it will give you about 2 billion items, which should be enough.
I have, or I think I have done it right, based on the answers above.
Hope this will help you.
private static class RecipeListAdapter extends BaseAdapter {
private static LayoutInflater mInflater;
private Integer[] mCouponImages;
private static ImageView viewHolder;
public RecipeListAdapter(Context c, Integer[] coupomImages) {
RecipeListAdapter.mInflater = LayoutInflater.from(c);
this.mCouponImages = coupomImages;
}
#Override
public int getCount() {
return Integer.MAX_VALUE;
}
#Override
public Object getItem(int position) {
// you can do your own tricks here. to let it display the right item in your array.
return position % mCouponImages.length;
}
#Override
public long getItemId(int position) {
return position;
// return position % mCouponImages.length;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.coupon_list_item, null);
viewHolder = (ImageView) convertView.findViewById(R.id.item_coupon);
convertView.setTag(viewHolder);
} else {
viewHolder = (ImageView) convertView.getTag();
}
viewHolder.setImageResource(this.mCouponImages[position % mCouponImages.length]);
return convertView;
}
}
And you would like to do this if you want to scroll down the list.
Commonly we can just scroll up and list then scroll down.
// see how many items we would like to sroll. in this case, Integer.MAX_VALUE
int listViewLength = adapter.getCount();
// see how many items a screen can dispaly, I use variable "span"
final int span = recipeListView.getLastVisiblePosition() - recipeListView.getFirstVisiblePosition();
// see how many pages we have
int howManySpans = listViewLength / span;
// see where do you want to be when start the listview. you dont have to do the "-3" stuff.
it is for my app to work right.
recipeListView.setSelection((span * (howManySpans / 2)) - 3);
I could see some good answers for this, One of my friend has tried to achieve this via a simple solution. Check the github project.
If using LoadersCallbacks I have created MyCircularCursor class which wraps the typical cursor like this:
#Override
public void onLoadFinished(Loader<Cursor> pCursorLoader, Cursor pCursor) {
mItemListAdapter.swapCursor(new MyCircularCursor(pCursor));
}
the decorator class code is here:
public class MyCircularCursor implements Cursor {
private Cursor mCursor;
public MyCircularCursor(Cursor pCursor) {
mCursor = pCursor;
}
#Override
public int getCount() {
return mCursor.getCount() == 0 ? 0 : Integer.MAX_VALUE;
}
#Override
public int getPosition() {
return mCursor.getPosition();
}
#Override
public boolean move(int pOffset) {
return mCursor.move(pOffset);
}
#Override
public boolean moveToPosition(int pPosition) {
int position = MathUtils.mod(pPosition, mCursor.getCount());
return mCursor.moveToPosition(position);
}
#Override
public boolean moveToFirst() {
return mCursor.moveToFirst();
}
#Override
public boolean moveToLast() {
return mCursor.moveToLast();
}
#Override
public boolean moveToNext() {
if (mCursor.isLast()) {
mCursor.moveToFirst();
return true;
} else {
return mCursor.moveToNext();
}
}
#Override
public boolean moveToPrevious() {
if (mCursor.isFirst()) {
mCursor.moveToLast();
return true;
} else {
return mCursor.moveToPrevious();
}
}
#Override
public boolean isFirst() {
return false;
}
#Override
public boolean isLast() {
return false;
}
#Override
public boolean isBeforeFirst() {
return false;
}
#Override
public boolean isAfterLast() {
return false;
}
#Override
public int getColumnIndex(String pColumnName) {
return mCursor.getColumnIndex(pColumnName);
}
#Override
public int getColumnIndexOrThrow(String pColumnName) throws IllegalArgumentException {
return mCursor.getColumnIndexOrThrow(pColumnName);
}
#Override
public String getColumnName(int pColumnIndex) {
return mCursor.getColumnName(pColumnIndex);
}
#Override
public String[] getColumnNames() {
return mCursor.getColumnNames();
}
#Override
public int getColumnCount() {
return mCursor.getColumnCount();
}
#Override
public byte[] getBlob(int pColumnIndex) {
return mCursor.getBlob(pColumnIndex);
}
#Override
public String getString(int pColumnIndex) {
return mCursor.getString(pColumnIndex);
}
#Override
public short getShort(int pColumnIndex) {
return mCursor.getShort(pColumnIndex);
}
#Override
public int getInt(int pColumnIndex) {
return mCursor.getInt(pColumnIndex);
}
#Override
public long getLong(int pColumnIndex) {
return mCursor.getLong(pColumnIndex);
}
#Override
public float getFloat(int pColumnIndex) {
return mCursor.getFloat(pColumnIndex);
}
#Override
public double getDouble(int pColumnIndex) {
return mCursor.getDouble(pColumnIndex);
}
#Override
public int getType(int pColumnIndex) {
return 0;
}
#Override
public boolean isNull(int pColumnIndex) {
return mCursor.isNull(pColumnIndex);
}
#Override
public void deactivate() {
mCursor.deactivate();
}
#Override
#Deprecated
public boolean requery() {
return mCursor.requery();
}
#Override
public void close() {
mCursor.close();
}
#Override
public boolean isClosed() {
return mCursor.isClosed();
}
#Override
public void registerContentObserver(ContentObserver pObserver) {
mCursor.registerContentObserver(pObserver);
}
#Override
public void unregisterContentObserver(ContentObserver pObserver) {
mCursor.unregisterContentObserver(pObserver);
}
#Override
public void registerDataSetObserver(DataSetObserver pObserver) {
mCursor.registerDataSetObserver(pObserver);
}
#Override
public void unregisterDataSetObserver(DataSetObserver pObserver) {
mCursor.unregisterDataSetObserver(pObserver);
}
#Override
public void setNotificationUri(ContentResolver pCr, Uri pUri) {
mCursor.setNotificationUri(pCr, pUri);
}
#Override
public boolean getWantsAllOnMoveCalls() {
return mCursor.getWantsAllOnMoveCalls();
}
#Override
public Bundle getExtras() {
return mCursor.getExtras();
}
#Override
public Bundle respond(Bundle pExtras) {
return mCursor.respond(pExtras);
}
#Override
public void copyStringToBuffer(int pColumnIndex, CharArrayBuffer pBuffer) {
mCursor.copyStringToBuffer(pColumnIndex, pBuffer);
}
}