Seems to be a common problem without a great solution that I have found. Goal is to stop a ScrollView from auto-scrolling to an EditText (or any view for that matter) that has focus.
You have a bunch of views (Buttons, TextViews, etc) in an ScrollView, one of which is an EditText. Upon clicking say a Button within the ScrollView, the ScrollView scrolls down to the EditText (its off screen). This is not desired, as there are other elements that you don't want scrolled off the screen.
Now I can stop this from happening when the screen first shows by having other focusable elements in the ScrollView. However, the general problem still exists. The user scrolls down manually to the EditText, enters some numbers, then scrolls up to the top (EditText off screen now), they click a button in the ScrollView, and guess what? The ScrollView scrolls down to that darn EditText.
I'm thinking about extending the ScrollView and overriding some of the methods there like findFocusableViewInBounds, but I have a feeling I'll just be getting myself into more trouble.
Please help if you can.
I've played around with things like having an 0 height EditText at the top of my ScrollView, adding Next Focusable element properties to the other items in the ScrollView, etc. I suppose one "hack" might be to get the EditText to lose focus when the virtual or manual keyboard gets hidden or something.
After struggling with that problem for quite some time, I've found a solution that seems to work without being too ugly. First, make sure that whatever ViewGroup (directly) contains your EditText has descendantFocusability set to "Before Descendants," focusable set to "true" and focusableInTouchMode set to "true." This will not be the ScrollView itself, but the layout inside where you have your various views. Next add an onTouchListener to your ScrollView that removes focus from the EditText whenever it is touched, like so:
ScrollView scroll = (ScrollView)findViewById(R.id.scrollView1);
scroll.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (myEditText.hasFocus()) {
myEditText.clearFocus();
}
return false;
}
});
Tell me if that doesn't fix it. What should happen is that the Layout gets focus instead of the EditText, so no scrolling should happen.
Just create an empty view at the top of linearlayout
<View android:layout_width="fill_parent" android:id="#+id/focus_view" android:layout_height="0dp" android:focusable="true" android:focusableInTouchMode="true"><requestFocus/></View>
Single line solves the problem
I had the same problem. There's one trick that I'm using to deal with this problem:
public void onClick(View v) {
button.requestFocusFromTouch(); //prevents from loosing focus and scrolling view down
....
}
The issue is not on the java code, but on the manifest code.
In your AndroidManifest.xml add an attribute to the Activity:
<activity android:name=".MyActivity" android:windowSoftInputMode="adjustPan"> </activity>
By adding 2 parameters in:
android:focusable="true"
android:focusableInTouchMode="true"
In which Main layout is there.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/layMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/background"
android:focusable="true"
android:focusableInTouchMode="true">
By this EditText will not be auto focused.
Here is what I did
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" style="#style/measurementTableRowStyle"
android:focusable="true" android:layout_height="fill_parent">
<requestFocus></requestFocus>
<LinearLayout android:id="#+id/linearLayout1"
android:orientation="horizontal" android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:id="#+id/desc_text" android:text="Value : "
style="#style/attributeNameTextStyle" android:layout_width="wrap_content"
android:focusable="true" android:layout_height="wrap_content">
<requestFocus></requestFocus>
</TextView>
<TextView style="#style/attributeValueStyle" android:id="#+id/value_text"
android:text="TextView" android:layout_width="wrap_content"
android:layout_height="wrap_content"></TextView>
</LinearLayout>
The reason is in such cases you have to make all other views focus-able inside the scrollview by an explicit android:focusable="true" and then <requestFocus></requestFocus> . This should work everytime IMO
thomas88wp answer, https://stackoverflow.com/a/6486348/528746 worked for me.
But I had two problems:
1. When scrolling, I wanted to hide the keyboard
2. I had lots of EditText views and didn't want to write it for each one of them
(I do getActivity() since I'm writing this inside a Fragment and not an activity)
ScrollView scroll = (ScrollView)view.findViewById(R.id.layout_scroll);
scroll.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// Check if the view with focus is EditText
if (getActivity().getCurrentFocus() instanceof EditText)
{
EditText ed = (EditText)getActivity().getCurrentFocus();
if (ed.hasFocus()) {
// Hide the keyboard
InputMethodManager inputManager = (InputMethodManager)
getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
// Clear the focus
ed.clearFocus();
}
}
return false;
}
});
My fix to this most horrific bug, (worth noting that this is pre API11 only where they modified the fling method not to be stupid).
The old fling method finds the next focus that it will get to.. which isn't really that helpful. Other versions of this class don't really work as they stop focus working when the user genuinely traverses the form from the keyboard.
public class NonFocusingScrollView extends ScrollView {
private boolean mBlockRequestFocusOnFling = false;
public NonFocusingScrollView(Context context) {
super(context);
}
public NonFocusingScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NonFocusingScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public ArrayList<View> getFocusables(int direction) {
if(mBlockRequestFocusOnFling)
return new ArrayList<View>();
return super.getFocusables(direction);
}
#Override
public void requestChildFocus(View child, View focused) {
if(!mBlockRequestFocusOnFling)
super.requestChildFocus(child, focused);
}
#Override
public void fling(int velocityY) {
mBlockRequestFocusOnFling = true;
super.fling(velocityY);
mBlockRequestFocusOnFling = false;
}
}
I was having a similar problem and finally got it to work. My scroll view contains a series of customized buttons, followed by an EditText (which normally has focus, but I don't want it to be losing focus). Any time the buttons were clicked, the scroll view auto-scrolled to the focused EditText. Overriding public boolean requestChildRectangleOnScreen(final View child, final Rect rectangle, final boolean immediate) and always returning false (default behavior of a ViewGroup) did the trick. Hope it helps with your situation too.
We can write a custom ScrollView and override the onScrollChanged method and clear the focus from the focused view and optionally hide the keyboard.
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
View v = getFocusedChild();
if (v != null) {
InputMethodManager imm = (InputMethodManager) getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
v.clearFocus();
}
super.onScrollChanged(l, t, oldl, oldt);
}
I often has this problem when my apps handle orientation change.
In that case I use the following kind of code:
#Override
protected void onCreate(Bundle savedInstanceState) {
...
// to avoid the scrollview to scroll to this element automatically
mEditTextSearch.setFocusable(false);
// Get the saved scroll position
final int scrolly = savedInstanceState.getInt("scrolly");
mScrollView.post(new Runnable() {
#Override
public void run() {
mScrollView.scrollTo(0, scrolly);
// Restore the initial state of the EditText
mEditTextSearch.setFocusable(true);
mEditTextSearch.setFocusableInTouchMode(true);
mEditTextSearch.setClickable(true);
}
});
...
}
Another version of thomas88wp's code:
ScrollView scroll = (ScrollView)getActivity().findViewById(R.id.scrollView_addNewBill);
scroll.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View arg0, MotionEvent arg1) {
View focussedView = getCurrentFocus();
if( focussedView != null ) focussedView.clearFocus();
return false;
}
});
I made a test project to experiment with the various solutions if anyone wants to play with it.
https://github.com/marchold/EditText-ErrorPopup-Scroll-View
Create a custom ScrollView (create a class and have it extend HorizontalScrollView) and make a getter setter for scrollable. Then override computeScrollDeltaToGetChildRectOnScreen.
How it works: Every time android has an edit text or something in focus that is off screen it calls method computeScrollDeltaToGetChildRectOnScreen to bring it into view. If you Override it and return 0 when it is disabled than it will not scroll...
So you will have A custom scroll view like this:
public class TrackableHorizontalScrollView extends HorizontalScrollView {
// true if we can scroll (not locked)
// false if we cannot scroll (locked)
private boolean mScrollable = true;
public TrackableHorizontalScrollView(Context context) {
super(context);
}
public TrackableHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TrackableHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setScrollingEnabled(boolean enabled) {
mScrollable = enabled;
}
public boolean isScrollable() {
return mScrollable;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// if we can scroll pass the event to the superclass
if (mScrollable) return super.onTouchEvent(ev);
// only continue to handle the touch event if scrolling enabled
return mScrollable; // mScrollable is always false at this point
default:
return super.onTouchEvent(ev);
}
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Don't do anything with intercepted touch events if
// we are not scrollable
if (!mScrollable) return false;
else return super.onInterceptTouchEvent(ev);
}
#Override
public void scrollTo(int x, int y){
if (!mScrollable) return;
super.scrollTo(x, y);
}
//Custom smooth scroll method since norm is final and cannot be overridden
public final void smooothScrollToIfEnabled(int x, int y){
if (!mScrollable) return;
smoothScrollTo(x, y);
}
#Override
protected int computeScrollDeltaToGetChildRectOnScreen(android.graphics.Rect rect){
if (!mScrollable) return 0;
return super.computeScrollDeltaToGetChildRectOnScreen(rect);
}
}
You can use this inside your XML like this:
<com.your.package.ui.widget.TrackableHorizontalScrollView
android:id="#+id/wi_et_credit_scroller"
android:layout_toRightOf="#id/wi_et_credit_iv"
android:layout_width="fill_parent"
android:scrollbars="none"
android:layout_height="wrap_content"
android:paddingRight="5dp"
android:layout_gravity="center_vertical">
<!--Whatever you have inside the scrollview-->
</com.your.package.ui.widget.TrackableHorizontalScrollView>
The best Solution is to add focus options for the child of your scrollview :
android:descendantFocusability="beforeDescendants"
android:focusable="true"
android:focusableInTouchMode="true"
Then your xml file will look like :
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="50dp"
android:descendantFocusability="beforeDescendants"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical" >
<EditText
android:id="#+id/editText_one"
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="TestApp 1" />
<EditText
android:id="#+id/editText_two"
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="TestApp 2" />
<EditText
android:id="#+id/editText_three"
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="TestApp 3" />
</LinearLayout>
</ScrollView>
For me, it didn't work to override ScrollView onTouch. Also did not work android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
This and another mentioned solutions only worked for the first time - only when EditText is not selected, but once you select it, scrollview autoscrolls again.
Because I was already written a code to hide a keyboard when touching other views, I just added two lines of code and it worked like a champ:
public static void setupUI(final Activity activity, final View view) {
//view is the parent view in your layout
OnTouchListener mTouchListener = new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
try {
View vFocused = null;
vFocused = activity.getCurrentFocus();
if (vFocused != null) {
hideSoftKeyboard(activity, v);
if (vFocused instanceof EditText) {
vFocused.clearFocus();//this is the trick to avoid ScrollView autoscroll
}
}
} catch (Exception e) {
}
return false;
}
};
// Set up touch listener for non-text box views to hide keyboard.
if (!(view instanceof EditText) && !(view instanceof ViewGroup)) {
view.setOnTouchListener(mTouchListener);
}
// If a layout container, iterate over children and seed recursion.
if (view instanceof ViewGroup) {
view.setOnTouchListener(mTouchListener);
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
View innerView = ((ViewGroup) view).getChildAt(i);
setupUI(activity, innerView);
}
}
}
public static void hideSoftKeyboard(Context context, View v) {
InputMethodManager inputMethodManager = (InputMethodManager) context
.getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
also added this in root view:
android:descendantFocusability="beforeDescendants" android:focusableInTouchMode="true"
Maybe its not really nice solution, but its working.
My solution is below, to trace the source code and override some function to stop auto scrolling by focused item.
You can check if the focusedView is TextView or its child is TextView,
by using focusedView.findViewById(R.id.textview_id_you_defined) != null or focusedView instanceof TextView == true.
public class StopAutoFocusScrollView extends ScrollView {
private View focusedView;
private ScrollMonitorListener listener;
public interface ScrollMonitorListener {
public boolean enableScroll(View view);
}
public StopAutoFocusScrollView(Context context) {
super(context);
}
public StopAutoFocusScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public StopAutoFocusScrollView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
public void setScrollMonitorListener(ScrollMonitorListener listener) {
this.listener = listener;
}
#Override
public void requestChildFocus(View child, View focused) {
focusedView = focused
super.requestChildFocus(child, focused);
}
//flow : requestChildFocus -> scrollToChild -> scrollBy
//Therefore, you can give listener to determine you want scroll to or not
#Override
public void scrollBy(int x, int y) {
if (listener == null || listener.enableScroll(focusedView)) {
super.scrollBy(x, y);
}
}
}
I had a slightly different objection to this infuriating deficiency. Whenever I tapped one of a number of RadioButtons below the EditTexts, the scroll position jumped to accommodate what Android determined to be the visible and focused EditText.
All attempts to retain the current desired scroll position via a Runnable that issued ScrollView.scrollTo(x,y) were dutifully IGNORED by Android!
I share my solution in the hope that it may save someone else 8 (eight) wasted hours.
/* This interesting little 'hack' prevents unwanted scroll 'jump' occurring when
user touches a RadioButton for example
[ Causes focus to change - but maybe this is a lesser evil! ] */
mScrollView.setOnTouchListener(new View.OnTouchListener() {
#Override public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() != MotionEvent.ACTION_UP)
return false;
mScrollView.clearFocus();
return false;
}
});
Only this code works for me:
public static void preventScrollViewFromScrollingToEdiText(ScrollView view) {
view.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
v.requestFocusFromTouch();
return false;
}
});
}
All credits go to this original answer.
Try this one :)
public class CustomHorizontalScrollView extends HorizontalScrollView {
public CustomHorizontalScrollView(Context context) {
super(context);
}
public CustomHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
return super.onTouchEvent(ev);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
#Override
public void scrollTo(int x, int y) {
super.scrollTo(x, y);
}
//Custom smooth scroll method since norm is final and cannot be overridden
public final void smooothScrollToIfEnabled(int x, int y) {
smoothScrollTo(x, y);
}
#Override
protected int computeScrollDeltaToGetChildRectOnScreen(android.graphics.Rect rect) {
/* if (getContext() != null && getContext() instanceof Activity) {
Activity activity = (Activity) getContext();
if (!activity.isFinishing()) {
View view = activity.getCurrentFocus();
if (view != null) {
if (view instanceof EditText) {
return 0;
}
}
}
}
return super.computeScrollDeltaToGetChildRectOnScreen(rect);
*/
return 0;
}
}
I solved this problem adding the descendantFocusability attribute to the ScrollView's containing LinearLayout, with the value blocksDescendants.
For example:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:descendantFocusability="blocksDescendants" >
None of these answers worked for me.
What I did was, I added: android:overScrollMode="never" in my ScrollView and set the height to wrap_content.
My view was very complex as it was legacy code with LinearLayout inside LinearLayout inside LinearLayout.
This helped me, hope it will help someone else too!
Related
I am using a ViewPager with a TouchImageView inside it and it works great, (I have used this solution in many of my Android apps).
However I have an app for which there are many other controls on the same screen so they are all inside a scrollview control.
In this scenario I see the scrollview does not play nice and I am not able to pan within the zoomed image. When I use my finger to pan upward or downward the entire page scrolls instead of the image panning.
So here is what I am trying to do....
Inside the TouchImageView I detect Zoom Begin and Zoom End and have created an interface to make a callback to my Activity onZoomBegin() and onZoomEnd() methods.
In the onZoomBegin() method I want to disable the scrollview from responding to any touch events and in onZoomEnd() I can re-enable it.
So far here are the things I have tried doing in the onZoomBegin() method for which none are working....
scrollView.setEnabled(false);
scrollView.requestDisallowInterceptTouchEvent(true);
also I have tried the answer to a similar question which was to takeover the onTouchListener like such:
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
This does stop the scrollview from scrolling but the scrollview is still intercepting the touch events cause the image still will not pan up or down.
I've tried checking nestedScrollingEnabled in the layout designer, no joy....
I just want to know is there a way to totally disable a scrollview and then re-enable it from responding to touch events?
I found this answer on another question somewhere but by the time I realized it was the solution to my problem (answer to my question) then I lost reference to it. I will keep looking so I can edit this post to give credit where credit is due.
public class CustomScrollView extends ScrollView {
// true if we can scroll the ScrollView
// false if we cannot scroll
private boolean scrollable = true;
public CustomScrollView(Context context) {
super(context);
}
public CustomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setScrollingEnabled(boolean scrollable) {
this.scrollable = scrollable;
}
public boolean isScrollable() {
return scrollable;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// if we can scroll pass the event to the superclass
if (scrollable)
return super.onTouchEvent(ev);
// only continue to handle the touch event if scrolling enabled
return false; // scrollable is always false at this point
default:
return super.onTouchEvent(ev);
}
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Don't do anything with intercepted touch events if
// we are not scrollable
if (!scrollable)
return false;
else
return super.onInterceptTouchEvent(ev);
}
}
This part I just figured out for myself.... In the TouchImageView I added a callback interface which is called when a zoom begins and ends so in my Activity I only had to do this:
private class OnZoomListener implements TouchImageView.OnZoomListener {
#Override
public void onZoomBegin() {
isZoomed = true;
scrollView.scrollTo(0, 0);
scrollView.setScrollingEnabled(false); // <-- disables scrollview
hideImageControls();
sizeViewPager();
}
#Override
public void onZoomEnd() {
scrollView.setScrollingEnabled(true); // <-- enables scrollview
showImageControls();
isZoomed = false;
}
}
I have a use case where there are two views on screen one of which is partially covering another. The one that is above needs to handle scroll events and ignore touch up. The partially obscured view should handle touch up events, including those that happen in the area of overlap that are ignored by the obscuring view.
a simplified example layout is below.
the closest i've come uses GestureDetectorCompat on the top view returning true in onDown (otherwise i don't get any further events,) true in onScroll, and false in onSingleTapUp. i have tried several things in the view behind all with the same results: i get taps on the un-obscured section, but the top view eats all of the motion events for the obscured portion.
What you want to do is not as straightforward as you would probably like because of how Android handles touch event flow. So let me set the stage with a little context first:
The reason this is a tricky proposition is because Android defines a gesture as all the events between an ACTION_DOWN and the corresponding ACTION_UP. ACTION_DOWN is the only point at which the framework is searching for a touch target (which is why you have to return true for that event to see any others). Once a suitable target has been found, ALL the remaining events in that gesture will be delivered directly to that view and nobody else.
This means that if you want a single event to go to a different destination, you will have to capture and redirect it yourself. All touch events flow from parent views to child views in one long chain. Parent views control when and how touch events move from one child to the next, including modifying the coordinates of the MotionEvent so the match the local bounds of each child view. Because of this, the most effective place to manipulate touch events is in a custom ViewGroup parent implementation.
The following example comes with a big bag of assumptions. Basically, I'm assuming that both views are nothing more than a dumb View with no internal wishes to handle touch (which is probably wrong). Applying this code to other, more complex, child views may requires some rework...but this should get you started.
The best place to force touch redirection is in a common parent of the two views, since it is the origin of the touch for both (as described above).
public class TouchUpRedirectLayout extends FrameLayout implements View.OnTouchListener {
private int mTargetViewId;
private View mTargetView;
private boolean mTargetTouchActive;
private GestureDetector mGestureDetector;
public TouchUpRedirectLayout(Context context) {
super(context);
init(context);
}
public TouchUpRedirectLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public TouchUpRedirectLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
mGestureDetector = new GestureDetector(context, mGestureListener);
}
public void setTargetViewId(int resId) {
mTargetViewId = resId;
updateTargetView();
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
//Find the target view, if set, once inflated
updateTargetView();
}
//Set the target view to handle gestures
private void updateTargetView() {
if (mTargetViewId > 0) {
mTargetView = findViewById(mTargetViewId);
if (mTargetView != null) {
mTargetView.setOnTouchListener(this);
}
}
}
private Rect mHitRect = new Rect();
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_UP:
if (mTargetTouchActive) {
mTargetTouchActive = false;
//Validate the up
int index = indexOfChild(mTargetView) - 1;
if (index < 0) {
return false;
}
for (int i=index; i >= 0; i--) {
final View child = getChildAt(i);
child.getHitRect(mHitRect);
if (mHitRect.contains((int) event.getX(), (int) event.getY())) {
//Dispatch and mark handled
return child.dispatchTouchEvent(event);
}
}
//Steal this event
return true;
}
//Allow default processing
return false;
default:
//Allow default processing
return false;
}
}
//Receive touch events from the target (scroll handling) view
#Override
public boolean onTouch(View v, MotionEvent event) {
mTargetTouchActive = true;
return mGestureDetector.onTouchEvent(event);
}
//Handle gesture events in target view
private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onDown(MotionEvent e) {
Log.d("TAG", "onDown");
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.d("TAG", "Scrolling...");
return true;
}
};
}
This example layout (I subclassed FrameLayout, but you could choose whichever layout you are using currently as the parent of the two views) tracks a single "target" view for the purposes of notifying the "down" and "scroll" gestures. It also notifies us when a gesture is in play that will include an ACTION_UP event that we need to capture and forward to another obscured view.
When an up event occurs, we use the intercept functionality of ViewGroup to direct that event away from the original "target" view, and dispatch it to the next available child view whose bounds fit the event. You could just as easily hard-code the second "obscured" view here as well, but I've written it to dispatch to any and all possible children underneath...similar to the way ViewGroup handles touch delegation to children in the first place.
Here is an example layout:
<com.example.touchoverlaptest.app.TouchUpRedirectLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/view_root"
android:layout_width="match_parent"
android:layout_height="400dp"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:paddingBottom="#dimen/activity_vertical_margin"
tools:context="com.example.touchoverlaptest.app.MainActivity">
<View
android:id="#+id/view_obscured"
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="#7A00" />
<View
android:id="#+id/view_overlap"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_gravity="bottom"
android:background="#70A0" />
</com.example.touchoverlaptest.app.TouchUpRedirectLayout>
...and Activity with the view in action:
public class MainActivity extends Activity implements View.OnTouchListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TouchUpRedirectLayout layout = (TouchUpRedirectLayout) findViewById(R.id.view_root);
layout.setTargetViewId(R.id.view_overlap);
layout.findViewById(R.id.view_obscured).setOnTouchListener(this);
}
#Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("TAG", "Obscured touch "+event.getActionMasked());
return true;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
The target view will fire all the gesture callbacks, and the obscured view will receive the up events. The OnTouchListener in the activity is simply to validate that the events are delivered.
If you would like more detail about custom touch handling in Android, here is a video link to a presentation I did recently on the topic.
So I have an Activity that is using windowSoftInputMode="adjustPan", and I have a OnPreDrawListener for an EditText that calls:
editText.requestFocus();
inputManager.showSoftInput(editText, 0);
Which works as expected and pushes the Activity up to make room for the EditText. However, if I dismiss the keyboard with the back button (which pans the window back to the original location), then touch the EditText again to show the keyboard, the keyboard shows, but the window does not adjust.
I've even tried adding an OnClickListener to the EditText and calling the same two calls again:
editText.requestFocus();
inputManager.showSoftInput(editText, 0);
But the window does not pan until I dismiss the window and show it again. Any suggestions?
I think this three steps will solve your problem.
1)in manifest file change windowSoftInputMode="adjustPan"
to windowSoftInputMode="adjustResize"
2) The layout you are using for EditText , tha change the parent layout to ScroolView.
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/android:list"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<EditText
android:id="#+id/edittextview"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:inputType="textFilter"
android:padding="10dp"
android:imeOptions="actionDone"
android:scrollbars="vertical"
android:textSize="14sp" />
</ScrollView>
3) To explicitly showing keyboard to avoid "dismiss the keyboard with the back button and window is not Adjust"
in Edittext onclick write
edittext.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
InputMethodManager m = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (m != null) {
m.toggleSoftInput(0, InputMethodManager.SHOW_IMPLICIT);
edittext.requestFocus();
}
}
});
Hope this will solve your problem.
So it's not exactly a solution to the problem, but it's the workaround I ended up going with. I created a subclass of LinearLayout to intercept the back button press before the IME receives it:
public class IMEInterceptLinearLayout extends LinearLayout {
//For some reason, the event seems to occur twice for every back press
//so track state to avoid firing multiple times
private boolean notifiedListener = false;
private OnBackPressedPreIMEListener listener;
public IMEInterceptLinearLayout(Context context) {
super(context);
}
public IMEInterceptLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public IMEInterceptLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setOnBackPressedPreIMEListener(OnBackPressedPreIMEListener listener) {
this.listener = listener;
}
private void fireOnBackPressedPreIME() {
if(listener != null && !notifiedListener) {
listener.onBackPressedPreIME();
notifiedListener = true;
}
}
#Override
public boolean dispatchKeyEventPreIme(KeyEvent event) {
if(event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
fireOnBackPressedPreIME();
return true;
} else return super.dispatchKeyEventPreIme(event);
}
public interface OnBackPressedPreIMEListener {
public void onBackPressedPreIME();
}
}
Then from there I just registered my window as a listener for the layout, and dismiss the window and the keyboard when I receive the event, disallowing the keyboard to be dismissed while the window is visible.
I have some views that I make visible upon a button press. I want them to disappear if I click outside of those views.
How would this be done on Android?
Also, I realize that the "back button" can also assist Android users with this - I might use that as a secondary way to close the views - but some of the tablets aren't even using a 'physical' back button anymore, it has been very de-emphasized.
An easy/stupid way:
Create a dummy empty view (let's say ImageView with no source), make it fill parent
If it is clicked, then do what you want to do.
You need to have the root tag in your XML file to be a RelativeLayout. It will contain two element: your dummy view (set its position to align the Parent Top). The other one is your original view containing the views and the button (this view might be a LinearLayout or whatever you make it. don't forget to set its position to align the Parent Top)
Hope this will help you, Good Luck !
Find the view rectangle, and then detect whether the click event is outside the view.
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Rect viewRect = new Rect();
mTooltip.getGlobalVisibleRect(viewRect);
if (!viewRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
setVisibility(View.GONE);
}
return true;
}
If you want to use the touch event other place, try
return super.dispatchTouchEvent(ev);
This is an old question but I thought I'd give an answer that isn't based on onTouch events. As was suggested by RedLeader it's also possible to achieve this using focus events. I had a case where I needed to show and hide a bunch of buttons arranged in a custom popup, ie the buttons were all placed in the same ViewGroup. Some things you need to do to make this work:
The view group that you wish to hide needs to have View.setFocusableInTouchMode(true) set. This can also be set in XML using android:focusableintouchmode.
Your view root, i.e. the root of your entire layout, probably some kind of Linear or Relative Layout, also needs to be able to be focusable as per #1 above
When the view group is shown you call View.requestFocus() to give it focus.
Your view group need to either override View.onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) or implement your own OnFocusChangeListener and use View.setOnFocusChangeListener()
When the user taps outside your view focus is transferred to either the view root (since you set it as focusable in #2) or to another view that inherently is focusable (EditText or similar)
When you detect focus loss using one of the methods in #4 you know that focus has be transferred to something outside your view group and you can hide it.
I guess this solution doesn't work in all scenarios, but it worked in my specific case and it sounds as if it could work for the OP as well.
I've been looking for a way to close my view when touching outside and none of these methods fit my needs really well. I did find a solution and will just post it here in case anyone is interested.
I have a base activity which pretty much all my activities extend. In it I have:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (myViewIsVisible()){
closeMyView();
return true;
}
return super.dispatchTouchEvent(ev);
}
So if my view is visible it will just close, and if not it will behave like a normal touch event. Not sure if it's the best way to do it, but it seems to work for me.
base on Kai Wang answer : i suggest first check visibility of Your view , base on my scenario when user clicked on fab myView become visible and then when user click outside myView disappears
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Rect viewRect = new Rect();
myView.getGlobalVisibleRect(viewRect);
if (myView.getVisibility() == View.VISIBLE && !viewRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
goneAnim(myView);
return true;
}
return super.dispatchTouchEvent(ev);
}
I needed the specific ability to not only remove a view when clicking outside it, but also allow the click to pass through to the activity normally. For example, I have a separate layout, notification_bar.xml, that I need to dynamically inflate and add to whatever the current activity is when needed.
If I create an overlay view the size of the screen to receive any clicks outside of the notification_bar view and remove both these views on a click, the parent view (the main view of the activity) has still not received any clicks, which means, when the notification_bar is visible, it takes two clicks to click a button (one to dismiss the notification_bar view, and one to click the button).
To solve this, you can just create your own DismissViewGroup that extends ViewGroup and overrides the following method:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ViewParent parent = getParent();
if(parent != null && parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(this);
}
return super.onInterceptTouchEvent(ev);
}
And then your dynamically added view will look a little like:
<com.example.DismissViewGroup android:id="#+id/touch_interceptor_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent" ...
<LinearLayout android:id="#+id/notification_bar_view" ...
This will allow you to interact with the view, and the moment you click outside the view, you both dismiss the view and interact normally with the activity.
Implement onTouchListener(). Check that the coordinates of the touch are outside of the coordinates of your view.
There is probably some kind of way to do it with onFocus(), etc. - But I don't know it.
Step 1: Make a wrapper view by Fragmelayout which will cover your main layout.
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- This is your main layout-->
</RelativeLayout>
<View
android:id="#+id/v_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- This is the wrapper layout-->
</View>
</FrameLayout>
Step 2: Now add logic in your java code like that -
View viewOverlay = findViewById(R.id.v_overlay);
View childView = findViewByID(R.id.childView);
Button button = findViewByID(R.id.button);
viewOverlay.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
childView.setVisibility(View.GONE);
view.setVisibility(View.GONE);
}
});
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
childView.setVisibility(View.VISIBLE);
// Make the wrapper view visible now after making the child view visible for handling the
// main visibility task.
viewOverlay.setVisibility(View.VISIBLE);
}
});
To hide the view when click performs outside the view:
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (isMenuVisible) {
if (!isWithinViewBounds(ev.rawX.toInt(), ev.rawY.toInt())) {
hideYourView()
return true
}
}
return super.dispatchTouchEvent(ev)
}
create a method to get the bounds(height & width) of your view, so when you click outside of your view it will hide the view and when click on the view will not hide:
private fun isWithinViewBounds(xPoint: Int, yPoint: Int): Boolean {
val l = IntArray(2)
llYourView.getLocationOnScreen(l)
val x = l[0]
val y = l[1]
val w: Int = llYourView.width
val h: Int = llYourView.height
return !(xPoint < x || xPoint > x + w || yPoint < y || yPoint > y + h)
}
I've created custom ViewGroup to display info box anchored to another view (popup balloon).
Child view is actual info box, BalloonView is fullscreen for absolute positioning of child, and intercepting touch.
public BalloonView(View anchor, View child) {
super(anchor.getContext());
//calculate popup position relative to anchor and do stuff
init(...);
//receive child via constructor, or inflate/create default one
this.child = child;
//this.child = inflate(...);
//this.child = new SomeView(anchor.getContext());
addView(child);
//this way I don't need to create intermediate ViewGroup to hold my View
//but it is fullscreen (good for dialogs and absolute positioning)
//if you need relative positioning, see #iturki answer above
((ViewGroup) anchor.getRootView()).addView(this);
}
private void dismiss() {
((ViewGroup) getParent()).removeView(this);
}
Handle clicks inside child:
child.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//write your code here to handle clicks inside
}
});
To dismiss my View by click outside WITHOUT delegating touch to underlying View:
BalloonView.this.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
dismiss();
}
});
To dismiss my View by click outside WITH delegating touch to underlying View:
#Override
public boolean onTouchEvent(MotionEvent event) {
dismiss();
return false; //allows underlying View to handle touch
}
To dismiss on Back button pressed:
//do this in constructor to be able to intercept key
setFocusableInTouchMode(true);
requestFocus();
#Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
dismiss();
return true;
}
return super.onKeyPreIme(keyCode, event);
}
I want to share my solution which I think it could be useful if :
you are able to add a custom ViewGroup as root layout
also the view which you want to disappear can be a custom one.
First, we create a custom ViewGroup to intercept touch events:
class OutsideTouchDispatcherLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val rect = Rect()
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
val x = ev.x.roundToInt()
val y = ev.y.roundToInt()
traverse { view ->
if (view is OutsideTouchInterceptor) {
view.getGlobalVisibleRect(rect)
val isOutside = rect.contains(x, y).not()
if (isOutside) {
view.interceptOutsideTouch(ev)
}
}
}
}
return false
}
interface OutsideTouchInterceptor {
fun interceptOutsideTouch(ev: MotionEvent)
}
}
fun ViewGroup.traverse(process: (View) -> Unit) {
for (i in 0 until childCount) {
val child = getChildAt(i)
process(child)
if (child is ViewGroup) {
child.traverse(process)
}
}
}
As you see, OutsideTouchDispatcherLayout intercepts touch events and informs each descendent view which implenets OutsideTouchInterceptor that some touch event occured outside of that view.
Here is how the descendent view could handle this event. Notice that it must implement OutsideTouchInterceptor interface:
class OutsideTouchInterceptorView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr),
OutsideTouchDispatcherLayout.OutsideTouchInterceptor {
override fun interceptOutsideTouch(ev: MotionEvent) {
visibility = GONE
}
}
Then you have outside touch detection easily just by a child-parent relation:
<?xml version="1.0" encoding="utf-8"?>
<com.example.touchinterceptor.OutsideTouchDispatcherLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.touchinterceptor.OutsideTouchInterceptorView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#eee"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.example.touchinterceptor.OutsideTouchDispatcherLayout>
Here's a simple approach to get your work done:
Step 1: Create an ID for the outside container of your element for which you want to generate a click outside event.
In my case, it is a Linear Layout for which I've given id as 'outsideContainer'
Step 2: Set an onTouchListener for that outside container which will simply act as a click outside event for your inner elements!
outsideContainer.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// perform your intended action for click outside here
Toast.makeText(YourActivity.this, "Clicked outside!", Toast.LENGTH_SHORT).show();
return false;
}
}
);
Wrapper layout that notifies us when a click occurred outside a given view:
class OutsideClickConstraintLayout(context: Context, attrs: AttributeSet?) :
ConstraintLayout(context, attrs) {
private var viewOutsideClickListenerMap = mutableMapOf<View, () -> Unit>()
fun setOnOutsideClickListenerForView(view: View, listener: () -> Unit) {
viewOutsideClickListenerMap[view] = listener
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
viewOutsideClickListenerMap.forEach { (view, function) ->
if (isMotionEventOutsideView(view, ev)) function.invoke()
}
return super.onInterceptTouchEvent(ev)
}
private fun isMotionEventOutsideView(view: View, motionEvent: MotionEvent): Boolean {
val viewRectangle = Rect()
view.getGlobalVisibleRect(viewRectangle)
return !viewRectangle.contains(motionEvent.rawX.toInt(), motionEvent.rawY.toInt())
}
}
Usage:
....
outsideClickContainerView.setOnOutsideClickListenerForView(someView) {
// handle click outside someView
}
....
thank #ituki for idea
FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/search_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000"
android:clickable="true">
<LinearLayout
android:clickable="true" // not trigger
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="#FFF"
android:orientation="vertical"
android:padding="20dp">
...............
</LinearLayout>
</FrameLayout>
and java code
mContainer = (View) view.findViewById(R.id.search_container);
mContainer.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
Log.d("aaaaa", "outsite");
return true;
}
return false;
}
});
it's work when touch outside LinearLayout
I would like to have a linearlayout with a header section on top and a webview below. The header will be short and the webview may be longer and wider than the screen.
What is the best way to get horizontal and vertical scrolling? Is a ScrollView nested inside a HorizontalScrollView a good idea?
Is a ScrollView nested inside a HorizontalScrollView a good idea?
Yes, and no.
Yes, my understanding is that ScrollView and HorizontalScrollView can be nested.
No, AFAIK, neither ScrollView nor HorizontalScrollView work with WebView.
I suggest that you have your WebView fit on the screen.
there is another way. moddified HorizontalScrollView as a wrapper for ScrollView. normal HorizontalScrollView when catch touch events don't forward them to ScrollView and you only can scroll one way at time. here is solution:
package your.package;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.view.MotionEvent;
import android.content.Context;
import android.util.AttributeSet;
public class WScrollView extends HorizontalScrollView
{
public ScrollView sv;
public WScrollView(Context context)
{
super(context);
}
public WScrollView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public WScrollView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
#Override public boolean onTouchEvent(MotionEvent event)
{
boolean ret = super.onTouchEvent(event);
ret = ret | sv.onTouchEvent(event);
return ret;
}
#Override public boolean onInterceptTouchEvent(MotionEvent event)
{
boolean ret = super.onInterceptTouchEvent(event);
ret = ret | sv.onInterceptTouchEvent(event);
return ret;
}
}
using:
#Override public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/*BIDIRECTIONAL SCROLLVIEW*/
ScrollView sv = new ScrollView(this);
WScrollView hsv = new WScrollView(this);
hsv.sv = sv;
/*END OF BIDIRECTIONAL SCROLLVIEW*/
RelativeLayout rl = new RelativeLayout(this);
rl.setBackgroundColor(0xFF0000FF);
sv.addView(rl, new LayoutParams(500, 500));
hsv.addView(sv, new LayoutParams(WRAP_CONTENT, MATCH_PARENT /*or FILL_PARENT if API < 8*/));
setContentView(hsv);
}
Two years further down the line I think the open source community might have to your rescue:
2D Scroll View.
Edit: The Link doesn't work anymore but here is a link to an old version of the blogpost;
I searched really long to make this work and finally found this thread here. wasikuss' answer came quite close to the solution, but still it did not work properly. Here is how it works very well (at least for me (Android 2.3.7)). I hope, it works on any other Android version as well.
Create a class called VScrollView:
package your.package.name;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
public class VScrollView extends ScrollView {
public HorizontalScrollView sv;
public VScrollView(Context context) {
super(context);
}
public VScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
sv.dispatchTouchEvent(event);
return true;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
sv.onInterceptTouchEvent(event);
return true;
}
}
Your layout should look like:
<your.package.name.VScrollView
android:id="#+id/scrollVertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<HorizontalScrollView
android:id="#+id/scrollHorizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TableLayout
android:id="#+id/table"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:stretchColumns="*" >
</TableLayout>
</HorizontalScrollView>
</your.package.name.VScrollView>
In your activity, you should do something like:
hScroll = (HorizontalScrollView) findViewById(R.id.scrollHorizontal);
vScroll = (VScrollView) findViewById(R.id.scrollVertical);
vScroll.sv = hScroll;
... and that's how it works. At least for me.
There is an easy workaround:
In you activity get a reference to the outer scrollView (I'm going to assume a vertical scrollview) and a reference to the first child of that scroll view.
Scrollview scrollY = (ScrollView)findViewById(R.id.scrollY);
LinearLayout scrollYChild = (LinearLayout)findViewById(R.id.scrollYChild);
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
scrollYChild.dispatchTouchEvent(event);
scrollY.onTouchEvent(event);
return true;
}
One could argue that this solution is a bit hacky. But it has worked great for me in several applications!
Late to answer, but hopefully might be helpful to someone.
You can check out droid-uiscrollview. This is heavily based on #MrCeeJ's answer, but I seemed to have a lot of trouble getting the actual content to be rendered. Hence I pulled in the latest source from HorizontalScrollView & ScrollView to create droid-uiscrollview. There are a few todo's left which I haven't gotten around to finish, but it does suffice to get content to scroll both horizontally & vertically at the same time
I've try both wasikuss and user1684030 solutions and I had to adapt them because of one warning log: HorizontalScrollView: Invalid pointerId=-1 in onTouchEvent, and because I wasn't fan of this need of creating 2 scroll views.
So here is my class:
public class ScrollView2D extends ScrollView {
private HorizontalScrollView innerScrollView;
public ScrollView2D(Context context) {
super(context);
addInnerScrollView(context);
}
public ScrollView2D(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() == 1) {
View subView = getChildAt(0);
removeViewAt(0);
addInnerScrollView(getContext());
this.innerScrollView.addView(subView);
} else {
addInnerScrollView(getContext());
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = super.onTouchEvent(event);
handled |= this.innerScrollView.dispatchTouchEvent(event);
return handled;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
return true;
}
public void setContent(View content) {
if (content != null) {
this.innerScrollView.addView(content);
}
}
private void addInnerScrollView(Context context) {
this.innerScrollView = new HorizontalScrollView(context);
this.innerScrollView.setHorizontalScrollBarEnabled(false);
addView(this.innerScrollView);
}
}
And when using it in XML, you have nothing to do if the content of this scroll view is set in here. Otherwise, you just need to call the method setContent(View content) in order to let this ScrollView2D knows what is its content.
For instance:
// Get or create a ScrollView2D.
ScrollView2D scrollView2D = new ScrollView2D(getContext());
scrollView2D.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
addView(scrollView2D);
// Set the content of scrollView2D.
RelativeLayout testView = new RelativeLayout(getContext());
testView.setBackgroundColor(0xff0000ff);
testView.setLayoutParams(new ViewGroup.LayoutParams(2000, 2000));
scrollView2D.setContent(testView);
For a while I've been trying solutions from here, but the one that worked best still had one problem: It ate all events, none were making it through to elements within the scroller.
So I've got ... yet another answer, in Github and well-commented at least hopefully: https://github.com/Wilm0r/giggity/blob/master/app/src/main/java/net/gaast/giggity/NestedScroller.java
Like all solutions, it's a nested HorizontalScrollview (outer) + ScrollView (inner), with the outer receiving touch events from Android, and the inner receiving them only internally from the outer view.
Yet I'm relying on the ScrollViews to decide whether a touch event is interesting and until they accept it, do nothing so touches (i.e. taps to open links/etc) can still make it to child elements.
(Also the view supports pinch to zoom which I needed.)
In the outer scroller:
#Override
public boolean onInterceptTouchEvent(MotionEvent event)
{
if (super.onInterceptTouchEvent(event) || vscroll.onInterceptTouchEventInt(event)) {
onTouchEvent(event);
return true;
}
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
super.onTouchEvent(event);
/* Beware: One ugliness of passing on events like this is that normally a ScrollView will
do transformation of the event coordinates which we're not doing here, mostly because
things work well enough without doing that.
For events that we pass through to the child view, transformation *will* happen (because
we're completely ignoring those and let the (H)ScrollView do the transformation for us).
*/
vscroll.onTouchEventInt(event);
return true;
}
vscroll here is the "InnerScroller", subclassed from ScrollView, with a few changes to event handling: I've done some terrible things to ensure incoming touch events directly from Android are discarded, and instead it will only take them from the outer class - and only then pass those on to the superclass:
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
/* All touch events should come in via the outer horizontal scroller (using the Int
functions below). If Android tries to send them here directly, reject. */
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
/* It will still try to send them anyway if it can't find any interested child elements.
Reject it harder (but pretend that we took it). */
return true;
}
public boolean onInterceptTouchEventInt(MotionEvent event) {
return super.onInterceptTouchEvent(event);
}
public boolean onTouchEventInt(MotionEvent event) {
super.onTouchEvent(event);
}
I know you have accepted your answer but may be this could give you some idea.
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<HorizontalScrollView
android:layout_alignParentBottom="true"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<ImageView
android:src="#drawable/device_wall"
android:scaleType="center"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</HorizontalScrollView>
</RelativeLayout>
</LinearLayout>
</ScrollView>