I have several layouts; each containing a TextView. If the user drags a TextView unto another TextView, I'm going to execute the method swapFamily() but it fires several times, depending on the number of families.
FamilyItemLayout is just a custom LinearLayout with a String attribute attached to it and with add'tl getters and setter for that attribute.
However, my issue is that,
private class onDropFamily implements View.OnDragListener {
#Override
public boolean onDrag(View view, DragEvent event) {
TextView txtDragged = (TextView) event.getLocalState();
TextView txtTarget = (TextView) view;
String familyDragged = ((FamilyItemLayout) txtDragged.getParent()).getFamily();
String familyTarget = ((FamilyItemLayout) txtTarget.getParent()).getFamily();
switch (event.getAction()) {
// Signals the start of a drag and drop operation.
case DragEvent.ACTION_DRAG_STARTED:
break;
// Signals to a View that the drag point has entered the bounding box of the View.
case DragEvent.ACTION_DRAG_ENTERED:
view.setBackgroundResource(R.mipmap.pill_sun);
break;
// Signals that the user has moved the drag shadow outside the bounding box of the View.
case DragEvent.ACTION_DRAG_EXITED:
view.setBackgroundResource(R.mipmap.pill_sky);
break;
// Signals to a View that the user has released the drag shadow, and the drag point is within the bounding box of the View.
case DragEvent.ACTION_DROP:
break;
// Signals to a View that the drag and drop operation has concluded.
case DragEvent.ACTION_DRAG_ENDED:
// Check if drag event was successful
if (dropEventHandled(event)) {
fixedPlan.swapFamily(familyDragged, familyTarget);
}
txtDragged.setVisibility(View.VISIBLE);
txtTarget.setBackgroundResource(R.mipmap.pill_sky);
break;
}
return true;
}
private boolean dropEventHandled(DragEvent dragEvent) {
return dragEvent.getResult();
}
}
I believe you should put those code in ACTION_DROP instead of ACTION_DRAG_ENDED
http://developer.android.com/reference/android/view/DragEvent.html
All views that received an ACTION_DRAG_STARTED event will receive the
ACTION_DRAG_ENDED event even if they are not currently visible when
the drag ends.
Also remember to return correct value for ACTION_DROP
The View should return true from its onDragEvent(DragEvent) handler or
OnDragListener.onDrag() listener if it accepted the drop, and false if
it ignored the drop.
Related
I have the following component structure:
- CustomComponent extends ConstraintLayout
- ImageView
- ImageView
- ViewPager2
- CustomFragment extends ListFragment
- ListView
- ListItem extends ConstraintLayout
- ImageView
- ListItem extends ConstraintLayout
- ImageView
- ListItem extends ConstraintLayout
- ImageView
- etc...
Visually it looks like this (I left out ListView for brevity):
The ImageView in ListItem will act as a drag button for dragging the ListItem, so that the items can be reordered. But ultimately, I also want to be able to drag a ListItem outside the ViewPager2 and drop it on one of the ImageViews in the CustomComponent. I think I will implements this by temporarily hiding the original and then "clone" and attach the ListItem as a child to CustomComponent.
However, before even getting there, the problem currently is that MotionEvent.ACTION_MOVE on the ImageView is constrained to its parent ListItem as illustrated by the red arrows. As soon as the touch gesture goes outside ListItem, I stop receiving MotionEvent.ACTION_MOVE events.
This is the listener I currently have on the drag button:
dragButton.setOnTouchListener((view, event) -> {
System.out.println("ListItem to drag is: " + view.getParent());
switch(event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
dragging = false;
break;
case MotionEvent.ACTION_MOVE:
if(!dragging) {
dragging = true;
System.out.println('drag start');
}
else {
System.out.println('drag');
}
break;
case MotionEvent.ACTION_UP:
if(dragging) {
dragging = false;
System.out.println('drag end');
}
break;
}
return true;
});
While I imagine that it will keep being detected for a bit if I were to move the ListItem along with the MotionEvent coordinates, I doubt it would be reliable, as it might lag behind and thereby slip from under the touch gesture. Moreover: with the above mentioned "cloning", this is not even relevant anymore, since its the "clone" that will be moving — not the original.
So, is there a way to detect MotionEvent.ACTION_MOVE outside ListItem when dragging the ImageView, as illustrated by the blue arrows?
I know about onInterceptTouchEvent(), which I could override in CustomComponent, but that gives me no information about which View the event is meant for, whereas in the above OnTouchListener I can easily determine which ListItem to drag by view.getParent().
I'm open to hearing completely other ideas of how to approach this as well, of course.
Solved it by using emandt's suggestion. My personal Solution added below.
I'm using Android Studio for this.
I searched for solutions but couldn't find anything resembling this.
I want to know on which ImageView an UP action occurs while starting the DOWN action on a different ImageView (to eventually be able to drag one image over the other and make it snap to the same position by getting the position of the image I dragged over).
My example has two ImageViews with the id imageView (left) and imageView2(right).
In my example I'm not dragging anything yet, I just want to touch the left image, see "Action was down" in the log and lift the finger over the right image showing "Action was up2".
I don't know if this is easily possible.
As far as I can tell from testing, the MotionEvent.ACTION_UP only fires for an ImageView when you also pressed down on it beforehand. So when I release on top of imageView2 it only shows "Action was up" from the left image.
I wondered if it was possible by playing with return false, since the return value tells if an ActionEvent is consumed so I thought if the UP event of imageView returns false, maybe it does trigger the UP event of imageView2 but no. (Either complete misunderstanding on my part or it doesn't recognise UP on the second because it didn't start with a DOWN and MotionEvents probably always have to start with a DOWN).
public class MainActivity extends Activity {
ImageView imageView;
ImageView imageView2;
String DEBUG_TAG = "action";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageView);
imageView2 = findViewById(R.id.imageView2);
imageView.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
//int action = MotionEventCompat.getActionMasked(event);
int action = event.getActionMasked();
switch(action) {
case (MotionEvent.ACTION_DOWN) :
Log.d(DEBUG_TAG,"Action was DOWN"+v.toString());
return true;
case (MotionEvent.ACTION_MOVE) :
//Log.d(DEBUG_TAG,"Action was MOVE");
return true;
case (MotionEvent.ACTION_UP) :
Log.d(DEBUG_TAG,"Action was UP"+v.toString());
return false;
default :
//return true;
}
return true;
}
});
imageView2.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
//int action = MotionEventCompat.getActionMasked(event);
int action = event.getActionMasked();
switch(action) {
case (MotionEvent.ACTION_DOWN) :
Log.d(DEBUG_TAG,"Action was DOWN2"+v.toString());
return true;
case (MotionEvent.ACTION_MOVE) :
//Log.d(DEBUG_TAG,"Action was MOVE");
return true;
case (MotionEvent.ACTION_UP) :
Log.d(DEBUG_TAG,"Action was UP2"+v.toString());
return true;
default :
//return true;
}
return true;
}
});
}
}
If there is no simple way to do this, I'm thinking about solving this mathematically, but maybe some of you can help.
So my question is, is there a way to recognise an UP action on a second ImageView while currently being in a MotionEvent of another ImageView?
SOLUTION (see emandt's answer)
I ditched the second OnClickListener because I realised that the 2nd image doesn't need any, I just need its position.
Added this method:
#Nullable
private View getDroppedView(View droppedView, int x, int y, List<View> arrayOfPossibilities) {
Rect cVisibleBoundsRect = new Rect();
for (View cView : arrayOfPossibilities) {
//if currently iterated view doesn't have values for getGlobalVisibleRect, skip the .contains part
//ignore the item which is your current active item (which would potentially be dropped)
//getGlobalVisibleRect sets cVisibleBoundsRect immediately to the Rect given as parameter
if (!cView.getGlobalVisibleRect(cVisibleBoundsRect)||(cView.equals(droppedView))) continue;
if (cVisibleBoundsRect.contains(x, y)) {
Log.d(DEBUG_TAG,"Found something");
//THIS "cView" IS THE VIEW WHERE YOU RELEASED THE FINGER
return cView;
}
}
Log.d(DEBUG_TAG,"Found nothing");
return null;
}
And added this in onUP:
case (MotionEvent.ACTION_UP) :
View dropTarget;
Log.d(DEBUG_TAG,"Action was UP"+v.toString());
dropTarget = getDroppedView(v, (int)event.getRawX(), (int)event.getRawY(), listOfViews);
if (dropTarget != null){
v.setX(dropTarget.getX());
v.setY(dropTarget.getY());
}
I think you want to know which is the View where you release the finger from the screen, am I right?
To do this you can use the same "View.OnTouchListener()" for all of your Views and in the ACTION_UP you have to call a new method similar to this (pseudo-code):
....
case (MotionEvent.ACTION_UP) :
View[] cArrayOfPossibileViews = new View[]{ findViewById(IMAGE_1), findViewById(IMAGE2) }
getDroppedView(v, event.getRawX(), event.getRawY(), cArrayOfPossibileViews);
break;
}
....
#Nullable
private View getDroppedView(View view, int x, int y, View[] arrayOfPossibilities) {
Rect cVisibleBoundsRect = new Rect();
for (View cView : arrayOfPossibilities) {
if (!cView.getGlobalVisibleRect(cVisibleBoundsRect)) continue;
if (cVisibleBoundsRect.contains(x, y)) {
//THIS "cView" IS THE VIEW WHERE YOU RELEASED THE FINGER
return cView;
}
}
return null;
}
This method get View bounds and compare them avains X and Y of your Touch Event. If X and Y are contained inside a View bounds it means that View is the one you need.
This is my problem:
https://youtu.be/k-N5uthYhYw
and this is my onBindViewHolder() method.
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.specName.setText(specList.get(position).getSpecName());
// Assign a tag number to later identify what radio-button
holder.specRadioBtn.setTag(new Integer(position));
/* Event listenr for longClick - we prob. won't use it, but it's here just in case */
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
Toast.makeText(context, "Long press", Toast.LENGTH_SHORT).show();
return false;
}
});
/* Little hack to select its Radio Button when a specific row is tapped */
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Turn rowSelectedFlag to true since the user selected this row
rowSelectedFlag = true;
// When the user taps on a row select that row's radio button
holder.specRadioBtn.setChecked(true);
// I'm not sure why, but locally the interface needs to be started by pointing it
// to where it should drive the data (to send the params)
tempInterface = new AdminUserSpecialty();
// Call the interface to send the data (row spec-name and id) back to AdminUserSpecialty
tempInterface.activateSpecSelect(specList.get(position).getSpecName().toString(),
specList.get(position).getSpecId().toString(), rowSelectedFlag);
int clickedPos = ((Integer) holder.specRadioBtn.getTag());
// Check if the radio button is already selected
if (holder.specRadioBtn.isChecked()) {
if (lastCheckedBtn != null) {
// Don't deselect if user taps on the same row several times
if (lastCheckedBtn == holder.specRadioBtn) {
// do nothing
}
// Otherwise do deselect the previously selected radio button
else {
lastCheckedBtn.setChecked(false);
}
}
lastCheckedBtn = holder.specRadioBtn;
lastCheckedPos = clickedPos;
}
// If radio is not checked set the lastCheckedBtn to null (reset counter)
else {
lastCheckedBtn = null;
}
}
});
/* ----------------------------------------------------------------------*/
}
I can't seem to preserve my radio-button selection on RecyclerView scroll. On scroll the selection becomes erratic and random. I understand that one of RecyclerView's features is to recycle rows as they leave the screen, but what do I need to do to keep my selection? Thanks much.
I know that this was answered already but if some of you are still looking for an easier answer and your application does not rely on the RecyclerView view recycling feature much (for example if you have a fixed size list of items...) you can always set your recycler view cache view size. That way it would not recycler your views hence it would not recycler the views and you will avoid copy selected values to another views...
yourRecyclerView..setItemViewCacheSize(yourItemList.size());
Save the checked / unchecked status of the radio button (you should use checkbox instead if you want to allow the user to select multiple items) to your model (i.e. your items in the list should have a field for this) when the onClick event happens. When you bind the ViewHolder, make sure you set checkbox's value to whatever you saved in your model.
It's happen because of the Recycling mechanism
(PS: its the same for the ListView or RecyclerView).
To fix that:
1) Add a booelan variable to your model to save the state of the RadioButton
2) Update your RadioButton state in onBindViewHolder() method from this boolean in the model.
3) Add setOnCheckedChangeListener() to your RadioButton to listen to his state (checked/unchecked) and to update the boolean in your model when the state changes.
I have an ImageView inside of a view pager with an ActionBar at the top. I would like to be able to single tap to hide the action bar, and I would also like to be able to pinch zoom and pan on each ImageView.
To implement the single tap to hide the action bar I have a simple OnClickListener that hides it.
To implement the pinch zoom and pan on each ImageView I am using the PhotoView Library Project.
I am having issues because only one touch event listener can be associated with an ImageView, and the implementing the PhotoView Library project overwrites my OnClickListener to hide the ActionBar with,
parent.requestDisallowInterceptTouchEvent(true);
I am not sure how to go about getting both implemented at the same time. It seems like the only solution is to create my own Pinch Zoom ImageView in order to control touch events myself.
Found out that the PhotoView library actually allows me to set onViewTap for the PhotoViewAttacher object which is exactly what I wanted.
To create the PhotoViewAttacher in the current Fragment/Activity have it implement PhotoViewAttacher.OnViewTapListener, create the attacher,
PhotoViewAttacher mAttacher = new PhotoViewAttacher(imageView);
mAttacher.setOnViewTapListener(this);
and add the following function,
public void onViewTap(View view, float x, float y) {
// your code here
}
Source
You'll have to override the PhotoView library itself. If you look at the source code, the PhotoViewAttacher class is the one that handles the onTouch events.
You'll have to add the special funcionality you're looking for at this part of the code (specially, the ACTION_DOWN) event:
#Override
public final boolean onTouch(View v, MotionEvent ev) {
boolean handled = false;
if (mZoomEnabled && hasDrawable((ImageView) v)) {
ViewParent parent = v.getParent();
switch (ev.getAction()) {
case ACTION_DOWN:
// First, disable the Parent from intercepting the touch
// event
if (null != parent)
parent.requestDisallowInterceptTouchEvent(true);
else
Log.i(LOG_TAG, "onTouch getParent() returned null");
// If we're flinging, and the user presses down, cancel
// fling
cancelFling();
break;
case ACTION_CANCEL:
case ACTION_UP:
// If the user has zoomed less than min scale, zoom back
// to min scale
if (getScale() < mMinScale) {
RectF rect = getDisplayRect();
if (null != rect) {
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
break;
}
// Check to see if the user double tapped
if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
if (!handled && null != parent) {
parent.requestDisallowInterceptTouchEvent(false);
}
// Finally, try the Scale/Drag detector
if (null != mScaleDragDetector
&& mScaleDragDetector.onTouchEvent(ev)) {
handled = true;
}
}
return handled;
}
I have got a textView or small Icon and I want to drag it only vertical.
What I tried:
Create an own ShadowBuilder but make him invisible. So others views get called over the OnDragListener but the ShadowView is not visible.
Instead I display an own view which is moved like here:
How to make the TextView drag in LinearLayout smooth, in android? with the different, only in vertical direction. The Problem is, the onTouchListener is only called twice if dragging is activated. So no moving could be done.
That solution does not work for me I think.
Drag an image only horizontally or vertically in android?
Maybe any idea? Thanks for reading.
try out my solution here, but only change the top&bottom margins instead.
findViewById(R.id.btn_submit).setOnTouchListener(new View.OnTouchListener()
{
int prevX,prevY;
#Override
public boolean onTouch(final View v,final MotionEvent event)
{
final LinearLayout.LayoutParams par=(LinearLayout.LayoutParams)v.getLayoutParams();
switch(event.getAction())
{
case MotionEvent.ACTION_MOVE:
{
par.topMargin+=(int)event.getRawY()-prevY;
prevY=(int)event.getRawY();
prevX=(int)event.getRawX();
v.setLayoutParams(par);
return true;
}
case MotionEvent.ACTION_UP:
{
par.topMargin+=(int)event.getRawY()-prevY;
v.setLayoutParams(par);
return true;
}
case MotionEvent.ACTION_DOWN:
{
prevX=(int)event.getRawX();
prevY=(int)event.getRawY();
par.bottomMargin=-2*v.getHeight();
v.setLayoutParams(par);
return true;
}
}
return false;
}
});