I have a form within a ScrollView. When I tap into an EditText the soft keyboard appears and the ScrollView scrolls the now focused EditText so that it just comes into view.
However, I have hint information just below the EditText that I also would like to show, so the scrolling should go just a bit further up, like this:
The EditText is embedded in a form element and actually I'd like to scroll to the bottom of that. I've checked the source code of ScrollView and it will just scroll to the bottom of the currently focused view. Maybe there's a way to tell the ScrollView that the form element is the currently focused element?
Of course I could write my own ScrollView sub class and override the scroll behavior, but I wonder if there's a more elegant way of doing this.
Any other suggestions (with adjust scrolling with a fixed offset or so) are also appreciated.
I have not really found any way to configure the scrolling behavior of the ScrollView from the outside. So I ended up to define my own sub class of ScrollView:`
/**
* {#link ScrollView} extension that allows to configure scroll offset.
*/
public class ConfigurableScrollView extends ScrollView {
private int scrollOffset = 0;
public ConfigurableScrollView (final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public void setScrollOffset (final int scrollOffset) {
this.scrollOffset = scrollOffset;
}
#Override
protected int computeScrollDeltaToGetChildRectOnScreen (final Rect rect) {
// adjust by scroll offset
int scrollDelta = super.computeScrollDeltaToGetChildRectOnScreen(rect);
int newScrollDelta = (int) Math.signum(scrollDelta) * (scrollDelta + this.scrollOffset);
return newScrollDelta;
}
}
computeScrollDelta(...) is the only protected method that can be targeted for overriding, apart from onSizeChanged(...).
The signum function in the example above ensures that scrolling is only increased, if the ScrollView really thinks that scrolling is necessary (e.g. when keyboard pops up).
I can now set the extra scroll offset once from the outside, as calculated from the height of the hint.
It's not hard to use the extended ConfigurableScrollView instead of the standard ScrollView, I only had to replace the ScrollView XML tag with the FQN of the new class.
Considering that you are using ScrollView you have the possibility to use the method ScrollTo as follow:
scrollView.post(new Runnable() {
#Override
public void run() {
sv.scrollTo(x-value, y-value);
}
});
where the first argument is the scroll value for X, while the second argument is the scroll value for Y. So you just have to set your scrollView offset when the keyboard is displayed.
Hope it helps;)
Related
I have a childView inside ScrollView which expands onClick. I used the below code to bring the expanded view fully in the screen.
scrollView.requestChildFocus(childView, focussedView);
But the scroll is not smooth. What can I do to make the scroll smooth?
You could use the smooth scroll methods provided by ScrollView. Here is the documentation.
Try these methods:
public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled)
// Set whether arrow scrolling will animate its transition.
public final void smoothScrollTo(int x, int y)
// Like scrollTo(int, int), but scroll smoothly instead of immediately.
public final void smoothScrollBy(int dx, int dy)
// Like View.scrollBy(int, int), but scroll smoothly instead of immediately.
I have a LinearLayout containing both a ListView and an EditText. When the On-Screen keyboard is launched by touching the EditText, the ListView resizes so that only the top few elements remain visible.
The context that the ListView is being used in has the bottom few elements being more visually relevant than the top, and so I'd like for it to resize so that the bottom remains visible, rather than the top. Any pointers?
(Incidentally, the current fix I'm using involves using smoothScrollToPosition, but the laggy scroll behaviour makes this undesirable)
I just solved a similar issue this morning, and thought I'd post my result here for the benefit of future searchers. My issue was that scrolling to the bottom wasn't helping since I was calling it before the view actually changed size. The solution? Wait until it does change size by using a GlobalLayoutListener
Steps:
1) implement the following method in the activity holding the listview
public void scrollToBottom(){
//I think this is supposed to run on the UI thread
listView.setSelection(mAdapter.getCount() - 1);
}
2) create the following class
public class OnGlobalLayoutListenerWithScrollToBottom implements OnGlobalLayoutListener{
private boolean scroll;
private OnScrollToBottomListener listener;
public interface OnScrollToBottomListener{
public void scrollToBottom();
}
public OnGlobalLayoutListenerWithScrollToBottom(OnScrollToBottomListener listener){
this.listener = listener;
}
#Override
public void onGlobalLayout() {
if(scroll){
listener.scrollToBottom();
scroll = false;
}
}
/**
* Lets the listener know to request a scroll to bottom the next time it is layed out
*/
public void scrollToBottomAtNextOpportunity(){
scroll = true;
}
};
3) In your activity, implement the interface from this class. Then, in your activity, create an instance of this OnGlobalLayoutListener and set it as the listener for your listView
//make sure your class implements OnGlobalLayoutListenerWithScrollToBottom.OnScrollToBottomListener
listViewLayoutListener = new OnGlobalLayoutListenerWithScrollToBottom(this);
listView.getViewTreeObserver().addOnGlobalLayoutListener(listViewLayoutListener);
4) In your activity, before you make changes that will affect the size of list view, such as showing and hiding other views or adding stuff to the listview, simply let the layout listener know to scroll at the next opportunity
listViewLayoutListener.scrollToBottomAtNextOpportunity();
You might achieve this with setSelectionFromTop() and by overriding onSizeChanged() in a custom listview. In your layout, you should have a RelativeLayout has a parent container and place the listview above the edittext.
By creating your own listview and overriding onSizeChange(), you will be able to get the last visible item's position before the listview resizing and get its new height, in order to finally set the "previous" position of the list with an offset of its height.
How it works: Your list will place the previous last visible item at its top and you will add the pixels to scroll it at its bottom, just above your edittext.
To override the method and display it with an offset, do as follows:
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// get last visible item's position before resizing
int lastPosition = super.getLastVisiblePosition();
// call the super method to resize the listview
super.onSizeChanged(w, h, oldw, oldh);
// after resizing, show the last visible item at the bottom of new listview's height
super.setSelectionFromTop(lastPosition, (h - lastItemHeight));
}
lastItemHeight is a little workaround, because I didn't find how to get the last item's height before that onSizeChanged is called. Then, in the case of your listview contains many types of items (without the same height), I prefer to get the selected height when an event occurs, just before the SoftKeyboard opens up.
So in the custom listview, you have this global variable:
int lastItemHeight = 0;
And in the activity (or fragment, whatever), you update this value in OnClickListener:
edittext.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// set a new Thread
listview.post(new Runnable() {
#Override
public void run() {
// get last visible position of items
int lastPosition = listview.getLastVisiblePosition() - 1;
// if the view's last child can be retrieved
if ( listview.getChildAt(lastPosition) != null ) {
// update the height of the last child in custom listview
listview.lastItemHeight = listview.getChildAt(lastPosition).getHeight();
}
}
});
}
});
Note: there is another possible solution but you have to set android:stackFromBottom="true" on the listview, which stackes its content from the bottom. Instead, this solution here can display a specific item without forcing the content to start from the bottom, with the default listview's behaviour.
Second note: (just in case) don't forget to add android:windowSoftInputMode="adjustResize" in the manifest.
You should add this attribute in the manifest for your activity and pick the correct value for it (probably you will pick "adjustResize"):
<activity android:name="package.yourActivity" android:windowSoftInputMode="adjustResize"/>
I have a screen filled with buttons, but want the onTouch-method to use the entire screen's coordinates. I first tried using a RelativeLayout with an onTouchListener, but never managed to make it "connect" with the listener (i.e. nothing happened when screen was touched), I also tried putting an ImageView on top of the screen, and then making this view invisible.
This last method gave correct responses to onClicks, but I never managed to make it invisible.
If this is the best solution, which I highly doubt, how do I make the ImageView totally invisible, without losing its onTouchListener (I've experimented with white backgroundColor and setAlpha(0)).
Can I somehow make the onTouchListener react to the whole screen, using global coordinates, while the screen is showing (and altering) several buttons (preferably without the invisible imageview)?
If you don't understand what I'm asking for, feel free to complain about that. I'll try to fill the gaps as needed.
Edit:
I've now managed to resolve the issue by using the regular onTouch-method. I ran into several problems making both ACTION_DOWN and ACTION_MOVE activate the buttons, but I finally got it working. For other people reading this: onInterceptTouchEvent could possibly be used (but I never figured out how to get the screen coordinates instead of the view-coordinates).
Sorry if I'm wrong, but I believe I've just had a similar problem. I wanted a title screen that displayed a picture and on the picture words that say "Click to go on" or something similar. I messed around for a bit and found that you can make a layout clickable.
android:focusable="true"
android:id="#+id/titlescreenframe">
is in my xml file for my layout. The background image is simply in the background attribute (I realize you aren't using images)
Anyway, back in my activity
private FrameLayout fl;
...
fl = (FrameLayout)findViewById(R.id.titlescreenframe);
fl.setOnClickListener(this);
And then I use a switch statement to handle that and the buttons that are on the next layout. Here if you need it: Using Switch Statement to Handle Button Clicks
Seems this should work with other layouts as well, and I don't have literally any views on my main layout. (unless the layout itself counts as one?)
Ha! Just realized you said you found the solution. Silly timing. I'll post on the off-chance this helps someone, happy coding everyone. :)
Have you tried onInterceptTouchEvent on the layout?
I used dispatchTouchEvent for a similar problem. You could consider it a drop-in replacement for onTouchEvent, except that it always sends you an event, even if it's over an existing view.
Return true if you're handling the event, otherwise, be sure to call super.dispatchTouchEvent() and return its result.
As for getting screen coordinates - simply call the MotionEvent's getRawX() and getRawY() rather than getX() and getY(). Those are the absolute screen coordinates, including the action bar and all. If you want to cross-reference those with views, getLocationOnScreen is probably the easiest solution.
touch event return's to child views first. and if you define onClick or onTouch listener for them, parnt view (for example fragment) will not receive any touch listener. So if you want define swipe listener for fragment in this situation, you must implement it in a new class:
package com.neganet.QRelations.fragments;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class SwipeListenerFragment extends FrameLayout {
private float x1,x2;
static final int MIN_DISTANCE=150;
private onSwipeEventDetected mSwipeDetectedListener;
public SwipeListenerFragment(Context context) {
super(context);
}
public SwipeListenerFragment(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwipeListenerFragment(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result=false;
switch(ev.getAction())
{
case MotionEvent.ACTION_DOWN:
x1 = ev.getX();
break;
case MotionEvent.ACTION_UP:
x2 = ev.getX();
float deltaX = x2 - x1;
if (Math.abs(deltaX) > MIN_DISTANCE)
{
if(deltaX<0)
{
result=true;
if(mSwipeDetectedListener!=null)
mSwipeDetectedListener.swipeLeftDetected();
}else if(deltaX>0){
result=true;
if(mSwipeDetectedListener!=null)
mSwipeDetectedListener.swipeRightDetected();
}
}
break;
}
return result;
}
public interface onSwipeEventDetected
{
public void swipeLeftDetected();
public void swipeRightDetected();
}
public void registerToSwipeEvents(onSwipeEventDetected listener)
{
this.mSwipeDetectedListener=listener;
}
}
you can make implements for other types of Layouts completely like this. this class can detect both right and left swipe and specially it returns onInterceptTouchEvent true after detect. its important because if we don't do it some times child views maybe receive event and both of Swipe for fragment and onClick for child view (for example) runs and cause some issues.
after making this class, you must change your fragment xml file:
<com.neganet.QRelations.fragments.SwipeListenerFragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:id="#+id/main_list_layout"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:layout_height="match_parent" tools:context="com.neganet.QRelations.fragments.mainList"
android:background="#color/main_frag_back">
<!-- TODO: Update blank fragment layout -->
<android.support.v7.widget.RecyclerView
android:id="#+id/farazList"
android:scrollbars="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="left|center_vertical" />
</com.neganet.QRelations.fragments.SwipeListenerFragment>
you see that begin tag is the class that we made. now in fragment class:
View view=inflater.inflate(R.layout.fragment_main_list, container, false);
SwipeListenerFragment tdView=(SwipeListenerFragment) view;
tdView.registerToSwipeEvents(this);
and then Implement SwipeListenerFragment.onSwipeEventDetected in it:
#Override
public void swipeLeftDetected() {
Toast.makeText(getActivity(), "left", Toast.LENGTH_SHORT).show();
}
#Override
public void swipeRightDetected() {
Toast.makeText(getActivity(), "right", Toast.LENGTH_SHORT).show();
}
It's a little complicated but works perfect :)
Heres the dilemma:
I am showing a screen with 3 input fields and 2 buttons inside of a tab(there are 3 tabs total, and they are on the bottom of the screen). the 2 buttons are set to the bottom left and right of the screen, right above the tabs. when i click on an input field, the tabs and buttons are all pushed up on top of the keyboard.
i desire to only push the buttons up, and leave the tabs where they originally are, on the bottom. i am thinking of setting the visibility of the tabs to GONE once i determine that the soft keyboard is showing, and visibility to VISIBLE once the soft keyboard is gone.
is there some kind of listener for the soft keyboard, or maybe the input field? maybe some tricky use of OnFocusChangeListener for the edit text? How can i determine whether the keyboard is visible or not?
Determining if the keyboard is showing is not possible apparently.
You might want to disable it alltogether with the windowSoftInputMode xml tag in your manifest: http://developer.android.com/reference/android/R.attr.html#windowSoftInputMode. Or you can have a look on how to remove focus to hide the keyboard: Hide soft keyboard on activity without any keyboard operations.
Neither will exactly solve your problem. I remember reading a blogpost strongly advising not to use tabs at the bottom, rather than the top of the screen, for UI clarity reasons. I recommend you to follow up on that.
As far as I know, you can't.
However, you can monitor size changes of your layout, and since keyboard showing up is the main cause for the resizes, you might be able to assume that the keyboard is shown or not.
Here's a sample code for monitoring the size-changes of a layout. Just use this layout as the parent of your original layout, and use its listener. If the height has decreased, you can assume the keyboard is shown, and if it was increased, you can assume it got closed.
public class LayoutSizeChangedSensorFrameLayout extends FrameLayout {
public enum SizeChange {
HEIGHT_INCREASED, HEIGHT_DECREASED, WIDTH_INCREASED, WIDTH_DECREASED
}
public interface OnLayoutSizeChangedListener {
void onSizeChanged(EnumSet<SizeChange> direction);
}
private OnLayoutSizeChangedListener mLayoutSizeChangeListener;
public LayoutSizeChangedSensorFrameLayout(final Context context) {
super(context);
}
public LayoutSizeChangedSensorFrameLayout(final Context context, final AttributeSet attributeSet) {
super(context, attributeSet);
}
public LayoutSizeChangedSensorFrameLayout(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mLayoutSizeChangeListener != null) {
final EnumSet<SizeChange> result = EnumSet.noneOf(SizeChange.class);
if (oldh > h)
result.add(SizeChange.HEIGHT_DECREASED);
else if (oldh < h)
result.add(SizeChange.HEIGHT_INCREASED);
if (oldw > w)
result.add(SizeChange.WIDTH_DECREASED);
else if (oldw < w)
result.add(SizeChange.WIDTH_INCREASED);
if (!result.isEmpty())
mLayoutSizeChangeListener.onSizeChanged(result);
}
}
public void setOnLayoutSizeChangedListener(final OnLayoutSizeChangedListener layoutSizeChangeListener) {
this.mLayoutSizeChangeListener = layoutSizeChangeListener;
}
public OnLayoutSizeChangedListener getOnLayoutSizeChangeListener() {
return mLayoutSizeChangeListener;
}
}
This might help
http://developer.android.com/reference/android/inputmethodservice/InputMethodService.html
https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/master/java/src/com/android/inputmethod/latin
The keyboard code is here, I grepped through it a little but give up, I don't see any Broadcasts being sent that would be of any use, at least in here. Maybe you can go find lower level code in the repo and find a useful Intent being sent. The first linke might tell you when it becomes visible, but I'm not sure how to tell when it becomes invisible.
Working solution for me -
ViewTreeObserver treeObserver = getViewTreeObserver();
if (treeObserver != null)
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int pHeight = getResources().getDisplayMetrics().heightPixels;
Rect visRect = new Rect();
viewGroup.getWindowVisibleDisplayFrame(visRect);
boolean keyboardVisible;
int keyboardHeight= pHeight-currentHeight;
if (keyboardHeight > (MIN_KEYBOARD_HEIGHT*pHeight))
{
TGVLog.d(debugTag, "keyboardVisible-- "+true);
keyboardVisible = true;
}
else
{
TGVLog.d(debugTag, "keyboardVisible-- "+false);
keyboardVisible = false;
}
}
});
I finally find solution after looking for several class.
I decide to use WindowsInset because it not call every seconde like use global layout.
There is a video talking about how to use windowsInset. Link
In my last application I had some issues in displaying the soft keyboard, then I have used the following code in my Manifest file:
<activity
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation"
android:name=".rate.Calculator"/>
I have a scrollview and I only want an event to happen if it's already scrolled to the bottom but I can't find a way to check if the scrollview is at the bottom.
I have solved it for the opposite; only allow the event to happen if it's already scrolled to the top:
ScrollView sv = (ScrollView) findViewById(R.id.Scroll);
if(sv.getScrollY() == 0) {
//do something
}
else {
//do nothing
}
I found a way to make it work. I needed to check the measured height of the child to the ScrollView, in this case a LinearLayout. I use the <= because it also should do something when scrolling isn't necessary. I.e. when the LinearLayout is not as high as the ScrollView. In those cases getScrollY is always 0.
ScrollView scrollView = (ScrollView) findViewById(R.id.ScrollView);
LinearLayout linearLayout = (LinearLayout) findViewById(R.id.LinearLayout);
if(linearLayout.getMeasuredHeight() <= scrollView.getScrollY() +
scrollView.getHeight()) {
//do something
}
else {
//do nothing
}
Here it is:
public class myScrollView extends ScrollView
{
public myScrollView(Context context)
{
super(context);
}
public myScrollView(Context context, AttributeSet attributeSet)
{
super(context,attributeSet);
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
{
View view = (View)getChildAt(getChildCount()-1);
int d = view.getBottom();
d -= (getHeight()+getScrollY());
if(d==0)
{
//you are at the end of the list in scrollview
//do what you wanna do here
}
else
super.onScrollChanged(l,t,oldl,oldt);
}
}
You can either use myScrollView in xml layout or instantiate it in your code.
Hint: with code above if user hits the end of the list 10 times frequently then your code will run 10 times. In some cases like when you want to load data from a remote server to update your list, that behavior would be undesired (most probably). try to predict undesired situation and avoid them.
Hint 2: sometimes getting close to the end of the list may be the right time to let your script run. for example user is reading a list of articles and is getting close to the end. then you start loading more before list finishes. To do so, just do this to fulfill your purpose:
if(d<=SOME_THRESHOLD) {}
You can get the maximum scroll for the width by doing this
int maxScroll = yourScrollView.getChildAt(0).getMeasuredWidth() - yourScrollView.getWidth();
change getWidth() to getHeight() for vertical scroll views
If scrollView.getHeight() == scrollView.getScrollY + screensize, then it is scrolled to bottom.