I have a LineChart contained within a ScrollView. When the chart is long enough for it to be necessary for a user to scroll to see it in its entirety, the drag features become unresponsive. Drag gestures only register when I hold my finger down for a short period of time in the chart bounds and then drag.
I have tried using the requestDisallowInterceptTouchEvent method that should prevent the chart's parents from intercepting the touch events (but this doesn't solve anything). I've also tried directly passing MotionEvents registered by the ScrollView straight to the chart/translating drag gestures to translateY calls but this doesn't do what I thought it would.
Note: Zooming continues to work perfectly.
Also, this is not an issue when the graph fits in the original window or when it's placed in any view that is not a ScrollView. I have considered getting rid of the ScrollView but it's a pretty necessary feature in my project.
Any ideas on why this could be happening would be appreciated!
Edit: the LineChart has a fixed height
This question is kind of old but I run into the same problem recently and to solve it, I did the following.
Added a transparent view over the graph on the xml.
Set setOnTouchListener on the view
ex.
clickInterceptorGraph.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return onTouchActionHandler(v, event);
}
});
On the MotionEvent Action Down, I'm setting requestDisallowInterceptTouchEvent to true and returning false so the event won't be consumed, so while the user is pressing on the transparent image over the graph requestDisallowInterceptTouchEvent will be true and it will disable the Touch Event on the scrollview which will disable the scroll and when you release the touch the scrollview will work normally again.
ex.
protected boolean onTouchActionHandler(View v, MotionEvent event){
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// Disallow RecyclerView to intercept touch events.
scrollView.requestDisallowInterceptTouchEvent(true);
Log.e(TAG, "onTouchActionHandler: ACTION_DOWN" );
// Disable touch on transparent view
return false;
default:
return true;
}
}
*** You could do everything on the setOntouchListener but in my case I need to reuse the requestDisallowInterceptTouchEvent.
Similar approach as Ruan_Lopes mentioned, you just don't need a transparent overlay view. You can also use chart gesture listener to intercept touches the same way.
lineChart.onChartGestureListener = object : OnChartGestureListener {
override fun onChartGestureEnd(
me: MotionEvent?,
lastPerformedGesture: ChartTouchListener.ChartGesture?
) = Unit
override fun onChartFling(
me1: MotionEvent?,
me2: MotionEvent?,
velocityX: Float,
velocityY: Float
) = Unit
override fun onChartSingleTapped(me: MotionEvent?) = Unit
override fun onChartGestureStart(
e: MotionEvent?,
lastPerformedGesture: ChartTouchListener.ChartGesture?
) = recyclerView.requestDisallowInterceptTouchEvent(true)
override fun onChartScale(me: MotionEvent?, scaleX: Float, scaleY: Float) = Unit
override fun onChartLongPressed(me: MotionEvent?) = Unit
override fun onChartDoubleTapped(me: MotionEvent?) = Unit
override fun onChartTranslate(me: MotionEvent?, dX: Float, dY: Float) = Unit
}
Related
How can I get notified at the moment when the selection menu appears for TextView?
See the screen recording below. If you select some text, context menu (copy/share/select all) appears. If you begin to drag the blue drag handle, the context menu disappears, and once you release the handle, the menu appears again. So, basically, the menu appears only when selection is done, not whilst you are still selecting the text.
I want to get notified when selection is done (i.e., the same time as the menu appears). I thought that onPrepareActionMode would be called when the selection is done and the menu appears, but after testing with the code below, it seemed that onPrepareActionMode is continuously called whilst I am dragging the handle, even when the selection menu is not visible. Also, it often got called twice for a single dragging. So onPrepareActionMode does not seem to be then answer. Then what is?
object: ActionMode.Callback{
#SuppressLint("ResourceType")
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean
{
Log.d("test", "onCreateActionMode");
return true;
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean
{
Log.d("test", "onPrepareActionMode");
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean
{
Log.d("test", "onActionItemClicked");
return false;
}
override fun onDestroyActionMode(mode: ActionMode?)
{
Log.d("test", "onDestroyActionMode");
}
};
No answer? I thought that this would be a common requirement to do something automatically when text is selected. Anyway, I spent a lot of time trying to find a way and it all failed. But the hint came from a random thing: I have noticed that whenever the text selection is done and the menu appears,
W/androidtc: TextClassifier called on main thread
is printed in the Logcat. That is how I found that there is TextClassifier in TextView. So, I tried the following code
val tc = object:TextClassifier{
override fun classifyText(request: TextClassification.Request): TextClassification
{
Log.d("test", "classifyText")
return super.classifyText(request)
}
}
text2.setTextClassifier(tc);
, and as I suspected, classifyText was called when the selection is done, not during I was dragging the selection handle. This is also called when the selection is closed (tap elsewhere), but I guess I can probably filter that out by checking if the selected text length is 0. I will use this workaround until someone who knows better posts a more elegant and correct solution.
PS: The code above works on Android 10/11/12, but caused a runtime exception on Android 8.1. I guess that it is because there are no default implementations for methods on Android 8.1.
For Android 8.1, I have tried the following and it worked. If you do not care about classifier itself, I guess you do not have to pass the default classifier and use the commented-out dummy return values.
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textClassificationManager = getSystemService(Context.TEXT_CLASSIFICATION_SERVICE) as TextClassificationManager;
val defaultOne = textClassificationManager.textClassifier;
val txt = findViewById<TextView>(R.id.textview1);
txt.setTextClassifier(MyTextClassfier(defaultOne));
}
inner class MyTextClassfier(private val fallback:TextClassifier) : TextClassifier by fallback
{
override fun suggestSelection(
text: CharSequence,
selectionStartIndex: Int,
selectionEndIndex: Int,
defaultLocales: LocaleList?
): TextSelection
{
return fallback.suggestSelection(text, selectionStartIndex, selectionEndIndex, defaultLocales);
//return TextSelection.Builder(selectionStartIndex, request.getEndIndex()).build();
}
override fun classifyText(
text: CharSequence,
startIndex: Int,
endIndex: Int,
defaultLocales: LocaleList?
): TextClassification
{
//Selection ended. User has lifted his finger.
return fallback.classifyText(text, startIndex, endIndex, defaultLocales);
//return TextClassification.Builder().build();
}
}
I found a lot of examples for swipe detection on a recyclerview item. But I try to detect right/left swipe on the whole view.
There are also examples like this Android: How to handle right to left swipe gestures
on how to detect swipe on views like layout container. But attaching this touch listener to the recyclerview is not working.
So how can I detect left / right swipe on a whole recyclerview?
Edit for SmartSwipe Lib:
I have tried to add the https://github.com/luckybilly/SmartSwipe Lib because the effect of swipe looks really nice.
But if I try to wrap it to the recyclerview I see no data only the swipe effect:
SmartSwipe.wrap(binding.recyclerView).addConsumer(BezierBackConsumer())
.enableHorizontal()
.addListener(object : SimpleSwipeListener() {
override fun onSwipeOpened(wrapper: SmartSwipeWrapper, consumer: SwipeConsumer, direction: Int) {
}
})
Attaching it to the constraint layout container has no effect at all.
Edit 2:
Attaching an onTouchListener to the view doesn't work for the recyclerview:
view.setOnTouchListener(object : OnHorizontalSwipeListener(requireContext()) {
override fun onRightSwipe() {
println("Swipe right")
}
override fun onLeftSwipe() {
println("Swipe left")
}
})
Attaching it to the Recyclerview leads to an exception:
binding.recyclerView.setOnTouchListener.....
E/MessageQueue-JNI: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter e1
at ui.OnHorizontalSwipeListener$GestureListener.onFling(Unknown Source:2)
Edit 3: Seems to work if I add null check to the listener class like this:
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
var result = false
try {
if (e1 != null && e2 != null) {
val diffY = e2.y - e1.y
val diffX = e2.x - e1.x
....
But starts working only after some scrolling. trying to swipe on a new initialized fragment doesn't work.
I'm trying to implement the android library SelectionTracker which allows to select items in a recyclerView.
Everything works fine except that when I click outside of an
item (which is in a grid layout), the all selection is cleared.
I actually have found the code which calls the clearSelection(). It's on the line 78 of the class TouchInputHandler.
It then calls the line 64 of ItemDetailsLookup which returns false because the touch event didn't occurred on an item.
I was wondering if anyone have found a workaround to prevent this behavior, because I didn't found any option in the documentation.
It's a gridLayout so it is quite "normal" to have space between items and I don't want my users to clear the selection because they have touch the side of an item.
This is my solution, based on that if we have predefined ItemDetail that will be used as "this is not the view you can select".
First, inside your ItemDetailsLookup instead of returning null you can pass single item with distinguish data that will make sure there is no name/position collision with any other data you can have
class AppItemDetailsLookup(private val rv: RecyclerView) : ItemDetailsLookup<String>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
val view = rv.findChildViewUnder(e.x, e.y) ?: return EMPTY_ITEM
return (rv.getChildViewHolder(view) as AppItemViewHolder).getItemDetails()
}
object EMPTY_ITEM : ItemDetails<String>() {
override fun getSelectionKey(): String? = "empty_item_selection_key_that_should_be_unique_somehow_that_is_why_i_made_it_so_long"
override fun getPosition(): Int = Integer.MAX_VALUE
}
}
And then when you are creating SelectionTracker with builder, instead of using standard predicate (default is SelectionPredicates.createSelectAnything()) you make your own that will notify that this EMPTY_ITEM cannot be selected
.withSelectionPredicate(object : SelectionTracker.SelectionPredicate<String>() {
override fun canSelectMultiple(): Boolean = true
override fun canSetStateForKey(key: String, nextState: Boolean): Boolean =
key != AppItemDetailsLookup.EMPTY_ITEM.selectionKey
override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean =
position != AppItemDetailsLookup.EMPTY_ITEM.position
})
I tested it with LinearLayoutManger, the selection was deselecting all items once i clicked outside any of them (my items did not had spacing decoration, but there were so few of them that i was seeing empty under last item)
I have my own custom view that extends RecyclerView and I use it to render a document with multiple pages. Each item in the adapter is a page.
I want to add the ability to zoom and pan the entire document (not each page individually).
I have tried following the Android guide here but I still can't get both zoom and pan to work properly.
My current approach is this:
Have a GestureDetector to detect pan gestures
Have a ScaleGestureDetector to detect pinch gestures
When each of these detectors register events, save the information regarding the current scale factor and the pan, call invalidate() and modify the Canvas in the drawChild method appropriately.
It is the last step that I'm unsure how to implement properly.
I'm using this data class to keep track of the view's current pan and zoom:
private data class PanAndZoom(
var scaleFactor: Float,
var focusX: Float,
var focusY: Float,
var panX: Float ,
var panY: Float
)
// field in my custom view initialised like this:
private val panAndZoom = PanAndZoom(1f, 0f, 0f, 0f, 0f)
and here is how I update the values when gestures are received:
private val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
panAndZoom.scaleFactor *= detector.scaleFactor
panAndZoom.focusX = detector.focusX
panAndZoom.focusY = detector.focusY
invalidate()
return true
}
}
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
panAndZoom.panX -= distanceX
panAndZoom.panY -= distanceY
invalidate()
return true
}
}
I override two methods in my custom view, one to pass touch events to my gesture detectors and one to draw child view:
override fun onTouchEvent(e: MotionEvent): Boolean {
gestureDetector.onTouchEvent(e)
scaleGestureDetector.onTouchEvent(e)
return super.onTouchEvent(e)
}
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
val save = canvas.save()
canvas.scale(panAndZoom.scaleFactor, panAndZoom.scaleFactor, panAndZoom.focusX, panAndZoom.focusY)
canvas.translate(panAndZoom.panX, panAndZoom.panY)
val result = super.drawChild(canvas, child, drawingTime)
canvas.restoreToCount(save)
return result
}
However this doesn't give the expected result, namely:
I am able to overscroll outside the RV altogether (I realise I'm not restricting my pan values, but how do I do that)?
Pan seems to be going very fast when zoomed in (how do I scale my pan values so that they respect the scale factor)?
If I am on page 2 of the document, and I zoom in, then pan to page 3, the layout manager doesn't realise that the view was actually scrolled (meaning, calling findFirstVisibleItem() will return the second page when it isn't actually visible.
Most likely related to the point above: I can pan past a page, and because the layout manager doesn't realise that another view is now displayed it doesn't render it (because it still thinks that view is hidden from view and so it optimises by not rendering it).
I'm working on a notepad app, and I'm having trouble with the context menus. The app has files/folders, so I have two separate context menus (one for files, one for folders). I got the menu to show for the right items by overriding the long-click listener, but it doesn't appear directly over the item; it appears like a dialog:
Here's the code for the long-click listener; I really feel like the problem is here; if not, I can post a little more:
override fun onItemLongClick(adapterView: AdapterView<*>?, p1: View?, pos: Int, p3: Long): Boolean {
val p : Int = adapterView?.getItemAtPosition(pos) as Int
currentFile = fileContents[p]
fileList.showContextMenu()
return true
}
You should provide the coordinates if you want to show the menu over the item.
instead of fileList.showContextMenu(), use p1.showContextMenu(p1.pivotX,p1.pivotY)
override fun onItemLongClick(adapterView: AdapterView<*>?, p1: View?, pos: Int, p3: Long): Boolean {
val p : Int = adapterView?.getItemAtPosition(pos) as Int
currentFile = fileContents[p]
p1.showContextMenu(p1.pivotX,p1.pivotY)
return true
}
Ok, so I figured it out; the credit partially goes to the answer above and to this answer: OnClickListener - x,y location of event?
Basically, you implement the onTouch listener to get the last x and y coordinates and then pass them in to the showContextMenu method. Here's the code I used to implement this:
override fun onTouch(p0: View?, event: MotionEvent?): Boolean {
if (event!!.actionMasked == MotionEvent.ACTION_DOWN) {
lastX = event.x
lastY = event.y
}
return false
}
I think you can get the result which you want using PopupMenu.
Here's the documentation: https://developer.android.com/reference/android/widget/PopupMenu
Here's the demo: https://play.google.com/store/apps/details?id=com.alphae.rishi.towatch
I have inflated PopupMenu whenever the user clicks on the three-dot menu.