Related
I was wondering if there is a good way to turn on/off numerous switches at once using Accessibility service.
So basically I have a view that contains a RecyclerView that has about 40 switches in it and I need a way to switch them on/off as fast as possible. Here is what I tried.
public static void scrollView(final AccessibilityNodeInfo nodeInfo) {
if (nodeInfo == null) return;
if (nodeInfo.isScrollable()) {
if (nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) {
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
turnOffSwitches(nodeInfo);
}
}, 1000);
}
}
}
public static void turnOffSwitches(final AccessibilityNodeInfo parentView) {
if (parentView.getClassName().equals("android.support.v7.widget.RecyclerView")) {
for (int i = 0; i < parentView.getChildCount(); i++) {
final AccessibilityNodeInfo child = parentView.getChild(i);
final Boolean isLasteOne = (i == parentView.getChildCount() - 1);
child.performAction(AccessibilityNodeInfo.ACTION_CLICK);
if (isLasteOne) {
scrollView(parentView);
}
}
}
}
I even tried introducing a delay between ACTION_CLICK actions that helped a bit but did not solve my problem fully. So the issue that I am having is that not all switches get turned off. It seems like there is a limit on how fast this can be done. So my question is, is there a better way to do this?
ACTION_SCROLL_FORWARD - it's not paging just scrolling some portion of your whole matrix of elements and some visible are already switched to the state so in your next loop you are trying to click on those which were already swiched (next visible area overlaps with previous one)
either check the state on/off - if the view is not already at the desired state before changing it
or check event.getScrollX() and event.getScrollY() if action was not already performed in those bounds
Full code here
Video of correct behavior in java and incorrect in react native here
I have modified a linear layout to respond to touch by resizing the left child while the right child takes up the rest of the space, simulating a horizontal scroll that can be 'opened' or 'closed' using the following code
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) leftView.getLayoutParams();
//Log.d("ll","width " + newWidth);
lp.width=newWidth;
leftView.setLayoutParams(lp);
In react native, touch is still calling this method and logging the expected values, but the size of the children is not updated. The only time it updates is when I switch the visibility to gone then visible again. Calling invalidate/requestLayout on the view from java or forceUpdate from js does not have any effect.
Is there some other code I need to call to invalidate and redraw the view? Is there a hint I need to give to react that this component scrolls or responds to touch?
I had the same problem and I found the answer on this old reported issue on react native's repo.
Add the following code in your custom layout and after that there is no need to even call requestLayout() or invalidate(). The changes are propagated as soon as you update your layout's LayoutParams. This solution is the same as the ones used in ReactPicker.java and ReactToolbar.java.
#Override
public void requestLayout() {
super.requestLayout();
// The spinner relies on a measure + layout pass happening after it calls requestLayout().
// Without this, the widget never actually changes the selection and doesn't call the
// appropriate listeners. Since we override onLayout in our ViewGroups, a layout pass never
// happens after a call to requestLayout, so we simulate one here.
post(measureAndLayout);
}
private final Runnable measureAndLayout = new Runnable() {
#Override
public void run() {
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
RN android is indeed not updating children layout or visibility or adapter changes under most conditions. By inserting hooks into the custom view when an update is needed that will invalidate/requestLayout then call this code normal behavior is restored, mostly. There are still some cases where measurement does not happen like it does normally and I have to postDelayed Runnables that then cause this invalidation. Dealing with a node's parents may not be strictly necessary in all cases, but it is for some.
In View Manager
Method markNewLayout, getShadowNode;
public ViewManager(){
super();
if (markNewLayout == null) {
try {
markNewLayout = CSSNode.class.getDeclaredMethod("markHasNewLayout");
markNewLayout.setAccessible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
try{
if (getShadowNode==null){
getShadowNode = UIImplementation.class.getDeclaredMethod("resolveShadowNode",int.class);
getShadowNode.setAccessible(true);
}
} catch (Exception e) {
e.printStackTrace();
}
public class MyShadowNode extends LayoutShadowNode {
#Override
public void markUpdated(){
super.markUpdated();
if (hasNewLayout()) markLayoutSeen();
dirty();
}
#Override
public boolean isDirty(){
return true;
}
#Override
protected CustomView createViewInstance(final ThemedReactContext reactContext) {
view.setRnUpdateListener(new CustomView.RNUpdateListener() {
MyShadowNode node;
#Override
public void needsUpdate() {
view.requestLayout();
Runnable r = new Runnable() {
#Override
public void run() {
if (node ==null){
try {
node = (MyShadowNode) getShadowNode.invoke(uiImplementation, view.getId());
}
catch (Exception e){
e.printStackTrace();
}
}
if (node != null) {
if (node.hasNewLayout()) node.markLayoutSeen();
ReactShadowNode parent = node.getParent();
while (parent != null) {
if (parent.hasNewLayout()) {
try {
markNewLayout.invoke(parent,view.getId());
} catch (Exception e) {
e.printStackTrace();
}
parent.markLayoutSeen();
}
parent = parent.getParent();
}
node.markUpdated();
}
Log.d(getName(), "markUpdated");
}
};
reactContext.runOnNativeModulesQueueThread(r);
}
});
}
Try removing content of the layout one by one. May be one among those children has wrap content. You can use hierarchyViewer and uiautomatorviewer for it
Also think about weights also. If possible post the hierarchyViewer data.
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) leftView.getLayoutParams();
lp.width=newWidth;
leftView.setLayoutParams(lp);
leftView.requestLayout();
Above code will solve you problem.
I have a RecyclerView that is inside a CardView. The CardView has a height of 500dp, but I want to shorten this height if the RecyclerView is smaller.
So I wonder if there is any listener that is called when the RecyclerView has finished laying down its items for the first time, making it possible to set the RecyclerView's height to the CardView's height (if smaller than 500dp).
I also needed to execute code after my recycler view finished inflating all elements. I tried checking in onBindViewHolder in my Adapter, if the position was the last, and then notified the observer. But at that point, the recycler view still was not fully populated.
As RecyclerView implements ViewGroup, this anwser was very helpful. You simply need to add an OnGlobalLayoutListener to the recyclerView:
View recyclerView = findViewById(R.id.myView);
recyclerView
.getViewTreeObserver()
.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// At this point the layout is complete and the
// dimensions of recyclerView and any child views
// are known.
recyclerView
.getViewTreeObserver()
.removeOnGlobalLayoutListener(this);
}
});
Working modification of #andrino anwser.
As #Juancho pointed in comment above. This method is called several times. In this case we want it to be triggered only once.
Create custom listener with instance e.g
private RecyclerViewReadyCallback recyclerViewReadyCallback;
public interface RecyclerViewReadyCallback {
void onLayoutReady();
}
Then set OnGlobalLayoutListener on your RecyclerView
recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (recyclerViewReadyCallback != null) {
recyclerViewReadyCallback.onLayoutReady();
}
recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
after that you only need implement custom listener with your code
recyclerViewReadyCallback = new RecyclerViewReadyCallback() {
#Override
public void onLayoutReady() {
//
//here comes your code that will be executed after all items are laid down
//
}
};
If you use Kotlin, then there is a more compact solution.
Sample from here.
This layout listener is usually used to do something after a View is measured, so you typically would need to wait until width and height are greater than 0.
... it can be used by any object that extends View and also be able to access to all its specific functions and properties from the listener.
// define 'afterMeasured' layout listener:
inline fun <T: View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
// using 'afterMeasured' handler:
myRecycler.afterMeasured {
// do the scroll (you can use the RecyclerView functions and properties directly)
// ...
}
The best way that I found to know when has finished laying down the items was using the LinearLayoutManager.
For example:
private RecyclerView recyclerView;
...
recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false){
#Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
// TODO
}
);
...
I improved the answer of android developer to fix this problem. It's a Kotlin code but should be simple to understand even if you know only Java.
I wrote a subclass of LinearLayoutManager which lets you listen to the onLayoutCompleted() event:
/**
* This class calls [mCallback] (instance of [OnLayoutCompleteCallback]) when all layout
* calculations are complete, e.g. following a call to
* [RecyclerView.Adapter.notifyDataSetChanged()] (or related methods).
*
* In a paginated listing, we will decide if load more needs to be called in the said callback.
*/
class NotifyingLinearLayoutManager(context: Context) : LinearLayoutManager(context, VERTICAL, false) {
var mCallback: OnLayoutCompleteCallback? = null
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
mCallback?.onLayoutComplete()
}
fun isLastItemCompletelyVisible() = findLastCompletelyVisibleItemPosition() == itemCount - 1
interface OnLayoutCompleteCallback {
fun onLayoutComplete()
}
}
Now I set the mCallback like below:
mLayoutManager.mCallback = object : NotifyingLinearLayoutManager.OnLayoutCompleteCallback {
override fun onLayoutComplete() {
// here we know that the view has been updated.
// now you can execute your code here
}
}
Note: what is different from the linked answer is that I use onLayoutComplete() which is only invoked once, as the docs say:
void onLayoutCompleted (RecyclerView.State state)
Called after a full layout calculation is finished. The layout
calculation may include multiple onLayoutChildren(Recycler, State)
calls due to animations or layout measurement but it will include only
one onLayoutCompleted(State) call. This method will be called at the
end of layout(int, int, int, int) call.
This is a good place for the LayoutManager to do some cleanup like
pending scroll position, saved state etc.
I tried this and it worked for me. Here is the Kotlin extension
fun RecyclerView.runWhenReady(action: () -> Unit) {
val globalLayoutListener = object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
action()
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
}
then call it
myRecyclerView.runWhenReady {
// Your action
}
Also in same cases you can use RecyclerView.post() method to run your code after list/grid items are popped up. In my cases it was pretty enough.
I have been struggling with trying to remove OnGlobalLayoutListener once it gets triggered but that throws an IllegalStateException. Since what I need is to scroll my recyclerView to the second element what I did was to check if it already have children and if it is the first time this is true, only then I do the scroll:
public class MyActivity extends BaseActivity implements BalanceView {
...
private boolean firstTime = true;
...
#Override
protected void onCreate(Bundle savedInstanceState) {
...
ViewTreeObserver vto = myRecyclerView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (myRecyclerView.getChildCount() > 0 && MyActivity.this.firstTime){
MyActivity.this.firstTime = false;
scrollToSecondPosition();
}
}
});
}
...
private void scrollToSecondPosition() {
// do the scroll
}
}
HTH someone!
(Of course, this was inspired on #andrino and #Phatee answers)
Here is an alternative way:
You can load your recycler view in a thread. Like this
First, create a TimerTask
void threadAliveChecker(final Thread thread){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run() {
if(!thread.isAlive()){
runOnUiThread(new Runnable() {
#Override
public void run() {
// stop your progressbar here
}
});
}
}
},500,500);
}
Second, create a runnable
Runnable myRunnable = new Runnable() {
#Override
public void run() {
runOnUiThread(new Runnable() {
#Override
public void run() {
// load recycler view from here
// you can also toast here
}
});
}
};
Third, create a thread
Thread myThread = new Thread(myRunnable);
threadAliveChecker();
// start showing progress bar according to your need (call a method)
myThread.start();
Understanding the above code now:
TimerTask - It will run and will check the thread
(every 500 milliseconds) is running or completed.
Runnable - runnable is just like a method, here you have written the code that is needed to be done in that thread. So our recycler view will be called from this runnable.
Thread - Runnable will be called using this thread. So we have started this thread and when the recyclerView load (runnable code load) then this thread will be completed (will not live in programming words).
So our timer is checking the thread is alive or not and when the thread.isAlive is false then we will remove the progress Bar.
If you are using the android-ktx library and if you need to perform an action after positioning all elements of the Activity, you can use this method:
// define 'afterMeasured' Activity listener:
fun Activity.afterMeasured(f: () -> Unit) {
window.decorView.findViewById<View>(android.R.id.content).doOnNextLayout {
f()
}
}
// in Activity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(...)
afterMeasured {
// do something here
}
}
This is how I did it
recyclerView.setLayoutManager(new LinearLayoutManager(this){
#Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
//code to run after loading recyclerview
new GuideView.Builder(MainActivity.this)
.setTargetView(targetView)
.setGravity(Gravity.auto)
.setDismissType(DismissType.outside)
.setContentTextSize(18)
.build()
.show();
}
});
I wish this will help you.
You can use with this approach
if ((adapterPosition + 1) == mHistoryResponse.size) {
Log.d("debug", "process done")
}
get the adapterPosition with plus 1 and check it with your data classes size, if it has same size, the process is practically complete.
For those that are not using Kotlin and are still struggling, I took a fast look at the doOnNextLayout(crossinline action: (view: T) -> Unit) solution they implemented, and it is pretty simple.
IF you are NOT working with a custom RecyclerView (CustomRecyclerView extends RecyclerView), you may want to rethink it as this will bring a lot of benefits you may want to add in the future (smooth scroll to position, vertical dividers, etc..)
Inside the CustomRecyclerView.class
public void doOnNextLayout(Consumer<List<View>> onChange) {
addOnLayoutChangeListener(
new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
onChange.accept(getChildren());
removeOnLayoutChangeListener(this);
}
}
);
}
The getChildren() method is building a List of size getChildCount(); and a add(getChild(i)) on each iteration.
Now...
One important aspect about the code is this: removeOnLayoutChangeListener(this);
This means that the devs are asking for you to execute this before each list submission to the adapter.
In theory we could only place the listener ONCE upon RecyclerView creation (which IMO would be cheaper/better) + because we are retrieving the views, we could retrieve their respective binds with DataBindingUtils. and get whatever data the adapter gave the view onBind via their DataBind.
To do this tho it requires more code.
First the adapter needs to be aware of the Fragment they inhabit, OR the RecyclerView::setAdapter needs to provide a ViewLifeCyclerOwner, a third easier option is to provide the adapter with a onViewDestroy() method, and execute it on Fragment's onDestroyView() method.
#Override
public void onDestroyView() {
super.onDestroyView();
adater.onViewDestroyed();
}
by overriding the onAttachedToRecyclerView, we are able to attach them as observers.
private final List<Runnable> submitter = new ArrayList<>();
#Override
public void onAttachedToRecyclerView(#NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
if (recyclerView instanceof CustomRecyclerView) {
submitter.add(((CustomRecyclerView) recyclerView)::onSubmit);
}
}
Where the onSubmit method on the CustomRecyclerView side will provide a boolean that will tell the recyclerView whether a list is being submitted.
private boolean submitting;
public void doOnNextLayout(Consumer<List<View>> onChange) {
addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (submitting) {
onChange.accept(getChildren());
submitting = false;
}
}
);
}
public void onSubmit() {
submitting = true;
}
Each Runnable will be executed at the moment of list submission:
In the case of the ListAdapter there are 2 possible entry points:
private void notifyRVs() {
for (Runnable r:submitter
) {
r.run();
}
}
#Override
public void submitList(#Nullable List<X> list, #Nullable Runnable commitCallback) {
notifyRVs();
super.submitList(list, commitCallback);
}
#Override
public void submitList(#Nullable List<X> list) {
notifyRVs();
super.submitList(list);
}
Now to prevent memory leaks we must clear the List of Runnables on ViewDestroyed()
inside the Adapter...
public void onViewDestroyed() {
submitter.clear();
}
Now because the functionality of the method changed we should rename it, and decouple the Consumer<List> from the LayoutChangeListener()
private Consumer<List<View>> onChange = views -> {};
public void setOnListSubmitted(Consumer<List<View>> onChange) {
this.onChange = onChange;
}
public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
//Read attributes
setOnListSubmissionListener();
}
private void setOnListSubmissionListener() {
addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (submitting) {
onChange.accept(getChildren());
submitting = false;
}
}
);
}
What worked for me was to add the listener after setting the RecyclerView adapter.
ServerRequest serverRequest = new ServerRequest(this);
serverRequest.fetchAnswersInBackground(question_id, new AnswersDataCallBack()
{
#Override
public void done(ArrayList<AnswerObject> returnedListOfAnswers)
{
mAdapter = new ForumAnswerRecyclerViewAdapter(returnedListOfAnswers, ForumAnswerActivity.this);
recyclerView.setAdapter(mAdapter);
recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
progressDialog.dismiss();
}
});
}
});
This dismisses the "progressDialog" after the global layout state or the visibility of views within the view tree changes.
// Another way
// Get the values
Maybe<List<itemClass>> getItemClass(){
return /* */
}
// Create a listener
void getAll(DisposableMaybeObserver<List<itemClass>> dmo) {
getItemClass().subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(dmo);
}
// In the code where you want to track the end of loading in recyclerView:
DisposableMaybeObserver<List<itemClass>> mSubscriber = new DisposableMaybeObserver<List<itemClass>>() {
#Override
public void onSuccess(List<itemClass> item_list) {
adapter.setWords(item_list);
adapter.notifyDataSetChanged();
Log.d("RECYCLER", "DONE");
}
#Override
public void onError(Throwable e) {
Log.d("RECYCLER", "ERROR " + e.getMessage());
}
#Override
public void onComplete() {
Log.d("RECYCLER", "COMPLETE");
}
};
void getAll(mSubscriber);
//and
#Override
public void onDestroy() {
super.onDestroy();
mSubscriber.dispose();
Log.d("RECYCLER","onDestroy");
}
recyclerView.getChildAt(recyclerView.getChildCount() - 1).postDelayed(new Runnable() {
#Override
public void run() {
//do something
}
}, 300);
RecyclerView only lays down specific number of items at a time, we can get the number by calling getChildCount(). Next, we need to get the last item by calling getChildAt (int index). The index is getChildCount() - 1.
I'm inspired by this person answer and I can't find his post again. He said it's important to use postDelayed() instead of regular post() if you want to do something to the last item. I think it's to avoid NullPointerException. 300 is delayed time in ms. You can change it to 50 like that person did.
I'm almost sold to RxJava, which is a perfect companion to Retrofit, but I'm struggling into a common pattern while migrating my code: to save bandwidth, I'd like to lazily fetch (paginated) objects from my webservice as needed, while my listview (or recyclerview) is scrolling using reactive programming.
My previous code was doing the job perfectly, but reactive programming seems worth the try.
Listening to listview/recyclerview scrolling (and other boring stuffs) isn't the concern and getting an Observable is easy using Retrofit:
#GET("/api/messages")
Observable<List<Message>> getMessages(#Path("offset") int offset, #Path("limit") int limit);
I just can't figure out the pattern to use in reactive programming.
The Concat operator seems a good starting point, along with ConnectableObservable at some point to defer emission and maybe flatMap, but how ?
EDIT:
Here's my current (naive) solution:
public interface Paged<T> {
boolean isLoading();
void cancel();
void next(int count);
void next(int count, Scheduler scheduler);
Observable<List<T>> asObservable();
boolean hasCompleted();
int position();
}
And my implementation using a subject:
public abstract class SimplePaged<T> implements Paged<T> {
final PublishSubject<List<T>> subject = PublishSubject.create();
private volatile boolean loading;
private volatile int offset;
private Subscription subscription;
#Override
public boolean isLoading() {
return loading;
}
#Override
public synchronized void cancel() {
if(subscription != null && !subscription.isUnsubscribed())
subscription.unsubscribe();
if(!hasCompleted())
subject.onCompleted();
subscription = null;
loading = false;
}
#Override
public void next(int count) {
next(count, null);
}
#Override
public synchronized void next(int count, Scheduler scheduler) {
if (isLoading())
throw new IllegalStateException("you can't call next() before onNext()");
if(hasCompleted())
throw new IllegalStateException("you can't call next() after onCompleted()");
loading = true;
Observable<List<T>> obs = onNextPage(offset, count).single();
if(scheduler != null)
obs = obs.subscribeOn(scheduler); // BEWARE! onNext/onError/onComplete will happens on that scheduler!
subscription = obs.subscribe(this::onNext, this::onError, this::onComplete);
}
#Override
public Observable<List<T>> asObservable() {
return subject.asObservable();
}
#Override
public boolean hasCompleted() {
return subject.hasCompleted();
}
#Override
public int position() {
return offset;
}
/* Warning: functions below may be called from another thread */
protected synchronized void onNext(List<T> items) {
if (items != null)
offset += items.size();
loading = false;
if (items == null || items.size() == 0)
subject.onCompleted();
else
subject.onNext(items);
}
protected synchronized void onError(Throwable t) {
loading = false;
subject.onError(t);
}
protected synchronized void onComplete() {
loading = false;
}
abstract protected Observable<List<T>> onNextPage(int offset, int count);
}
Here's one out of a few potential ways to handle reactive paging. Let's assume we have a method getNextPageTrigger which returns an Observable emits some event object when the scroll listener (or whatever input) wants a new page to be loaded. In real life it would probably have the debounce operator, but in addition to that we'll make sure we only trigger it after the latest page has loaded.
We also define a method to unwrap the messages from their list:
Observable<Message> getPage(final int page) {
return service.getMessages(page * PAGE_SIZE, PAGE_SIZE)
.flatMap(messageList -> Observable.from(messageList));
}
Then we can make the actual fetching logic:
// Start with the first page.
getPage(0)
// Add on each incremental future page.
.concatWith(Observable.range(1, Integer.MAX_VALUE)
// Uses a little trick to get the next page to wait for a signal to load.
// By ignoring all actual elements emitted and casting, the trigger must
// complete before the actual page request will be made.
.concatMap(page -> getNextPageTrigger().limit(1)
.ignoreElements()
.cast(Message.class)
.concatWith(getPage(page))) // Then subscribe, etc..
This is still missing a couple potentially important things:
1 - This obviously doesn't know when to stop fetching additional pages, which means once it hits the end, depending on what the server returns, it could either keep hitting errors or empty results every time scroll is triggered. Approaches to solving this depend on how you signal to the client that there are no more pages to load.
2 - If you need error retries, I would suggest looking into the retryWhen operator. Otherwise, common network errors could cause an error in a page load to propagate.
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());
}
}
}
}