Related
I have a View Pager (VP) which contains a Horizontal Scroll View (HSV). If the HSV reaches one of its edges or is not able to scroll at all, on a new swipe in the blocked direction VP should take over scrolling to the next page. I hesitated to ask this question because I found similar ones like these:
Can I use Horizontal Scrollview Inside a Viewpager in Android?
or
horizontalscrollview inside viewpager
But the solution did not work for me. 'v instanceof HorizontalScrollView' gets true but viewPager does not scroll
Any other ideas how to achieve the desired behaviour?
public class MyViewPager extends ViewPager {
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// Update 1
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
//return super.onInterceptTouchEvent(ev);
}
/**
* https://stackoverflow.com/questions/22781496/can-i-use-horizontal-scrollview-inside-a-viewpager-in-android
*/
#Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof HorizontalScrollView) {
return true;
}
return super.canScroll(v, checkV, dx, x, y);
}
}
child view: view_pager_page.xml:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<include layout="#layout/" />
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
</FrameLayout>
</RelativeLayout>
parent view: view_pager.xml
<android.support.design.widget.CoordinatorLayout>
...
<LinearLayout>
<packagepath.MyViewPager
android:layout_width="match_parent"
android:layout_height="wrap_content">
</packagepath.MyViewPager>
</LinearLayout>
...
<android.support.design.widget.CoordinatorLayout>
Update 1: When overriding 'onInterceptTouchEvent' and let it always return true VP scrolls, but HSV doesn't. I think this must return true only if HSV reaches edges right? How can I figure out in this method if it is the case?
Update 2: I reconstructed the touch event mechanism of android hoping to get some insight of how to intercept the motion event flow. E.g. in HSV I can simply return false to let VP consume this and all subsequent motion events. Unfortunately I need two motion events of type MotionEvent.MOVE to decide if HSV or VP should scroll when reaching an edge (if HSV has reached right edge, a right swipe scrolls HSV back and a left swipe scrolls to next page of VP). But if I skip the MotionEvent.DOWN action neither HSV or VP starts scrolling... so hard to solve. Any ideas?
Touchevent Mechanism in Android
(Warning: Graphic is not complete and will contain mistakes, everyone is invited to correct it :-))
Update 3: Finally I got it working. Understanding the Touchevent mechanism helped a lot and also the first comment of ZeroOne. I will post my solution when I have time for it.
I solved this with a custom HorizontalScrollView. The key is to override the onTouchEvent() method and return false if you are at the left edge and swiping right, or the right edge and swiping left. Returning false means this view didn't consume the touch event and this event can bubble back up the view hierarchy to be handled by the ViewPager.
public class HorizontalScrollViewForViewPager extends HorizontalScrollView {
float old_x, old_y;
public HorizontalScrollViewForViewPager(Context context) {
super(context);
}
public HorizontalScrollViewForViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HorizontalScrollViewForViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
//Start of touch. Could be tap, could be drag.
old_x = ev.getX();
old_y = ev.getY();
} else if (action == MotionEvent.ACTION_MOVE) {
//Drag movement underway
float deltaX = ev.getX() - old_x;
float deltaY = ev.getY() - old_y;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
//scrolling more left/right than up/down
if (deltaX > 0 && getScrollX() == 0) {
//dragging left, at left edge of HorizontalScrollView. Don't handle this touch event, let it bubble up to ViewPager
return false;
} else {
//dragging right. Use first child to determine width of content inside HorizontalScrollView
int childWidth = getChildAt(0).getWidth();
if (deltaX < 0 && (this.getScrollX() + this.getWidth()) >= childWidth) {
//swiping left, and at right edge of HorizontalScrollView. Don't handle this touch event, let it bubble up to ViewPager
return false;
}
}
}
}
return super.onTouchEvent(ev);
}
}
1.Extend ViewPager Class:
public class ViewPagerContainingHorizontalScrollView extends ViewPager {
private Float x_old;
private boolean bDoIntercept = false;
private boolean bHsvRightEdge = false;
private boolean bHsvLeftEdge = true;
public ViewPagerContainingHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
private float calculateDistanceSwipe(MotionEvent ev){
float distance = 0;
if (x_old == null) {
x_old = ev.getX();
} else {
distance = ev.getX() - x_old;
x_old = null;
}
return distance;
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
mDoIntercept = false;
if(ev.getAction() == MotionEvent.ACTION_MOVE) {
float distance = calculateDistanceSwipe(ev);
if (distance < 0) {//scrolling left direction
if (bHsvRightEdge) { //HSV right edge
bDoIntercept = true;
//When scrolling slow VP may not switch page.
//Then HSV snaps back into old position.
//To allow HSV to scroll into non blocked direction set following to false.
bHsvRightEdge = false;
}
bHsvLeftEdge = false;//scrolling left means left edge not reached
} else if (distance > 0) {//scrolling right direction
if (bHsvLeftEdge) { //HSV left edge
bDoIntercept = true;
//When scrolling slow VP may not switch page.
//Then HSV snaps back into old position.
//To allow HSV to scroll into non blocked direction set following to false.
bHsvLeftEdge = false;
}
bHsvRightEdge = false;//scrolling right means right edge not reached
}
}
return super.dispatchTouchEvent(ev);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(bDoIntercept){
return true;
}else{
return super.onInterceptTouchEvent(ev);
}
}
#Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof HorizontalScrollView) {
HorizontalScrollView hsv = (HorizontalScrollView) v;
int max_scrollX = hsv.getChildAt(0).getWidth() - hsv.getWidth();
int min_scrollX = 0;
int current_scroll_x = hsv.getScrollX();
if (current_scroll_x == max_scrollX) { //HSV right edge
bHsvRightEdge = true;
}
if (current_scroll_x == min_scrollX) { //HSV left edge
bHsvLeftEdge = true;
}
return true;
}
return super.canScroll(v, checkV, dx, x, y);
}
}
Use this custom VP in XML.
Enjoy nested HSV scrolling in VP :-)
Touch Event Mechanism Overview for this specific case
I have a TextView in a Layout. It's so simple.
I put a OnClickListener in the layout and some part of the TextView is set to be ClickableSpan.
I want the ClickableSpan to do something in the onClick function when it's clicked and
when the other part of the TextView is clicked, it has to do something in the onClick functions of the OnClickListener of the layout.
Here's my code.
RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
l.setOnClickListener(new OnClickListener(){
#Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "whole layout", Toast.LENGTH_SHORT).show();
}
});
TextView textView = (TextView)findViewById(R.id.t1);
textView.setMovementMethod(LinkMovementMethod.getInstance());
SpannableString spannableString = new SpannableString(textView.getText().toString());
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
Toast.makeText(MainActivity.this, "just word", Toast.LENGTH_SHORT).show();
}
};
spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(spannableString);
I've also run into this problem, and thanks to the source code #KMDev mentioned, I've came up with a much cleaner approach.
First, since I'm only having a TextView that is to be made partially clickable, in fact I don't need most of the functionalities LinkMovementMethod (and its super class ScrollingMovementMethod) which adds ability to handle key press, scrolling, etc.
Instead, create a custom MovementMethod that uses the OnTouch() code from LinkMovementMethod:
ClickableMovementMethod.java
package com.example.yourapplication;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.BaseMovementMethod;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.MotionEvent;
import android.widget.TextView;
/**
* A movement method that traverses links in the text buffer and fires clicks. Unlike
* {#link LinkMovementMethod}, this will not consume touch events outside {#link ClickableSpan}s.
*/
public class ClickableMovementMethod extends BaseMovementMethod {
private static ClickableMovementMethod sInstance;
public static ClickableMovementMethod getInstance() {
if (sInstance == null) {
sInstance = new ClickableMovementMethod();
}
return sInstance;
}
#Override
public boolean canSelectArbitrarily() {
return false;
}
#Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length > 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else {
Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return false;
}
#Override
public void initialize(TextView widget, Spannable text) {
Selection.removeSelection(text);
}
}
Then using this ClickableMovementMethod, touch event will not be consumed by movement method any more. However, TextView.setMovementMethod() which calls TextView.fixFocusableAndClickableSettings() will set clickable, long-clickable and focusable to true which will make View.onTouchEvent() consume the touch event. To fix for this, simply reset the three attributes.
So the final utility method, to accompany the ClickableMovementMethod, is here:
public static void setTextViewLinkClickable(TextView textView) {
textView.setMovementMethod(ClickableMovementMethod.getInstance());
// Reset for TextView.fixFocusableAndClickableSettings(). We don't want View.onTouchEvent()
// to consume touch events.
textView.setClickable(false);
textView.setLongClickable(false);
}
This works like a charm for me.
Click events on ClickableSpans are fired, and click outside them are passed throught to parent layout listener.
Note that if your are making your TextView selectable, I haven't tested for that case, and maybe you need to dig into the source yourself :P
The first answer to your question is that you aren't setting a click listener on your TextView which is consuming click events as user2558882 points out. After you set a click listener on your TextView, you'll see that areas outside the ClickableSpans's touch area will work as expected. However, you'll then find that when you click on one of your ClickableSpans, the TextView's onClick callback will be fired as well. That leads us to a difficult issue if having both fire is an issue for you. user2558882's reply can't guarantee that your ClickableSpan's onClick callback will be fired before your TextView's. Here's some solutions from a similar thread that are better implemented and an explanation from the source. The accepted answer that thread should work on most devices, but the comments for that answer mention certain devices having issues. It looks like some devices with custom carrier/manufacturer UIs are to blame, but that's speculation.
So why can't you guarantee onClick callback order? If you take a look at the source for TextView (Android 4.3), you'll notice that in the onTouchEvent method, boolean superResult = super.onTouchEvent(event); (super is View) is called before handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); which is the call to your movement method which then calls your ClickableSpan's onClick. Taking a look at super's (View) onTouchEvent(..), you'll notice:
// Use a Runnable and post this rather than
// performClick directly. This lets other visual
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) { // <---- In the case that this won't post,
performClick(); // it'll fallback to calling it directly
}
performClick() calls the click listener set, which in this case is our TextView's click listener. What this means, is that you won't know in what order your onClick callbacks are going to fire. What you DO know, is that your ClickableSpan and TextView click listeners WILL be called. The solution on the thread I mentioned previously, helps ensure the order so you can use flags.
If ensuring compatibility with a lot of devices is a priority, you are best served by taking a second look at your layout to see if you can avoid being stuck in this situation. There are usually lots of layout options to skirt cases like this.
Edit for comment answer:
When your TextView executes onTouchEvent, it calls your LinkMovementMethod's onTouchEvent so that it can handle calls to your various ClickableSpan's onClick methods. Your LinkMovementMethod does the following in its onTouchEvent:
#Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}
You'll notice that it takes the MotionEvent, gets the action (ACTION_UP: lifting finger, ACTION_DOWN: pressing down finger), the x and y coordinates of where the touch originated and then finds which line number and offset (position in the text) the touch hit. Finally, if there are ClickableSpans that encompass that point, they are retrieved and their onClick methods are called. Since we want to pass on any touches to your parent layout, you could either call your layouts onTouchEvent if you want it to do everything it does when touched, or you could call it's click listener if that implements your needed functionality. Here's where do to that:
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
// Your call to your layout's onTouchEvent or it's
//onClick listener depending on your needs
}
}
So to review, you'll create a new class that extends LinkMovementMethod, override it's onTouchEvent method, copy and paste this source with your calls in the correct position where I commented, ensure you're setting your TextView's movement method to this new subclass and you should be set.
Edited again for possible side effect avoidance
Take a look at ScrollingMovementMethod's source (LinkMovementMethod's parent) and you'll see that it's a delegate method which calls a static method return Touch.onTouchEvent(widget, buffer, event); This means that you can just add that as your last line in the method and avoid calling super's (LinkMovementMethod's) onTouchEvent implementation which would duplicate what you're pasting in and other events can fall through as expected.
Here is an easy solution and it worked for me
You can achieve this using a work around in getSelectionStart() and getSelectionEnd() functions of the Textview class,
tv.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
ClassroomLog.log(TAG, "Textview Click listener ");
if (tv.getSelectionStart() == -1 && tv.getSelectionEnd() == -1) {
//This condition will satisfy only when it is not an autolinked text
//Fired only when you touch the part of the text that is not hyperlinked
}
}
});
Declare a global boolean variable:
boolean wordClicked = false;
Declare and initialize l as final:
final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
Add an OnClickListener to textView:
textView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if (!wordClicked) {
// Let the click be handled by `l's` OnClickListener
l.performClick();
}
}
});
Change span to:
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
wordClicked = true;
Toast.makeText(Trial.this, "just word", Toast.LENGTH_SHORT).show();
// A 100 millisecond delay to let the click event propagate to `textView's`
// OnClickListener and to let the check `if (!wordClicked)` fail
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
wordClicked = false;
}
}, 100L);
}
};
Edit:
Keeping user KMDev's answer in view, the following code will meet your specifications. We create two spans: one for the specified length: spannableString.setSpan(.., 0, 5, ..); and the other with the remainder: spannableString.setSpan(.., 6, spannableString.legth(), ..);. The second ClickableSpan(span2) performs a click on the RelativeLayout. Moreover, by overriding updateDrawState(TextPaint), we are able to give the second span a non-distinctive (non-styled) look. Whereas, first span has a link color and is underlined.
final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
l.setOnClickListener(new OnClickListener(){
#Override
public void onClick(View v) {
Toast.makeText(Trial.this, "whole layout", Toast.LENGTH_SHORT).show();
}
});
TextView textView = (TextView)findViewById(R.id.t1);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT);
SpannableString spannableString = new SpannableString(textView.getText().toString());
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
Toast.makeText(Trial.this, "just word", Toast.LENGTH_SHORT).show();
}
};
spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
ClickableSpan span2 = new ClickableSpan() {
#Override
public void onClick(View widget) {
l.performClick();
}
#Override
public void updateDrawState(TextPaint tp) {
tp.bgColor = getResources().getColor(android.R.color.transparent);
tp.setUnderlineText(false);
}
};
spannableString.setSpan(span2, 6, spannableString.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(spannableString);
Special thanks to user KMDev for noticing the issues with my original answer. There's no need for performing a (faulty) check using boolean variable(s), and setting an OnclickListener for the TextView is not required.
The easiest and fastest way to implement ClickableSpan is:
new SmartClickableSpan
.Builder(this)
.regularText("I agree to all ")
.clickableText(new ClickableOptions().setText("Terms of Use").setOnClick(new
ClickableSpan() {
#Override
public void onClick(#NonNull View view) {
// Your Code..
}
}))
.into(myTextView);
Adding regular Clickable Spans in Android requires calculating sizes for each clickable text, and when it comes to adding a lot of clickable and regular words or sentences in a TextView is becomes a mess.. By using SmartClickableSpan, you'll be able to add whatever amount of clickable words or sentences without any worries of calculating length of each text on every update on it.
SmartClickableSpan Github:
https://github.com/HseinNd98/SmartClickableSpan
I have an EditText view in my Android app. I need "inner links" in it, this means that I need some buttons or span inside EditText and with onClick to this button I can do some actions (not redirect to web page).
I realized this buttons with ClickableSpan() like this
linkWord = "my link";
link = new SpannableString(linkWord);
cs = new ClickableSpan(){
private String w = linkWord;
#Override
public void onClick(View widget) {
wrd.setText(w);
}
};
link.setSpan(cs, 0, linkWord.length(), 0);
et.append(link);
For make this span clickable I used
et.setMovementMethod(LinkMovementMethod.getInstance());
"Inner links" works fine, but after using et.setMovementMethod() copy and paste items are disable on OnLongClick menu. And this is a problem, because I need "links" in EditText and copy text from this view in the same time.
I have idea to set in listener OnLongClickListener something like removeMovementMethod() for temporary disable "links" function and use menu with copy/paste and after coping text switch on setMovementMethod() method again. But I don't know how to realize this.
Can you help me? You may be there are some another ways...
Thank you!
I don't think that having the user switch between link and copy mode will win you a usability prize. My solution allows you to select text and open the links at the same time. To achieve this I simply extend ArrowKeyMovementMethod, which allows to select text, and add the onTouchEvent() method from the LinkMovementMethod which handles the clicking/touching of links. There's but one line of code that needs to be changed, which is the one removing the selection from the TextView when no link could be found at the coordinates the screen was touched.
Here's the complete class:
public class MyMovementMethod extends ArrowKeyMovementMethod {
private static MyMovementMethod sInstance;
public static MovementMethod getInstance() {
if (sInstance == null) {
sInstance = new MyMovementMethod ();
}
return sInstance;
}
#Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
}
else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]));
}
return true;
}
/*else {
that's the line we need to remove
Selection.removeSelection(buffer);
}*/
}
return super.onTouchEvent(widget, buffer, event);
}
}
Doing this is pretty safe even if the documentation states:
This interface [MovementMethod] is intended for use by the framework;
it should not be implemented directly by applications.
http://developer.android.com/reference/android/text/method/MovementMethod.html
The code above extends a documented class rather than implement the interface. All it does is adding a check to see if a link was tapped and otherwise uses the super class methods.
I solved this problem and may be this will be interesting for someone...
For clickable links inside EditText I used
et.setMovementMethod(LinkMovementMethod.getInstance());
in this case in longClick menu there are not copy/paste items.
For activate them I need back to normal EditText state, I can do it with:
et.setMovementMethod(ArrowKeyMovementMethod.getInstance());
After this method links will not work but appear normal longClick menu.
Therefore I added new item to the context menu and switched between this two options:
#Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if(et.getSelectionStart() == -1){ // in case of setMovementMethod(LinkMovementMethod.getInstance())
menu.add(0, 1, 0, "Enable copy");
}
else{
menu.add(0, 2, 0, "Enable links");
}
}
#Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case 1:
et.setMovementMethod(ArrowKeyMovementMethod.getInstance());
et.setSelection(0, 0);
//re-register EditText for context menu:
unregisterForContextMenu(et);
registerForContextMenu(et);
break;
case 2:
et.setMovementMethod(LinkMovementMethod.getInstance());
break;
}
return true;
}
Also I registered EditText for context menu:
registerForContextMenu(et);
Have a hope that this will help someone!
The problem is that if i Linkify the textView the underliyng ScrollView don't listen the sweep Gestures I've setted.Is there a way to have Linkify without messing with the underliyng view's gestures?
I tried to override ontouchEvent and return false to ACTION_MOVE but the scrollview's gesture needs the ACTION_DOWN and ACTION_UP event to function. Is there a way to achieve that?
Linkify applies to a movementMethod to the textView LinkMovementMethod. That movement method thought it implements a scrolling vertically method it overrides any other scrolling method the parent has. Although touchEvent can be dispached to the parent, the specific parent ScrollView needed the whole sequence ACTION_DOWN , ACTION_MOVE, ACTION_UP to perform (sweep detection).
So the solution to my problem is after Linkify to remove the textView's scrolling method and handle the LinkMovementMethod link detection action in onTouchEvent of the textView.
#override
public boolean onTouchEvent(MotionEvent event) {
TextView widget = (TextView) this;
Object text = widget.getText();
if (text instanceof Spanned) {
Spannable buffer = (Spannable) text;
int action = event.getAction();
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off,
ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
}
}
}
return false;
}
This way i have the Link_Click detection (performed only with the user touches the link and not the whole textview) and i don't have the whole LinkMovementMethod.
#weakwire and #Ridicully answers are correct. I just created a small gist that you can re-use in your project.
This is the link: https://gist.github.com/amilcar-andrade/e4b76840da1dc92febfc
There is a small bad thing that
TextView::setText(...) method utilizing autoLink flag,
if (mAutoLinkMask != 0) {
Spannable s2;
if (type == BufferType.EDITABLE || text instanceof Spannable) {
s2 = (Spannable) text;
} else {
s2 = mSpannableFactory.newSpannable(text);
}
if (Linkify.addLinks(s2, mAutoLinkMask)) {
text = s2;
type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
/*
* We must go ahead and set the text before changing the
* movement method, because setMovementMethod() may call
* setText() again to try to upgrade the buffer type.
*/
mText = text;
// Do not change the movement method for text that support text selection as it
// would prevent an arbitrary cursor displacement.
if (mLinksClickable && !textCanBeSelected()) {
setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
So I spent a time to understand,
why i'm disabling links in ListView item,
but it obtains a link sometimes!
You need to set that flag in needed value
and then call a setText(...)
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!