android jetpack compose: ACTION_UP only reached for tap but not after ACTION_MOVE when using pointerInteropFilter - pointers

I need to scroll programatically (in order to snap items to adjust symmetrically top and bottom) after scrolling or after a tap (click) in a lazy column. I also need to start at a specific item when app launches - starts.
I am using pointerInteropFilter to be able to run some code at these actions: down, move, up. The code runs ok when I tap but it does not trigger ACTION_UP after a move is done.
[this is the desired result as soon as I release the finger from the screen... that is... a "jump" or scroll to a value that in this cases is item 10 and some offset][1]
The code is working only for tap... but the coroutine is not working when Action_up
I read that we are adviced to "prefer pointerInput] and use this only for interoperation with
existing code that consumes [MotionEvent]s"
It also say that pointerinteropFilter try to make a seamless treatment between view and motion events... but I dont know if it is relevant.
Thanks.
Mauricio
#Composable
fun Greeting(name: String) {
val listState2 = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
state = listState2,
) {
items (50) {index ->
Text(
modifier = Modifier
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
Log.i(ContentValues.TAG, "down pressed")
false
}
MotionEvent.ACTION_MOVE -> {
Log.i(ContentValues.TAG, "moved done")
false
}
MotionEvent.ACTION_UP -> {
coroutineScope.launch {
listState2.scrollToItem(10, 28)
}
Log.i(ContentValues.TAG, "up detected")
}
MotionEvent.ACTION_CANCEL -> {
coroutineScope.launch {
listState2.scrollToItem(10, 28)
}
Log.i(ContentValues.TAG, "canceled chosen")
false
}
else ->
false
}
true
},
text = "item $index",
)
}
}
}```
[1]: https://i.stack.imgur.com/vSiCG.png

I've faced the same issues.
The onClick= function doesn't work for all descendants, that are placed in the same composable ConstraintLayout, if the layout uses .pointerInteropFilter and returns true as a result of catching ACTION_UP MotionEvent.
It's related to the fact that clickable logic under the hood expects PressInteraction.Press which value is a simple mapping from ACTION_UP. Thus ConstraintLayout by catching ACTION_UP MoutionEvent prevents passing of PressInteraction.Press to descendants.
If the ConstraintLayout will return false on ACTION_UP MoutionEvent, then the next MotionEvents won't be dispatched to it next time. Because of implementation under the hood.
So the root cause of the issue is misusing of the Modifier.pointerInteropFilter. The idea behind the function is filtering events, what it actually does.
In my case looks like it's required to rewrite Touch logic to Modifier.pointerInput.
Maybe this findings will help you as well

Related

Kotlin Execution order and result issues

I'm trying to get all files from firebase's storage through listAll.
By the way..
storageReference.listAll().addOnSuccessListener { listResult ->
val image_task : FileDownloadTask
for (fileRef in listResult.items) {
fileRef.downloadUrl.addOnSuccessListener { Uri ->
image_list.add(Uri.toString())
println("size1 : " + image_list.size)
}
}
println("size2 : " + image_list.size)
}//addOnSuccessListener
enter image description here
Why is the execution order like this?
How do I solve it??
When you add a listener or callback to something, the code inside the listener will not be called until sometime later. Everything else in the current function will happen first.
You are adding listeners for each item using your for loop. No code in the listeners is running yet. Then your "size2" println call is made after the for loop. At some later time, all your listeners will fire.
If you want asynchronous code like this to be written sequentially, then you need to use coroutines. That's a huge topic, but your code would look something like this (but probably a little more involved than this if you want to properly handle errors). I'm using lifecycleScope from an Android Activity or Fragment for this example. If you're not on Android, you need to use some other CoroutineScope.
The calls to await() are an alternative to adding success and failure listeners. await() suspends the coroutine and then returns a result or throws an exception on failure.
lifecycleScope.launch {
val results = try {
storageReference.listAll().await()
} catch (e: Exception) {
println("Failed to get list: ${e.message}")
return#launch
}
val uris = try {
results.map { it.downloadUrl.await().toString() }
} catch (e: Exception) {
println("Failed to get at least one URI: ${e.message}")
return#launch
}
image_list.addAll(uris)
}
There is nothing wrong with the execution order here.
fileRef.downloadUrl.addOnSuccessListener { Uri ->
the downloadUrl is an asynchronous action which means it doesn't wait for the action to actually complete in order to move along with the code.
You receive the result with the success listener (at least in this case)
If you want to deal with it in a sequential way, look at coroutines.

Firebase And Kotlin Coroutines

I have this static (companion object) function to download event information from Firebase Firestore and event images from Firebase Storage:
fun downloadEventInformationAndImages() {
FirebaseFirestore.getInstance().collection("events").document(downloadedEventID)
.get().addOnSuccessListener { snap ->
//Download Event Information Here
//Do stuff
GlobalScope.launch(Dispatchers.Main) {
//Download Event Images Here
val downloadEventImage = FirebaseStorage.getInstance().reference.child("Images/Events/$eventID/eventPhoto.jpeg")
.getBytes(1024 * 1024).asDeferred()
val downloadEventFounderImage = FirebaseStorage.getInstance().reference.child("Images/Users/$founderID/profilePhoto.jpeg")
.getBytes(1024 * 1024).asDeferred()
try {
val downloadedImages = mutableListOf<ByteArray>(
downloadEventImage.await(),
downloadEventFounderImage.await())
// Update stuff on UI
} catch(e: StorageException) {
// Error handling
}
}.addOnFailureListener { exception ->
// Error handling
}
}
}
What I want to do is avoid using GlobalScope but when I tried to add runBlocking to downloadEventInformationAndImages():
fun downloadEventInformationAndImages() = runBlocking {
// Do stuff
launch(Dispatchers.Main) {
it didn't work (It didn't wait Firebase to finish downloading - then I moved runBlocking to inside of function, also didn't work). How can I avoid using GlobalScope? Thanks in advance.
The best option I see here is to pass a CoroutineScope as a parameter of downloadEventInformationAndImages. So that would be
fun downloadEventInformationAndImages(scope: CoroutineScope) {
FirebaseFirestore.getInstance().collection("events").document(downloadedEventID)
.get().addOnSuccessListener { snap ->
//Download Event Information Here
//Do stuff
scope.launch(Dispatchers.Main) { ... }
}
}
The one thing you have to be careful is that every coroutine you launch here is now launched in the scope you pass in, which means that if it fails or is cancelled, it'll also cancel any parent coroutines. To understand how to deal with this, you should check the documentation for Jobs. On the other hand, you can also build your CoroutineScope with a SupervisorJob (mentioned in the documentation link above), where child coroutines fail without affecting the parents. Finally, it's also good practice to cleanup your CoroutineScope when the object that owns it reaches the end of its lifecycle. This will avoid possible memory leaks. The cleanup can be done either with scope.cancel() or scope.coroutineContext.cancelChildren(). The first one terminates the scope's job (which gets propagated to all child jobs), and the second one just cancels any child jobs that may exist. I suggest you spend some of your time just reading articles or even documentation about coroutines, because there are a lot of nuances :)
You can write your function as CoroutineScope extension function:
fun CoroutineScope.downloadEventInformationAndImages() {
...
launch(Dispatchers.Main) {
...
}
and call it from ViewModel or some other place with scope:
uiScope.downloadEventInformationAndImages()

WatchOS5 - how to refresh my complication instantly?

I have an apple watch complication and the iPhone app running side by side. I have a button within the app to transmit application context dictionary to the watch. I expect to see the complication title to be refreshed.
I cannot seem to force the "tap button -> see update on the complication" kind of behavior.
What is the appropriate method to force a complication update? How can I refresh my apple watch complication instantly?
I do see the title changes, but I think it requires me to tap on the complication to open it's apple watch app first. How can I get the complication to update itself on the Watch home screen?
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: #escaping (CLKComplicationTimelineEntry?) -> Void) {
if complication.family == .graphicRectangular {
let template = CLKComplicationTemplateGraphicRectangularLargeImage()
//...configure
return template
}
}
I see this apple provided code that refreshes the complication. I'm not sure if it is too much, or if calling extendTimeline alone is sufficient if I'm generating the complication using the entry above.
func refreshComplication() {
#if os(watchOS)
let server = CLKComplicationServer.sharedInstance()
if let complications = server.activeComplications {
for complication in complications {
// Call this method sparingly. If your existing complication data is still valid,
// consider calling the extendTimeline(for:) method instead.
server.reloadTimeline(for: complication)
}
}
#endif
}
You should be able to do this by calling the refreshComplication() function from your didReceiveApplicationContext block in the file which has your WCSessionDelegate.
So if you are receiving the title via an applicationContext message your code would look something along these lines.
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
if let updatedTitle = applicationContext["updatedTitle"] {
if let title = updateTitle as? String {
//Remeber that complicationServer.swift is a seperate process therefore you will need to store the received data somehow.
UserDefaults.standard.set(title, forKey: "complicationTitle")
refreshComplication()
}
}
}
I have a setting in my iOS App that lets the user change their target and using this method refreshed the complication with the new target almost instantly. However, I believe once your complication has used up its cpu budget nothing will happen, but hopefully that is not happening for you. See https://developer.apple.com/documentation/clockkit/clkcomplicationserver/1627891-reloadtimeline
Hope that helps, let me know how you get on.
Drew

Meteor on finish renderlist

Is there a way to get a callback for when an entire list renders?
I've tried
Template.articles.rendered = function() {
var lastChapter = Chapters.findOne({}, {
sort: {
createdTime: -1
}
})
if (lastChapter._id != this.data._id)
return
doSomething()
};
But this is unreliable because chapters are added 1 by 1 instead of all at once, so this actually fires multiple times.
Thanks.
rendered is called when a part of the template is re-rendered, so you should check inside your rendered method whether you want to do anything now. When does "the entire list renders" happen? You know that in your code, for instance by checking if the list is of an expected length yet.

Deactivating events with .off

Most of my .on events use closures:
ref.on( 'child_added',
function( snapshot ) {
userCallback( snapshot.val() );
} );
which means, it is not possible to deactivate these monitors with .off(), since off needs the original callback pointer as well as the eventType. How can I do this sort of thing?
My app (jQuery Mobile) is page-based. When the user hits a certain page, I want to activate monitors. When the user leaves that page, I want to de-activate, so when they re-enter, I don't end up with multiple monitors. I think I want to call .off() with only eventType and have all callbacks removed.
Right now the only option is to store a reference to the callback. We actually make this a little easier by having .on() return it back to you. So you can do:
var childCallback = ref.on('child_added', function(snapshot) { /* whatever */ });
// later...
ref.off('child_added', childCallback);
But we've received several pieces of feedback like yours, saying it is sometimes cumbersome to keep track of your callback references. So we have a planned API change so that you can call .off() with only an event type or with no arguments at all, and we'll just remove whatever callbacks are registered. But we're focused on other features right now, so this change is probably 1+ months out.
Thanks for the feedback!
In the interim, you can reproduce that sort of behavior with a simple manager pattern. For instance:
function ObserverManager( firebaseRef, page ) {
this.firebaseRef = firebaseRef;
this.listeners = {child_added: [], child_removed: [], value: [], child_updated: [], child_changed: []};
this.page = page;
}
FirebaseObservable.prototype.on(event, callback) {
this.listeners[event].push(
this.firebaseRef.on(event, function(snapshot) {
callback(snapshot.val());
})
);
}
FirebaseObservable.prototype.off(event) {
var list = this.listeners[event], i = list.length;
while(i--) { // 50% more efficient than for(i..; list.length; ...) in IE due to scoping
firebaseRef.off(event, list[i]);
}
}

Resources