In my test, after one action, there are two possible views which can appear and both of them are correct. How can I check if one of the view is displayed. For a single view I can check with is Displayed(). But that would fail if other view is visible instead. I want to pass the test if any one of those two views are displayed.
onMyButton.perform(click());
onMyPageOne.check(matches(isDisplayed())); //view 1
or
onMyPageTwo.check(matches(isDisplayed())); //view 2
After, perform click on MyButton, any one of the view (1 or 2) is expected to appear but not both. It is not fixed that which one would be displayed.
How can I check if any one of them is displayed?
It's possible to catch the exceptions raised by Espresso like this:
If you want to test if a view is in hierarchy:
try {
onView(withText("Button")).perform(click());
// View is in hierarchy
} catch (NoMatchingViewException e) {
// View is not in hierarchy
}
This exception will be thrown if the view is not in the hierarchy.
Sometimes the view can be in the hierarchy, but we need to test if it is displayed, so there is another exception for assertions, like this:
try {
onView(withText("Button")).check(matches(isDisplayed()));
// View is displayed
} catch (AssertionFailedError e) {
// View not displayed
}
There are two cases here that you could be trying to cover. The first is if you are checking if the view "is displayed on the screen to the user" in which case you would use isDisplayed()
onView(matcher).check(matches(isDisplayed()));
or the negation
onView(matcher).check(matches(not(isDisplayed())));
The other case is if you are checking if the view is visible but not necessarily displayed on the screen (ie. an item in a scrollview). For this you can use withEffectiveVisibility(Visibility)
onView(matcher).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
You can use Matchers.anyOf to check if any of the two views are displayed:
onView(
anyOf(withId(R.id.view_1), withId(R.id.view_2))
).check(matches(isDisplayed()));
For the ones looking to check the visibility status for a view; here are some utility functions I use.
fun ViewInteraction.isGone() = getViewAssertion(ViewMatchers.Visibility.GONE)
fun ViewInteraction.isVisible() = getViewAssertion(ViewMatchers.Visibility.VISIBLE)
fun ViewInteraction.isInvisible() = getViewAssertion(ViewMatchers.Visibility.INVISIBLE)
private fun getViewAssertion(visibility: ViewMatchers.Visibility): ViewAssertion? {
return ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(visibility))
}
And can be used as follows
onView(withId(R.id.progressBar)).isVisible()
onView(withId(R.id.progressBar)).isGone()
I researched Espresso a bit, and I found this # Espresso Samples.
Search text "Asserting that a view is not displayed". It says "The above approach works if the view is still part of the hierarchy." So I think your code should work but you need to use ViewAssertions also. Using your code, perhaps do this:
if (ViewAssertions.doesNotExist()) == null) {
return;
}
onMyPageOne.check(matches(isDisplayed()));
Another technique is check for UI existence. Search for text "Asserting that a view is not present".
Using your code, my best suggestion is:
onMyPageOne.check(doesNotExist());
Note: This calls doesNotExist method.
Their sample code is: onView(withId(R.id.bottom_left)).check(doesNotExist());
Utility class which allows to check if view is visible, gone or invisible:
public class ExtraAssertions {
public static ViewAssertion isVisible() {
return new ViewAssertion() {
public void check(View view, NoMatchingViewException noView) {
assertThat(view, new VisibilityMatcher(View.VISIBLE));
}
};
}
public static ViewAssertion isGone() {
return new ViewAssertion() {
public void check(View view, NoMatchingViewException noView) {
assertThat(view, new VisibilityMatcher(View.GONE));
}
};
}
public static ViewAssertion isInvisible() {
return new ViewAssertion() {
public void check(View view, NoMatchingViewException noView) {
assertThat(view, new VisibilityMatcher(View.INVISIBLE));
}
};
}
private static class VisibilityMatcher extends BaseMatcher<View> {
private int visibility;
public VisibilityMatcher(int visibility) {
this.visibility = visibility;
}
#Override public void describeTo(Description description) {
String visibilityName;
if (visibility == View.GONE) visibilityName = "GONE";
else if (visibility == View.VISIBLE) visibilityName = "VISIBLE";
else visibilityName = "INVISIBLE";
description.appendText("View visibility must has equals " + visibilityName);
}
#Override public boolean matches(Object o) {
if (o == null) {
if (visibility == View.GONE || visibility == View.INVISIBLE) return true;
else if (visibility == View.VISIBLE) return false;
}
if (!(o instanceof View))
throw new IllegalArgumentException("Object must be instance of View. Object is instance of " + o);
return ((View) o).getVisibility() == visibility;
}
}
}
And usage could look like this:
onView(withId(R.id.text_message)).check(isVisible());
Another view assertion which could help to check extra visibility properties of a view and its parents: it checks visibility, isAttachedToWindow, alpha:
class IsVisible : ViewAssertion {
override fun check(view: View, noViewFoundException: NoMatchingViewException?) {
ViewMatchers.assertThat(
"View is not visible. " +
"visibility: ${view.visibility}, " +
"isAttachedToWindow: ${view.isAttachedToWindow}, " +
"alpha: ${view.alpha}",
true, `is`(isViewTreeVisible(view)))
}
private fun isViewTreeVisible(view: View?): Boolean {
return if (view != null) {
val viewVisible = view.isAttachedToWindow && view.visibility == View.VISIBLE && view.alpha == 1.0f
if (view.parent !is View) viewVisible
else viewVisible && isViewTreeVisible(view.parent as View)
} else {
true
}
}
}
The problem is that all assertoin() and check() methods return Assertion that stops test flow if failed.
One simple way to check for a View or its subclass like a Button is to use method
getVisibility from View class. I must caution that visibility attribute is not clearly defined in the GUI world. A view may be considered visible but may be overlapped with another view, for one example, making it hidden.
Another way but more accurate (I have not tried) is to check for the rectangular bounds of the View. Not so simple.
Is that clear enough? cannot give you specific examples since you did not post code.
final AtomicBoolean view1Displayed = new AtomicBoolean(true);
Espresso.onView(ViewMatchers.withId(viewId1)).inRoot(RootMatchers.withDecorView(Matchers.is(intentsTestRule.getActivity().getWindow().getDecorView()))).withFailureHandler(new FailureHandler() {
#Override
public void handle(Throwable error, Matcher<View> viewMatcher) {
view1Displayed.set(false);
}
}).check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
if (view1Displayed.get()) {
try {
Espresso.onView(ViewMatchers.withId(viewId2)).inRoot(RootMatchers.withDecorView(Matchers.is(intentsTestRule.getActivity().getWindow().getDecorView()))).check(ViewAssertions.matches(Matchers.not(ViewMatchers.isDisplayed())));
} catch (NoMatchingViewException ignore) {
}
} else {
Espresso.onView(ViewMatchers.withId(viewId2)).inRoot(RootMatchers.withDecorView(Matchers.is(intentsTestRule.getActivity().getWindow().getDecorView()))).check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
}
When I face this situation I generally split into multiple tests. One test sets the conditions for view #1 to be displayed and the other test sets the conditions for view #2 to be displayed.
But let's say that you can't really control the conditions. For example, what if it depends on a random number or it depends on a third-party resource such as a calculation on a server? In that case, I usually solve the problem mocking. That way I can control the conditions so I know exactly which view to expect. I use Dependency Injection to set the mock I need for each test.
Related
There's something that android TalkBack does that I want to do too. Specifically it's to identify the view that the user touched. I made this so far:
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
//This only shows the package of the activity that the view is in, I want to identify the view itself
Toast.makeText(this, "" + event.getSource().getPackageName(), Toast.LENGTH_LONG).show();
}
}
You can get the view's id, but I think you can only access the AccessibilityNodeInfo associated with the view - I don't think you have access to the view itself. You can check the docs for more information on this.
// kotlin code
event.source.viewIdResourceName
// or
findFocusedViewInfo().viewIdResourceName
According to the docs:
Gets the fully qualified resource name of the source view's id.Note: The primary usage of this API is for UI test automation and in order to report the source view id of an AccessibilityNodeInfo the client has to set the AccessibilityServiceInfo#FLAG_REPORT_VIEW_IDS flag when configuring the AccessibilityService.
the question is not clear, but maybe this will help you:-
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
AccessibilityNodeInfo source = event.getSource();
if (source == null) {
return;
}
List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId = source.findAccessibilityNodeInfosByViewId("YOUR PACKAGE NAME:id/RESOURCE ID FROM WHERE YOU WANT DATA");
if (findAccessibilityNodeInfosByViewId.size() > 0) {
AccessibilityNodeInfo parent = (AccessibilityNodeInfo) findAccessibilityNodeInfosByViewId.get(0);
// You can also traverse the list if required data is deep in view hierarchy.
String requiredText = parent.getText().toString();
Log.i("Required Text", requiredText);
}
}
also read this tutorial
This gets called when a button is clicked
#Override
public void onFavoriteIconClicked() {
viewModel.isFavoriteExist(test.getId()).observe(getViewLifecycleOwner(), new Observer<Boolean>() {
#Override
public void onChanged(Boolean aBoolean) {
viewModel.isFavoriteExist(test.getId()).removeObserver(this);
if (aBoolean) {
binding.addToFavorite.setImageResource(R.drawable.non_fav);
viewModel.delete(test);
} else if (getActivity() != null) {
Test test2 = new Test(test.getId(), test.getName());
viewModel.insert(test2);
binding.addToFavorite.setImageResource(R.drawable.fav);
}
}
});
}
If the test object exists in the Favorite database, I have to delete it. After deleting, it calls this again (since it observed a chane) and inserts it again.
It keeps looping infinitely. Is there a better way to implement this or stop this?
It seems like some business logic has entered your view (Activity) class.
Since LiveData & Room are meant to be used when receiving updates about Database changes is needed, and your use of the DB is not requiring constant updates, I would suggest going with a more direct approach.
First, Remove the use of LiveData from your Database. Use simple return values.
Your view (Activity/Fragment) can then tell the view model that a button was clicked.
#Override
public void onFavoriteIconClicked() {
viewModel.onFavoriteClicked()
}
The view will observe the view model in order to receive the correct icon to show.
Something like:
viewModel.favoriteIcon.observe(getViewLifecycleOwner(), new Observer<Integer>() {
#Override
public void onChanged(Integer iconResId) {
binding.addToFavorite.setImageResource(iconResId)
}
}
Now the viewModel can handle the logic (or better add a Repository layer - See Here)
Upon click, Check if entry exist in DB.
If exists: remove it from DB and set favoriteIcon value:
favoriteIcon.setValue(R.drawable.non_fav)
If doesn't exist: Add it to DB and set favoriteIcon value.
favoriteIcon.setValue(R.drawable.fav)
For a good tutorial about using Room & LiveData - as well as doing so using the View/ViewModel/Repository pattern, check this link
UPDATE:
I see the same error ("Inconsistency detected. Invalid view holder adapter position") in another situation - this time when bulk adding.
The situation is I am implementing a nested recyclerview, each of which uses a RealmRecyclerViewAdapter and each has an OrderedRealmCollection as its basis. The result I'm going after is this:
I have implemented this at the first level by a query for distinct items in my realm keyed off of year and month:
OrderedRealmCollection<Media> monthMedias = InTouchDataMgr.get().getDistinctMedias(null,new String[]{Media.MEDIA_SELECT_YEAR,Media.MEDIA_SELECT_MONTH});
This gives me one entry for July, one for August, of 2019 in this example.
Then for each ViewHolder in that list, during the bind phase I make another query to determine how many Media items are in each month for that year:
void bindItem(Media media) {
this.media = media;
// Get all the images associated with the year in that date, set adapter in recyclerview
OrderedRealmCollection<Media> medias = InTouchDataMgr.get().getAllMediasForYearAndMonth(null, media.getYear(), media.getMonth());
// This adapter loads the CardView's recyclerView with a StaggeredGridLayoutManager
int minSize = Math.min(MAX_THUMBNAILS_PER_MONTH_CARDVIEW, medias.size());
imageRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(minSize >= 3 ? 3 : Math.max(minSize, 1), LinearLayoutManager.VERTICAL));
imageRecyclerView.setAdapter(new RealmCardViewMediaAdapter(medias, MAX_THUMBNAILS_PER_MONTH_CARDVIEW));
}
At this point I have the single month which is bound to the first ViewHolder, and now I have the count of media for that month, and I want to cause this ViewHolder to display a sampling of those items (maximum of MAX_THUMBNAILS_PER_MONTH_CARDVIEW which is initialized as 5) with the full count shown in the header.
So I pass the full OrderedRealmCollection of that media to the "second level" adapter that handles the list for this CardView.
That adapter looks like this:
private class RealmCardViewMediaAdapter extends RealmRecyclerViewAdapter<Media, CardViewMediaHolder> {
int forcedCount = NO_FORCED_COUNT;
RealmCardViewMediaAdapter(OrderedRealmCollection<Media> data, int forcedCount) {
super(data, true);
this.forcedCount = forcedCount;
}
#NonNull
#Override
public CardViewMediaHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(InTouch.getInstance().getApplicationContext());
View view = layoutInflater.inflate(R.layout.timeline_recycler_row_content, parent, false);
return new CardViewMediaHolder(view);
}
#Override
public void onBindViewHolder(#NonNull CardViewMediaHolder holder, int position) {
// Let Glide load the thumbnail
GlideApp.with(InTouch.getInstance().getApplicationContext())
.load(Objects.requireNonNull(getData()).get(position).getUriPathToMedia())
.thumbnail(0.05f)
.placeholder(InTouchUtils.getProgressDrawable())
.error(R.drawable.ic_image_error)
.into(holder.mMediaImageView);
}
#Override
public int getItemCount() {
//TODO - the below attempts to keep the item count at forced count when so specified, but this is causing
// "Inconsistency detected. Invalid view holder adapter position" exceptions when adding a bulk number of images
return (forcedCount == NO_FORCED_COUNT ? getData().size() : Math.min(forcedCount,getData().size()));
//return getData().size();
}
}
So what this is attempting to do is limit the number of items reported by the adapter to the smaller set of thumbnails to show in the first level CardView to a max of 5, spread around using that StaggeredGridLayout.
All this works perfectly until I do a bulk add from another thread. The use case is the user has selected the FAB to add images, and they have selected a bunch (my test was ~250). Then the Uri for all of this is passed to a thread, which does a callback into the method below:
public void handleMediaCreateRequest(ArrayList<Uri> mediaUris, String listId) {
if ( handlingAutoAddRequest) {
// This will only be done a single time when in autoAdd mode, so clear it here
// then add to it below
autoAddedIDs.clear();
}
// This method called from a thread, so different realm needed.
Realm threadedRealm = InTouchDataMgr.get().getRealm();
try {
// For each mediaPath, create a new Media and add it to the Realm
int x = 0;
for ( Uri uri: mediaUris) {
try {
Media media = new Media();
InTouchUtils.populateMediaFromUri(this, media, uri);
InTouchDataMgr.get().addMedia(media, STATUS_UNKNOWN, threadedRealm);
autoAddedIDs.add(media.getId());
if ( x > 2) {
// Let user see what is going on
runOnUiThread(this::updateUI);
x = 0;
}
x++;
} catch (Exception e) {
Timber.e("Error creating new media in a batch, uri was %s, error was %s", uri.getPath(),e.getMessage());
}
}
} finally {
InTouchDataMgr.get().closeRealmSafely(threadedRealm);
runOnUiThread(this::updateUI);
}
}
This method is operating against the realm, which is then making its normal callback into the OrderedCollection which is the base of the list in the recyclerview(s).
The addMedia() method is standard Realm activity, and works fine everywhere else.
updateUI() essentially causes an adapter.notifyDataSetChanged() call, in this case to the RealmCardViewMediaAdapter.
If I either don't use a separate thread, or I don't attempt to limit the number of items the adapter returns to a max of 5 items, then this all works perfectly.
If I leave the limit of 5 in as the return value from getItemCount() and don't refresh the UI until all has been added, then this also works, even from a different thread.
So it seems there is something about notifyDataSetChanged() being called as the Realm based list of managed objects is being updated in real time that is generating this error. But I don't know why or how to fix?
UPDATE END
I am using Realm Java DB 6.0.2 and realm:android-adapters:3.1.0
I created a class that extends RealmRecyclerViewAdapter class for my RecyclerView:
class ItemViewAdapter extends RealmRecyclerViewAdapter<RealmObject, BindableViewHolder> implements Filterable {
ItemViewAdapter(OrderedRealmCollection data) {
super(data, true);
}
I am initializing this adapter using the standard pattern of passing an OrderedRealmCollection to the adapter:
ItemViewAdapter createItemAdapter() {
return new ItemViewAdapter(realm.where(Contact.class).sort("displayName"));
}
"realm" has been previously initialized in the class creating the adapter.
I allow the user to identify one or more rows in this recyclerView that they want to delete, then I execute an AsyncTask which calls the method handling the delete:
public static class DoHandleMultiDeleteFromAlertTask extends AsyncTask {
private final WeakReference<ListActivity> listActivity;
private final ActionMode mode;
DoHandleMultiDeleteFromAlertTask(ListActivity listActivity, ActionMode mode) {
this.listActivity = new WeakReference<>(listActivity);
this.mode = mode;
}
#Override
protected void onPreExecute() {
listActivity.get().mProgressBar.setVisibility(View.VISIBLE);
}
#Override
protected void onPostExecute(Object o) {
// Cause multi-select to end and selected map to clear
mode.finish();
listActivity.get().mProgressBar.setVisibility(View.GONE);
listActivity.get().updateUI(); // Calls a notifyDataSetChanged() call on the adapter
}
#Override
protected Object doInBackground(Object[] objects) {
// Cause deletion to happen.
listActivity.get().handleMultiItemDeleteFromAlert();
return null;
}
}
Inside handleMultiItemDeleteFromAlert(), since we are being called from a different thread I create and close a Realm instance to do the delete work:
void handleMultiItemDeleteFromAlert() {
Realm handleDeleteRealm = InTouchDataMgr.get().getRealm();
try {
String contactId;
ArrayList<String> contactIds = new ArrayList<>();
for (String key : mSelectedPositions.keySet()) {
// The key finds the Contact ID to delete
contactId = mSelectedPositions.getString(key);
if (contactId != null) {
contactIds.add(contactId);
}
}
// Since we are running this from the non-UI thread, I pass a runnable that will
// Update the UI every 3rd delete to give the use some sense of activity happening.
InTouchDataMgr.get().deleteContact(contactIds, handleDeleteRealm, () -> runOnUiThread(ContactListActivity.this::updateUI));
} finally {
InTouchDataMgr.get().closeRealmSafely(handleDeleteRealm);
}
}
And the deleteContact() method looks like this:
public void deleteContact(ArrayList<String> contactIds, Realm realm, Runnable UIRefreshRunnable) {
boolean success = false;
try {
realm.beginTransaction();
int x = 0;
for ( String contactId : contactIds ) {
Contact c = getContact(contactId, realm);
if (c == null) {
continue;
}
// Delete from the realm
c.deleteFromRealm();
if ( UIRefreshRunnable != null && x > 2 ) {
try {
UIRefreshRunnable.run();
} catch (Exception e) {
//No-op
}
x = 0;
}
x++;
}
success = true;
} catch (Exception e) {
Timber.d("Exception deleting contact from realm: %s", e.getMessage());
} finally {
if (success) {
realm.commitTransaction();
} else {
realm.cancelTransaction();
}
}
Now my problem - when I was doing this work entirely from the UI thread I had no errors. But now when the transaction is committed I am getting:
Inconsistency detected. Invalid item position 1(offset:-1).state:5 androidx.recyclerview.widget.RecyclerView{f6a65cc VFED..... .F....ID 0,0-1440,2240 #7f090158 app:id/list_recycler_view}, adapter:com.reddragon.intouch.ui.ListActivity$ItemViewAdapter#5d77178,
<a bunch of other lines here>
I thought that RealmRecyclerViewAdapter already registered listeners, kept everything straight, etc. What more do I need to do?
The reason I am using a separate thread here is that if a user identifies a few dozen (or perhaps hundreds) items in the list to delete, it can take several seconds to perform the delete (depending on which list we are talking about - there are various checks and other updates that have to happen to preferences, etc.), and I didn't want the UI locked during this process.
How is the adapter getting "inconsistent"?
I solved this by adjusting the architecture slightly. It seems there is some issue with StaggeredGridLayoutManager when mixed with:
The dynamic ability of RealmRecyclerView to update itself automatically during bulk adds or deletes.
An adapter that attempts to limit what is shown by returning a count from getItemCount() that is not equal to the current list count.
I suspect this has to do with how ViewHolder instances get created and positioned by the layout manager, since this is where the error is pointing to.
So what I did instead is rather than have the adapter return a value that can be less than the actual count of the list being managed at any point in time, I changed the realm query to use the .limit() capability. Even when the query returns less than the limit initially, it has the nice side effect of capping itself at the limit requested as the list dynamically grows from the bulk add. And it has the benefit of allowing getItemCount() to return whatever the current size of that list is (which always works).
To recap - when in "Month View" (where I want the user to only see a max of 5 images like the screen shot above), the first step is to populate the adapter of the top level RealmRecyclerView with the result of a DISTINCT type query which results in an OrderedRealmCollection of Media objects which correspond to each month from each year in my media Library.
Then in the "bind" flow of that adapter, the MonthViewHolder performs the second realm query, this time with a limit() clause:
OrderedRealmCollection<Media> medias = InTouchDataMgr.get().getMediasForYearAndMonthWithLimit(null,
media.getYear(),
media.getMonth(),
MAX_THUMBNAILS_PER_MONTH_CARDVIEW); // Limit the results to our max per month
Then the adapter for the RealmRecyclerView associated with this specific month uses the results of this query as its list to manage.
Now it can return getData().size() as the result of the getItemCount() call whether I am in the Month view (capped by limit() ) or in my week view which returns all media items for that week and year.
The default behavior for a ListView when calling smoothScrollToPosition on it, it to move with linear speed to the specified position.
Digging into ListView's and AbsListView's code, I can see that this behavior takes place because AbsListView uses a PositionScroller object (implementing AbsPositionScroller) that in turn uses a FlingRunnable object on which the method startScroll gets called with linear = true (which ends up having its OverScroller object use a LinearInterpolator).
I want to modify this behavior, and have it use for example the Scroller.ViscousFluidInterpolator class that the OverScroller class would use by default, but I'm not finding a way to do it.
I see that AbsListView defines a AbsPosScroller interface (that himself implements with a PositionScroller class), that I could try to implement with my own class to have it end up using the ViscousFluidInterpolator, but for some reason this interface is private to the package android.widget...
Am I missing something, or does it look like this has been written in a way that prevents it to have a behavior like that one be customized? Why would they bother writing up a AbsPosScroller interface in first place?
Any leads on how could I get the behavior I want without having to write my entire ListView class from scratch?
While I still don't know why would they write these components in a way that their behavior can't be customized easily when it would've been pretty easy to do it, I came up with an alternative implementation of smoothScrollToPosition (awesomeScrollToPosition in the code below) that does what I needed.
This solution makes use of an OverScroller object (that internally uses the ViscousInterpolator unless a different one is specified) to provide the effect I was looking for, for scrolling to elements within the visible page (the solution to achieve scrolling across pages is more convoluted, but this works for the problem I needed to solve).
I basically implemented a Runnable class private to my own ListView subclass (MyListView) that deals with the scrolling animation, re-posting itself to the UI thread for as long as the animation needs to run, using scrollingListBy in every frame (this method is only available since KitKat [19] though).
public class MyListView extends ListView {
private MyScroller mScroller;
/* MyListView constructors here */
public void awesomeScrollToPosition(int position, int duration) {
if (getChildCount() == 0) {
// Can't scroll without children (visible list items)
return;
}
if (mScroller == null) {
mScroller = new MyScroller();
}
if (mScroller.isRunning()) {
mScroller.stop();
}
int firstPos = getFirstVisiblePosition();
int lastPos = getLastVisiblePosition();
if (!(firstPos <= position && position <= lastPos)) {
// Can't scroll to an item outside of the visible range this easily
return;
}
int targetPosition = position - firstPos;
int targetTop = getChildAt(targetPosition).getTop();
mScroller.start(targetTop, duration);
}
private class MyScroller implements Runnable {
OverScroller mScroller;
boolean mRunning;
int mLastY;
MyScroller() {
mScroller = new OverScroller(getContext());
mRunning = false;
}
void start(int y, int duration) {
// start scrolling
mLastY = 0;
mScroller.startScroll(0, 0, 0, y, duration);
mRunning = true;
postOnAnimation(this);
}
boolean isRunning() {
return mRunning;
}
#Override
public void run() {
boolean more = mScroller.computeScrollOffset();
final int currentY = mScroller.getCurrY();
// actual scrolling
scrollListBy(currentY - mLastY);
if (more) {
mLastY = currentY;
// schedule next run
postOnAnimation(this);
} else {
stop();
}
}
public void stop() {
mRunning = false;
removeCallbacks(this);
}
}
}
Does anyone recognize this pattern and know of a tidy solution?
I've got a view that can be in certain states. Let's call them Neutral, Success, Error, InProgress. In the view I've got multiple elements (Buttons, TextViews and a ProgressBar) that should either be visible/enabled depending on the state the view is in.
Currently I've got methods that represent the states that do the necessary .setEnabled() and .setVisibility() calls. With 4 states and a couple of elements this becomes messy quite fast.
I also feel that the State Pattern is not necessarily a good solution but is something that personally springs to mind.
I would love to hear what any of you think is a simple and tidy solution.
Sample code:
void setIsRegistering() {
isRegistering = true;
isRegistered = false;
progressBar.setVisibility(View.VISIBLE);
successText.setVisibility(View.GONE);
errorText.setVisibility(View.GONE);
setupFooterButton.setEnabled(false);
setupFooterButton.setText("Adding browser");
}
void setIsRegistered() {
isRegistering = false;
isRegistered = true;
progressBar.setVisibility(View.INVISIBLE);
successText.setVisibility(View.VISIBLE);
errorText.setVisibility(View.GONE);
setupFooterButton.setEnabled(true);
setupFooterButton.setText("Next");
}
void setIsNotRegistered() {
isRegistering = false;
isRegistered = false;
progressBar.setVisibility(View.INVISIBLE);
successText.setVisibility(View.INVISIBLE);
errorText.setVisibility(View.GONE);
setupFooterButton.setEnabled(true);
setupFooterButton.setText("Add browser");
}`
You can use a ViewAnimator for this: (http://developer.android.com/reference/android/widget/ViewAnimator.html)
You can then call viewAnimator.setDisplayedChild() to set the selected item.
setDisplayedChild() takes an integer, so I typically create an enum to hold the states I want:
enum ViewStates {
NEUTRAL, SUCCESS, ERROR
}
setDisplayedChild(ViewStates.Neutral.ordinal());
Or if that's too verbose:
enum ViewStates {
NEUTRAL, SUCCESS, ERROR
public static int neutral = NEUTRAL.ordinal();
public static int success = SUCCESS.ordinal();
public static int error = ERROR.ordinal();
}
setDisplayedChild(neutral);