Currently developping on a Pepper robot (Android dev), I'm trying to use some "basic" animations (from QiSQK lib).
For instance, when calling a WS, I'm starting a "think" animation using animation/animate. Then, when the WS call ends, I try to use another animation ("showing the tablet").
I saw that Pepper can't animate twice if the previous animation isn't finished/cancelled.
So, I used requestCancellation(), but it didn't stop the animation.
I also used cancel(mayInterruptIfRunning), didn't stop either.
So, I can't chain 2 animations without waiting the previous animation to stop (my WS call = 3-4s max).
Any idea ?
Example :
private var animate: Future<Animate>? = null
fun animate(animRes: Int) {
animate?.requestCancellation()
AnimationBuilder
.with(qiContext)
.withResources(animRes)
.buildAsync()
.thenConsume { futureAnimation ->
animate = AnimateBuilder
.with(qiContext)
.withAnimation(futureAnimation?.value)
.buildAsync()
animate?.andThenConsume {
it.async().run()
}
}
}
Thx,
Bastien.
Finally found out my issue.
Actually, I was storing the animate reference when creating the object like this :
animate = AnimateBuilder
.with(qiContext)
.withAnimation(futureAnimation?.value)
.buildAsync()
So, I printed my objects (like every Future's used in my callbacks), then I found that after using it.async().run() from my animate andThenConsume callback, which returns the running animation reference, is different from the one I created before (was thinking reusing same old ref).
So, instead, here my new (working) code :
fun animate(animRes: Int) {
//Cancelling possible running animation to display a new one
animate?.requestCancellation()
AnimationBuilder
.with(qiContext)
.withResources(animRes)
.buildAsync()
.thenConsume { futureAnimation ->
AnimateBuilder
.with(qiContext)
.withAnimation(futureAnimation?.value)
.buildAsync()
.andThenConsume {
animate = it.async().run() as Future<Animate>
}
}
}
Related
I want to add an animation to the texts, which will work automatically when the screen is opened
In Android View it can be done easily by just putting this in OnCreate
val animation = AnimationUtils.loadAnimation(this,R.anim.example)
text.startAnimation(animation)
How can the same idea work in Compose ?
What I know in Compose is that the state must be changed to run a particular element animation
You can use a side effect
DisposableEffect(Unit) {
//do something
onDispose { }
}
or:
LaunchedEffect(Unit) {
//update the value to start the animation
}
For tests I use Espresso and Barista
I have a test in which I need to open another screen by pressing a button. How can I check if this screen opens? Did the screen I need open?
Can I somehow check the chain of screens? To understand that the screens open in the order I need?
If someone throws links to good tutorials on UI tests in Android, I will be very grateful.
An easy solution would be to just check for an element of the new screen to be shown like this:
onView(withId(R.id.id_of_element_in_your_new_screen)).check(matches(isDisplayed()))
If you really want to check out for the current activity that is shown, you could try something like this:
Gather the current activity via InstrumentationRegistry and check for the activity in stage RESUMED.
fun getTopActivity(): Activity? {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
if (resumedActivities.iterator().hasNext()) {
resumedActivities.iterator().next()?.let {
activity = it
}
}
}
return activity
}
You could then check this in a test like this:
#Test
fun checkForActivity() {
val currentActivity = getTopActivity()
assertTrue(currentActivity?.javaClass == YourActivityToCheckAgainst::class.java)
}
I personally use intended(hasComponent(YourActivityToCheckAgainst::class.java.name)), which checks if the last intent was done with a desired activity, set as its component.
I also wrote an extensive Android UI testing tutorial using Espresso + Barista libraries.
I need to show a play/stop toggle instead of a play/pause button when we are casting live content to the chromecast, but we still need the play/pause when casting other videos.
I set the minicontroller casting buttons to a custom button where in the onResume I called my function that bound different drawables depending on what is being casted. The problem is that when I change the video to live (or other way around) on a page with a already showing minicontroller it doens`t call onResume again (rightfully so), and it keeps the same buttons. I don't know if there is another event inside minicontroller that I can use. I try to use SessionManagerListener and a UiController to bound my functions to certain events but both of them there were problems (I probably did something wrong, dont know).
My function is:
fun checkButton() {
activity?.let {
val mCastContext = CastContext.getSharedInstance(activity!!.baseContext)
val mCastSession = mCastContext.sessionManager.currentCastSession
if (mCastSession?.remoteMediaClient?.currentItem != null) {
val drawablePlay = ContextCompat.getDrawable(it, R.drawable.cast_ic_mini_controller_play)
val drawableStop = ContextCompat.getDrawable(it, R.drawable.cast_ic_mini_controller_stop)
uiMediaController.bindImageViewToPlayPauseToggle(button, drawablePlay!!, drawableStop!!, drawableStop, ProgressBar(it), false)
}
}
}
I expect to call my function every time the miniController loads or something like that.
Thanks!
Found it... looks like uiMediaController.bindImageViewToPlayPauseToggle already do this automatically, it already has the logic to use the stop button when is live streaming...
I have a problem I have not yet been able to understand, nor solve.
On my android project, I have a BaseActivity. There, among other functions, I decided to add a function to show or hide a loading view when necessary. It works as intended, but sometimes an error happens.
I will try to raise some important info about my project I think can be useful. My app is integrated with an external login app. I call it when the services I call need to refresh it's token. When the user logs in on the app, it calls a listener and give me back control on mine.
The problem is the next one:
I come to an activity that needs to call a service and I have the token all right, but then I lock the phone. After a long period of time, I unlock the phone and, from the same activity, I call again the service. My activity shows de loader as intended and, as my token is expired, I call the login app from it's SDK.
When I come back to my app, and I call the service I wanted successfully, the app tries to hide the loader. This is where the fail comes, as I can't change the visibility to GONE. I looked for it on the view hierarchy and find it, but with visibility = VISIBLE.
Here is the piece of code from the loader, hope someone can find where I'm making the mistake!
abstract class BaseActivity : DaggerAppCompatActivity(){
// These are the IDS of the Views I'm adding to the activity, so I can track them and change their visibility
var imgLoadingID = -1
var rvLoadingID = -1
fun showLoading() {
// If the views are added I show them
if (imgLoadingID > 0 && rvLoadingID > 0) {
val imageView = findViewById<ImageView>(imgLoadingID)
val relativeLayout = findViewById<RelativeLayout>(rvLoadingID)
relativeLayout.visibility = View.VISIBLE
imageView.visibility = VISIBLE
imageView.isClickable = false
imageView.isFocusable = false
} else {
// else I create them and show them
val imgLoading = ImageView(this)
imgLoading.id = View.generateViewId()
imgLoadingID = imgLoading.id
val maxpx = CustomUtils.ViewUtils.converIntToDps(65, this)
Glide.with(this).asGif().load(R.mipmap.loading).into(imgLoading)
val relativeLayout = RelativeLayout(this)
relativeLayout.id = View.generateViewId()
rvLoadingID = relativeLayout.id
var params = RelativeLayout.LayoutParams(maxpx, WRAP_CONTENT)
params.addRule(RelativeLayout.CENTER_IN_PARENT)
relativeLayout.addView(imgLoading, params)
relativeLayout.background = getDrawable(R.color.pure_white_97)
relativeLayout.isClickable = true
relativeLayout.isFocusable = true
findViewById<ViewGroup>(android.R.id.content).addView(relativeLayout, RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
imgLoading.visibility = VISIBLE
}
// I lock the back button so people don't cancel my requests
esBackPressedBlocked = true
}
// Here I find the views and change their visibility.
fun hideLoading(){
if(imgLoadingID > 0 && rvLoadingID > 0) {
val imageView = findViewById<ImageView>(imgLoadingID)
val relativeLayout = findViewById<RelativeLayout>(rvLoadingID)
relativeLayout.visibility = View.GONE
imageView.visibility = View.GONE
}
esBackPressedBlocked = false
}
}
I deleted some logs I added to the whole function, but when it fails, it enters on the hideLoading() function, even on the relativeLayout.visibility = View.GONE part.
The function used to be with the Views as an whole object instead of their ids, but I found it more reliable this way, and saving the views instead of their Id's had the same problem.
My main concern is how Android manages my application while the phone is locked for this period of time (the fails happened after 8-10 hours of inactivity). I think something there can be creating this issue. I thought also about the external Login app, since it's sdk is launching their app's intent and calling me from a listener when coming back, but, since my code is being executed, I think Android it's managing my views on an strange way. Or maybe I try to hide the loading view before I'm on the resumed activity... I don't really know.
BTW, I know there are easier solutions on showing a loader, but I wanted to create it the cleanest way. If you have any cleaner approach I'm open to any solution.
If there is anything unclear let me know in the comments, and I hope my English was clear enough to express myself, it's a tricky problem that I can't understand, so it's difficult for me to explain it.
Thanks!!
I'm using AppGyver Steroids and Supersonic to build an app and I'm having some issues navigating between views programmatically.
Based on the docs, you navigate between views like this:
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.push(view_obj);
However, when I inspect things via the Chrome DevTools, it appears that a second duplicate view is created i.e. If I navigate away from the index page and then navigate back, I now have two index pages, instead of what [I think] should be one. It also doesn't close the previous view I was on.
How can I prevent this from happening and simply move to the existing view, instead of duplicating views? How do I close a view after I have navigated away from it?
Thanks.
The problem you're encountering is that you're creating a new supersonic.ui.View("main#index") every time you navigate. On top of this, I think you want to return to the same view when you navigate back to a view for the second time, i.e. you want the view to remain in memory even if it has been removed from the navigation stack with pop() (rather than pushing a new instance of that view). For this, you need to preload or "start()" the view, as described in the docs here.
I implemented my own helper function to make this easier; here is my code:
start = function(dest, isModal) {
var viewId=dest,
view=new supersonic.ui.View({
location: dest,
id: viewId
});
view.isStarted().then(function(started) {
if (started) {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
} else {
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
Use it like start('module#view');. As a bonus, you can pass true as the second argument and it gets pushed as a modal instead.
It checks if you've already started a view - if so, it just pushes that view back onto the stack. If not, it start()s (i.e. preloads) it, then pushes it. This ensures that the view stays in memory (with any user input that has been modified) even when you pop() it from the stack.
You have to imagine that the layer stack is actually a stack in the Computer Science sense. You can only add and remove views at the top of the stack. The consequence of this is that complex navigations such as A > B > C > D > B are difficult/hacky to do (in this case, you'd have to pop() D and C in succession to get back to B).
Views will close if you pop() them, as long as you didn't start() them. If you did, and you pop() them, they remain in memory. To kill that view, you have to call stop() on it, as described in the docs I linked above.
try
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.replace(view_obj);
And take a look at supersonic.ui.layers.pop();
Thanks to LeedsEbooks for helping me get my head around this challenge. I was able to find a solution. Here is the code:
var start = function(route_str, isModal) {
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view_id_str = match_obj[2],
view_location_str = route_str,
view = new supersonic.ui.View({
location: view_location_str,
id: view_id_str
});
view.isStarted().then(function(started) {
if (started)
{
if (isModal)
{
supersonic.ui.modal.show(view);
}
else {
supersonic.ui.layers.push(view);
}
}
else
{
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal)
{
supersonic.ui.modal.show(view);
}
else
{
supersonic.ui.layers.push(view);
}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
You must ensure that your route has the format module#view as defined in the documentation.
PLEASE NOTE
There seems to some problem with the supersonic ui method for starting views. If you run the following code:
supersonic.ui.views.start("myapp#first-view");
supersonic.ui.views.find("first-view").then( function(startedView) {
console.log(startedView);
});
You'll notice that your view id and location are identical. This seems to be wrong as the id should be first-view and location should be myapp#first-view.
So I decided to not use the AppGyver methods and create my own preload method instead, which I run from the controller attached to my home view (this ensures that all the views I want to preload are handled when the app loads). Here is the function to do this:
var preload = function(route_str)
{
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view = new supersonic.ui.View({
location: route_str,
id: match_obj[2]
});
view.start();
};
By doing this, I'm sure that the view will get loaded with the right location and id, and that when I use my start() function later, I won't have any problems.
You'll want to make sure that your structure.coffee file doesn't have any preload instructions so as not to create duplicate views that you'll have problems with later.
Finally, I have a view that is 2 levels in that is a form that posts data via AJAX operation. I wanted the view to go back to the previous view when the AJAX operation was complete. Using my earlier function resulted in the push() being rejected. It would be nice if AppGyver Supersonic could intelligently detect that pushing to a previous view should default to a layers.pop operation, but you don't always get what you want. Anyway, I managed to solve this using supersonic.ui.layers.pop(), which simply does what the Back button would have done.
Everything working as intended now.