I'm trying to write some tests with the new android-test-kit (Espresso). But I can't find any information on how to check if a view is displayed and perform some actions on it (like clicking buttons, e.t.c.). Note that the view I need to check if it exists or not. If it does perform action on the view and if not proceed to the next view.
Any help would be appreciated. I just need a link, or some example code for the basics:
Check if the view exists
If yes,Perform action
If not, proceed to next screen
You can use a classic option "try/catch":
try {
onView(withText("Text")).check(matches(isDisplayed()));
//perform some actions on this view
} catch (NoMatchingViewException notExist) {
//proceed to the next screen
}
Object currentActivity;
#Nullable
private Activity getCurrentActivity() throws Throwable {
getInstrumentation().waitForIdleSync();
getInstrumentation().runOnMainSync(new Runnable() {
public void run() {
Collection resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(RESUMED);
if (resumedActivities.iterator().hasNext()) {
currentActivity = resumedActivities.iterator().next();
}
}
});
return (Activity) currentActivity;
}
with this, you can get the Activity that is currently displayed. After that by doing something like this, you can make a safe code section
HOMESCREEN:
{
for (; ; ) {
if (getCurrentActivity() != null) {
//check if it is the required screen
if (getCurrentActivity().getLocalClassName().toLowerCase().contains("homescreen")) {
//if it is the required screen, break the
//loop and continue execution
break HOMESCREEN;
} else {
//wait for 2 seconds and run the loop again
sleep(2000);
}
} else {
break HOMESCREEN;
}
}
}
sleep(2000) is a custom function which just calls thread.sleep like shown below:
private void sleep(long milliseconds) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException("Cannot execute Thread.sleep()");
}
}
You must control the behavior of your test. So, you must have to add some preconditions, or create the #Rule that will control the behavior, for example by adding parameters depending on which your view will be displayed or not.
In order to detect finish of app bar collapsing I called addOnOffsetChangedListener. In listener's onOffsetChanged I catch and handle the moment of collapsing done. Then I need to stop listen for offset changes.
In most examples here is call of removeOnOffsetChangedListener(this) from inside of onOffsetChanged. But looking in AppBarLayout.java I see:
private void dispatchOffsetUpdates(int offset) {
// Iterate backwards through the list so that most recently added listeners
// get the first chance to decide
if (mListeners != null) {
for (int i = 0, z = mListeners.size(); i < z; i++) {
final OnOffsetChangedListener listener = mListeners.get(i);
if (listener != null) {
listener.onOffsetChanged(this, offset);
}
}
}
}
So if there is more than one listener installed, calling removeOnOffsetChangedListener(this) naturally results in IndexOutOfBoundsException.
Have I missed something? Is there safe way to 'unsubscribe' from listening for offset updates?
Interestingly, this wouldn't be a problem if their code actually did what the comment says. Anyway, we can defer the removal by putting the call to removeOnOffsetChangedListener() in a Runnable, and posting it to the AppBarLayout in onOffsetChanged(), instead of calling it directly there.
For example:
AppBarLayout.OnOffsetChangedListener listener = new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(final AppBarLayout abl, int offset) {
abl.post(new Runnable() {
#Override
public void run() {
abl.removeOnOffsetChangedListener(listener);
}
}
);
}
};
Im trying to do this silly "game" where I have a relative layout and classes that extend from View. I create the Views and want to add them onto the Relative layout using a Thread. So far so good. I had it working just fine with little object views scrolling down the relative layout. I then added two more screens for options and only after those screens I wanted to show the Activity with the scrolling views. The problem is that it stop working with the following error:
08-03 20:13:48.140: E/AndroidRuntime(5152): java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
I canĀ“t understand this error...
Here is the sample code for the thread that starts and the function that updates the UI:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);
context = this.getApplicationContext();
/*load all bitmaps*/
loadBitmaps();
/*get all the activities components*/
gameCanvasRL = (RelativeLayout)findViewById(R.id.gameCanvasRL);
playerScoreTV = (TextView)findViewById(R.id.playerPoints);
/*GameManager instance*/
manager = GameManager.getInstance();
/*Start Threads*/
startAllThreads();
}
private void startAllThreads() {
refreshCanvasHandler = new Handler();
spawnHandler = new Handler();
refreshCanvasThread = new Thread(new RefreshCanvasTask());
spawnTask = new Thread(new spawnTask());
refreshCanvasThread.start();
spawnTask.start();
}
class RefreshCanvasTask implements Runnable {
#Override
public void run() {
while(true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
refreshCanvasHandler.post(new Runnable() {
#Override
public void run() {
refreshCanvas();
}
});
}
}
}
private void refreshCanvas() {
int germSize;
/*Move all germs*/
manager.moveGerms();
germSize = manager.getGerms().size();
if(gameCanvasRL.getChildCount() > 0)
gameCanvasRL.removeAllViews();
for(int i = 0; i < germSize; i++) {
gameCanvasRL.addView(manager.getGerms().get(i));
}
}
The error means you're trying to add a view that is already added to another view. A view may only have 1 parent, so the second add throws an exception. You need to remove it from the original parent before adding it to the new one.
I want current X & Y co ordinates of an ImageView on onCreate of Activity, is there any solution for the same ? Please share your idea on same.
Actually, when you call setContentView() the Android nutshell starts the views drawing on surface, Which you can observe on using viewTrewwObserve So you can not get the height and width of ImageView in onCreate() as its not currently draw yet.
You can use, (Not tried)
imageView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// Get ImageView height width here <------------
}
});
It will Register a callback to be invoked when the global layout state or the visibility of views within the view tree changes.
Update:
Or you can use onWindowFocusChanged (boolean hasFocus).
This is the best indicator of whether this activity is visible to the user. The default implementation clears the key tracking state, so should always be called.
(1)You can get it using the getViewTreeObserver() or (2)you can add an asynchronous Task on your onCreate()
#Override
protected void onCreate(Bundle savedInstanceState)
{
//SIMPLY CALL YOUR ASYN TASK WITH DELAY OVER HERE LIKE THIS.
new MyImageDrawClass().execute();
}
class MyImageDrawClass extends AsyncTask<Void, Void, Void>
{
#Override
protected Void doInBackground(Void... params)
{
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
#Override
protected void onPostExecute(Void result)
{
LayoutParams lp = mContent.getLayoutParams();
value_wdth = mContent.getWidth();
value_hght = mContent.getHeight();
//PERFORM YOUR OTHER DRAWING STUFF..
super.onPostExecute(result);
}
}
let me know if you find issue with this, you can also reduce the time delay to Thread.sleep(10);
I generally use this method to get height and to get width.
I would like a ScrollView to start all the way at the bottom. Any methods?
you should run the code inside the scroll.post like this:
scroll.post(new Runnable() {
#Override
public void run() {
scroll.fullScroll(View.FOCUS_DOWN);
}
});
scroll.fullScroll(View.FOCUS_DOWN) also should work.
Put this in a scroll.Post(Runnable run)
Kotlin Code
scrollView.post {
scrollView.fullScroll(View.FOCUS_DOWN)
}
scroll.fullScroll(View.FOCUS_DOWN) will lead to the change of focus. That will bring some strange behavior when there are more than one focusable views, e.g two EditText. There is another way for this question.
View lastChild = scrollLayout.getChildAt(scrollLayout.getChildCount() - 1);
int bottom = lastChild.getBottom() + scrollLayout.getPaddingBottom();
int sy = scrollLayout.getScrollY();
int sh = scrollLayout.getHeight();
int delta = bottom - (sy + sh);
scrollLayout.smoothScrollBy(0, delta);
This works well.
Kotlin Extension
fun ScrollView.scrollToBottom() {
val lastChild = getChildAt(childCount - 1)
val bottom = lastChild.bottom + paddingBottom
val delta = bottom - (scrollY+ height)
smoothScrollBy(0, delta)
}
Sometimes scrollView.post doesn't work
scrollView.post(new Runnable() {
#Override
public void run() {
scrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
});
BUT if you use scrollView.postDelayed, it will definitely work
scrollView.postDelayed(new Runnable() {
#Override
public void run() {
scrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
},1000);
What worked best for me is
scroll_view.post(new Runnable() {
#Override
public void run() {
// This method works but animates the scrolling
// which looks weird on first load
// scroll_view.fullScroll(View.FOCUS_DOWN);
// This method works even better because there are no animations.
scroll_view.scrollTo(0, scroll_view.getBottom());
}
});
I increment to work perfectly.
private void sendScroll(){
final Handler handler = new Handler();
new Thread(new Runnable() {
#Override
public void run() {
try {Thread.sleep(100);} catch (InterruptedException e) {}
handler.post(new Runnable() {
#Override
public void run() {
scrollView.fullScroll(View.FOCUS_DOWN);
}
});
}
}).start();
}
Note
This answer is a workaround for really old versions of android. Today the postDelayed has no more that bug and you should use it.
i tried that successful.
scrollView.postDelayed(new Runnable() {
#Override
public void run() {
scrollView.smoothScrollTo(0, scrollView.getHeight());
}
}, 1000);
Here is some other ways to scroll to bottom
fun ScrollView.scrollToBottom() {
// use this for scroll immediately
scrollTo(0, this.getChildAt(0).height)
// or this for smooth scroll
//smoothScrollBy(0, this.getChildAt(0).height)
// or this for **very** smooth scroll
//ObjectAnimator.ofInt(this, "scrollY", this.getChildAt(0).height).setDuration(2000).start()
}
Using
If you scrollview already laid out
my_scroll_view.scrollToBottom()
If your scrollview is not finish laid out (eg: you scroll to bottom in Activity onCreate method ...)
my_scroll_view.post {
my_scroll_view.scrollToBottom()
}
When the view is not loaded yet, you cannot scroll. You can do it 'later' with a post or sleep call as above, but this is not very elegant.
It is better to plan the scroll and do it on the next onLayout(). Example code here:
https://stackoverflow.com/a/10209457/1310343
One thing to consider is what NOT to set. Make certain your child controls, especially EditText controls, do not have the RequestFocus property set. This may be one of the last interpreted properties on the layout and it will override gravity settings on its parents (the layout or ScrollView).
Not exactly the answer to the question, but I needed to scroll down as soon as an EditText got the focus. However the accepted answer would make the ET also lose focus right away (to the ScrollView I assume).
My workaround was the following:
emailEt.setOnFocusChangeListener(new View.OnFocusChangeListener() {
#Override
public void onFocusChange(View v, boolean hasFocus) {
if(hasFocus){
Toast.makeText(getActivity(), "got the focus", Toast.LENGTH_LONG).show();
scrollView.postDelayed(new Runnable() {
#Override
public void run() {
scrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
}, 200);
}else {
Toast.makeText(getActivity(), "lost the focus", Toast.LENGTH_LONG).show();
}
}
});
I actually found that calling fullScroll twice does the trick:
myScrollView.fullScroll(View.FOCUS_DOWN);
myScrollView.post(new Runnable() {
#Override
public void run() {
myScrollView.fullScroll(View.FOCUS_DOWN);
}
});
It may have something to do with the activation of the post() method right after performing the first (unsuccessful) scroll. I think this behavior occurs after any previous method call on myScrollView, so you can try replacing the first fullScroll() method by anything else that may be relevant to you.
Using there is another cool way to do this with Kotlin coroutines. The advantage of using a coroutine opposed to a Handler with a runnable (post/postDelayed) is that it does not fire up an expensive thread to execute a delayed action.
launch(UI){
delay(300)
scrollView.fullScroll(View.FOCUS_DOWN)
}
It is important to specify the coroutine's HandlerContext as UI otherwise the delayed action might not be called from the UI thread.
In those case were using just scroll.scrollTo(0, sc.getBottom()) don't work, use it using scroll.post
Example:
scroll.post(new Runnable() {
#Override
public void run() {
scroll.fullScroll(View.FOCUS_DOWN);
}
});
One possible reason of why scroll.fullScroll(View.FOCUS_DOWN) might not work even wrapped in .post() is that the view is not laid out. In this case View.doOnLayout() could be a better option:
scroll.doOnLayout(){
scroll.fullScroll(View.FOCUS_DOWN)
}
Or, something more elaborated for the brave souls: https://chris.banes.dev/2019/12/03/suspending-views/
A combination of all answers did the trick for me:
Extension Function PostDelayed
private fun ScrollView.postDelayed(
time: Long = 325, // ms
block: ScrollView.() -> Unit
) {
postDelayed({block()}, time)
}
Extension Function measureScrollHeight
fun ScrollView.measureScrollHeight(): Int {
val lastChild = getChildAt(childCount - 1)
val bottom = lastChild.bottom + paddingBottom
val delta = bottom - (scrollY+ height)
return delta
}
Extension Function ScrolltoBottom
fun ScrollView.scrollToBottom() {
postDelayed {
smoothScrollBy(0, measureScrollHeight())
}
}
Be aware that the minimum delay should be at least 325ms or the scrolling will be buggy (not scrolling to the entire bottom). The larger your delta between the current height and the bottom is, the larger should be the delayed time.
Some people here said that scrollView.post didn't work.
If you don't want to use scrollView.postDelayed, another option is to use a listener. Here is what I did in another use case :
ViewTreeObserver.OnPreDrawListener viewVisibilityChanged = new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
if (my_view.getVisibility() == View.VISIBLE) {
scroll_view.smoothScrollTo(0, scroll_view.getHeight());
}
return true;
}
};
You can add it to your view this way :
my_view.getViewTreeObserver().addOnPreDrawListener(viewVisibilityChanged);
If your minimum SDK is 29 or upper you could use this:
View childView = findViewById(R.id.your_view_id_in_the_scroll_view)
if(childView != null){
scrollview.post(() -> scrollview.scrollToDescendant(childView));
}
This works instantly. Without delay.
// wait for the scroll view to be laid out
scrollView.post(new Runnable() {
public void run() {
// then wait for the child of the scroll view (normally a LinearLayout) to be laid out
scrollView.getChildAt(0).post(new Runnable() {
public void run() {
// finally scroll without animation
scrollView.scrollTo(0, scrollView.getBottom());
}
}
}
}