I have been working on the launcher app for android similar to nova launcher. I have setup OnItemLongClickListener and OnDragListener. When i long click on an icon a popup is displayed with menu like "Remove", "Change Icon" etc. Following figure shows the progress of the app with popup open while Long click.
The problem is when the popup is opened the drag works but drop doesnot work. It seems that the i cannot log the x, y position once the popup is open. Also when the drop is performed the following message is shown in logcat.
I/ViewRootImpl: Reporting drop result: false
My code goes something like this in OnDragListener
public boolean onDrag(View v, DragEvent event) {
int dragEvent = event.getAction();
switch (dragEvent)
{
case DragEvent.ACTION_DRAG_LOCATION:
//Open popup here; note: its opened only once. popup.show();
//Log.i("Position x : ", Float.toString(event.getX())); log x or y
/*code to detect x any y change amount and close the popup
once user drags the icon little further and app knows that
user is trying to drag instead of opening the popup
and hence close the popup. popup.dismiss();
*/
// other case like ACTION_DROP etx goes after this
}
}
But it seems that after the popup is opened i cannot log x or y; also the code that determines if the action was intended for "drag" or "popup open", cannot be run.
So how do i solve this problem? I want to close the popup once the drag amount in any is sufficient to know that user wants to drag. And if not stop the drag and display the popup only.
Edit
I solved the problem with popup by using both OnTouchListner and OnDragListner. Following shows my code for OnDragListner.
//bottomAppDrawer is a GridView
bottomAppDrawer.setOnDragListener(new View.OnDragListener() {
#Override
public boolean onDrag(View v, DragEvent event) {
int dragEvent = event.getAction();
LinearLayout draggedItem = (LinearLayout) event.getLocalState(); //dragged LinearLayout
GridView targetItem = (GridView) v; /* How do i get this drop target as LinearLayout so that i can delete or swap data */
switch (dragEvent)
{
case DragEvent.ACTION_DRAG_LOCATION:
if(reset==false) {
dragPositionStart = event.getX();
reset= true;
}
if(Math.abs(dragPositionStart - event.getX())>=20) {
Log.i("Position close : ", Float.toString(dragPositionStart));
if(isPopupOpen) {
popupMenu.dismiss();
v.startDrag(data, dragShadow, itemView, 0);
Toast.makeText(mContext, "popup closed", Toast.LENGTH_SHORT).show();
isPopupOpen = false;
}
reset = false;
}
break;
case DragEvent.ACTION_DROP:
Toast.makeText(mContext, "drop" + Integer.toString(targetItem.getChildCount()), Toast.LENGTH_SHORT).show();
break;
}
return true;
}
});
Now the problem is I am getting the drop target "Gridview" as I am dropping LinearLayout in "Gridview". Also this "LinearLayout is child of the "Gridview". And i want the drop target to be another "LinearLayout" inside the same "GridView". So that i can swap data or reorder. As in figure below.
From what I understand there are two things that you want to do. 1)Reorder the Views after drag. and 2)Change the View types after the reorder.
For problem 1 since it is a grid view it sounds like we really just want to reorder the data in the adapter, and possibly change the data to cause it to display differently. But we need to figure out the position of the original item and the target destination position.
we can extend the GridView to do that:
public class DragAndDropGridView extends GridView {
public void handleMove(int x, int y, int originalPosition) {
Rect rect = new Rect();
PushbackAdapter adapter = (PushbackAdapter) getAdapter();
for (int visiblePosition = getFirstVisiblePosition(); visiblePosition <= getLastVisiblePosition(); visiblePosition++) {
// TODO verify that there are no edge cases not covered by this
View view = getChildAt(visiblePosition);
int left = view.getLeft();
int top = view.getTop();
getChildVisibleRect(view, rect, null);
rect.offsetTo(left, top);
if (rect.contains(x, y)) {
// yay the user tried drop the view at this location
// determine if they wanted to drop it here or after this side
int centerX = rect.centerX();
if (x <= centerX) {
// we want to drop it here
adapter.move(originalPosition, visiblePosition);
adapter.notifyDataSetInvalidated();
break;
} else {
// we want to drop it behind
adapter.move(originalPosition, visiblePosition + 1);
adapter.notifyDataSetInvalidated();
break;
}
}
}
}
}
That leaves us with calling the handleMoveMethod. We do that from within the ACTION_DROP method.
case DragEvent.ACTION_DROP:
handleMove((int)event.getX(), (int)event.getY(), getPositionForView(draggedItem));
Toast.makeText(mContext, "drop" + Integer.toString(targetItem.getChildCount()), Toast.LENGTH_SHORT).show();
break;
Lastly(problem 2) it sounds like you may want to change either the content of the object at the position or the type of view it is contained in. I'd suggest using the getItemViewType and getItemViewTypeCount methods if you need to have different types of views. E.g. something along the lines of the following:
private static class PushbackAdapter extends ArrayAdapter {
ArrayList<Object> mItems;
public void move(int originalPosition, int targetPosition){
// TODO verify that this move logic is correct
Object item = mItems.remove(originalPosition);
item.useLinearLayoutType(true);
mItems.add(targetPosition, item);
}
...
#Override
public int getItemViewType(int i) {
return mItems.get(i).isLeanearLayoutType()? 1 : 0;
}
There could be bugs with this so please test thoroughly
Find position of LinerLayout(which dragging) in Gridview using targetItem.pointToPosition(..).
Swipe LinerLayout using below code:
int i =targetItem.pointToPosition((int)event.getX(), (int)event.getY());
int j = Integer.parseInt(event.getClipData().getItemAt(0).getText().toString());
Collections.swap(targetItem, i, j);//swap Linerlayout
Log.i(TAG, "Swapped " + i+ " with " + j);
Code is not tested. I hope its help you. :)
Related
I have a custom view, assume it looks like this:
I would like for my custom view to respond to the onClicks, however the catch is that I would like it to respond to the clicks ONLY on the red portion/circle. Not the whole view.
Is it possible to make to make the text above and the grey portion not clickable?
Thank you.
In a custom view, you handle clicks by overriding the onTouchEvent method of android's View class. First check that the location the user has clicked is within the circle. Then normally you would give some feedback on the MotionEvent.ACTION_DOWN event to let user know they have clicked, such as highlight the circle. Then on MotionEvent.ACTION_UP you can call your onClick method.
#Override
public boolean onTouchEvent(MotionEvent event) {
boolean isTouchInCircle = checkTouchInCircle(event.getX(), event.getY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isTouchInCircle) {
circleColor = highlightColor;
invalidate();
}
break;
case MotionEvent.ACTION_MOVE:
if (isTouchInCircle) {
circleColor = highlightColor;
} else {
circleColor = normalColor
}
invalidate();
break;
case MotionEvent.ACTION_UP:
if (isTouchInCircle) {
onClickCircle();
}
break;
}
return true;
}
// Circle click zone approximated as a square
private boolean checkTouchInCircle(float touchX, float touchY) {
if (touchX < circleCenterX + circleRadius
&& touchX > circleCenterX - circleRadius
&& touchY < circleCenterY + circleRadius
&& touchY > circleCenterY - circleRadius) {
return true;
} else {
return false;
}
}
Unfortunately the answer Carson posted was not exactly what I was looking for as my example was only a simple one, with the reality sometimes it being a lot more difficult and checking the touch locations would be convoluted (imagine multiple views/shapes within the custom view being the click locations).
What I did was in the custom view do a find view by id on the elements of the custom view. Then I did setOnClickListener(this) on each element that I would like to be clicked able rather than on the whole view itself, so mCircle.setOnClickListener(this); and mInput.setOnClickListener(this);, then did implements View.OnClickListener for the custom view to handle the actions.
I currently have Views lined up horizontally in a ViewPager and can cycle through them with a PagerAdapter. Currently, to perform the action that I would like to do on swipe, I have to do a double-tap on the View's page. I could post code, but it's somewhat difficult to extract the necessary pieces...
What I would like is the ability to swipe vertically on these views, have them translate vertically with swipe and fade-out, and then perform an action when they reach a certain distance away from the edge of the device.
To get an idea of what I am thinking, in the Gallery app you can pinch an opened photo to zoom-out and open a horizontal filmstrip view. From there you can swipe up (or down) on a photo/video to delete it. For those who do not have the same Gallery app, it's exactly like closing applications on iOS.
I've tried scanning though the source code for the Gallery app, but no luck finding the correct Activity.
view.setOnTouchListener(new View.OnTouchListener(){
public boolean onTouch(View v, MotionEvent motion) {
float y = motion.getY();
/* NOTE: the following line might need to be in runOnUiThread() */
view.animate().alpha(1-Math.abs(y-height/2)/(height/2)).setDuration(50).start();
return true; //false if you want to pass this event on to other listeners
}
});
The explanation for using 1-Math.abs(y-height/2)/(height/2) is that I want alpha to be 1 when I am in the center, and alpha to be 0 when it is at the top or bottom. You have to determine yourself how you obtain the height value, or if you want to use a different method to calculate alpha. If you want to get the touch position relative to the screen instead of the position relative to the view, use getRawY().
Additionally, it may be useful for you to know that to see if the MotionEvent is a press, drag, or release event, use
motion.getAction() == with MotionEvent.ACTION_UP, MotionEvent.ACTION_MOVE, and MotionEvent.ACTION_DOWN, respectively.
I ended up getting this working more-or-less by cloning the well-written Android-SwipeToDismiss library and just replacing the ListView code with a ViewPager.
The finished product looked like this.
Check the below code, this may helpful to you:
public class MainActivity extends Activity implements View.OnTouchListener{
private RelativeLayout baseLayout;
private int previousFingerPosition = 0;
private int baseLayoutPosition = 0;
private int defaultViewHeight;
private boolean isClosing = false;
private boolean isScrollingUp = false;
private boolean isScrollingDown = false;
#Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_popup);
baseLayout = (RelativeLayout) findViewById(R.id.base_popup_layout);//this is the main layout
baseLayout.setOnTouchListener(this);
}
public boolean onTouch(View view, MotionEvent event) {
// Get finger position on screen
final int Y = (int) event.getRawY();
// Switch on motion event type
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// save default base layout height
defaultViewHeight = baseLayout.getHeight();
// Init finger and view position
previousFingerPosition = Y;
baseLayoutPosition = (int) baseLayout.getY();
break;
case MotionEvent.ACTION_UP:
// If user was doing a scroll up
if(isScrollingUp){
// Reset baselayout position
baseLayout.setY(0);
// We are not in scrolling up mode anymore
isScrollingUp = false;
}
// If user was doing a scroll down
if(isScrollingDown){
// Reset baselayout position
baseLayout.setY(0);
// Reset base layout size
baseLayout.getLayoutParams().height = defaultViewHeight;
baseLayout.requestLayout();
// We are not in scrolling down mode anymore
isScrollingDown = false;
}
break;
case MotionEvent.ACTION_MOVE:
if(!isClosing){
int currentYPosition = (int) baseLayout.getY();
// If we scroll up
if(previousFingerPosition >Y){
// First time android rise an event for "up" move
if(!isScrollingUp){
isScrollingUp = true;
}
// Has user scroll down before -> view is smaller than it's default size -> resize it instead of change it position
if(baseLayout.getHeight()<defaultViewHeight){
baseLayout.getLayoutParams().height = baseLayout.getHeight() - (Y - previousFingerPosition);
baseLayout.requestLayout();
}
else {
// Has user scroll enough to "auto close" popup ?
if ((baseLayoutPosition - currentYPosition) > defaultViewHeight / 4) {
closeUpAndDismissDialog(currentYPosition);
return true;
}
//
}
baseLayout.setY(baseLayout.getY() + (Y - previousFingerPosition));
}
// If we scroll down
else{
// First time android rise an event for "down" move
if(!isScrollingDown){
isScrollingDown = true;
}
// Has user scroll enough to "auto close" popup ?
if (Math.abs(baseLayoutPosition - currentYPosition) > defaultViewHeight / 2)
{
closeDownAndDismissDialog(currentYPosition);
return true;
}
// Change base layout size and position (must change position because view anchor is top left corner)
baseLayout.setY(baseLayout.getY() + (Y - previousFingerPosition));
baseLayout.getLayoutParams().height = baseLayout.getHeight() - (Y - previousFingerPosition);
baseLayout.requestLayout();
}
// Update position
previousFingerPosition = Y;
}
break;
}
return true;
}
}
For animation use the below methods:
public void closeUpAndDismissDialog(int currentPosition){
isClosing = true;
ObjectAnimator positionAnimator = ObjectAnimator.ofFloat(baseLayout, "y", currentPosition, -baseLayout.getHeight());
positionAnimator.setDuration(300);
positionAnimator.addListener(new Animator.AnimatorListener()
{
. . .
#Override
public void onAnimationEnd(Animator animator)
{
finish();
}
. . .
});
positionAnimator.start();
}
public void closeDownAndDismissDialog(int currentPosition){
isClosing = true;
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int screenHeight = size.y;
ObjectAnimator positionAnimator = ObjectAnimator.ofFloat(baseLayout, "y", currentPosition, screenHeight+baseLayout.getHeight());
positionAnimator.setDuration(300);
positionAnimator.addListener(new Animator.AnimatorListener()
{
. . .
#Override
public void onAnimationEnd(Animator animator)
{
finish();
}
. . .
});
positionAnimator.start();
}
So I have a layout as such:
I am using a Listview right now and each row has an Imageview and 2 Textviews.
I want to be able to do 3 things on this page:
Clicking on a row (or just clicking on the Imageview in that row works for me too) brings me to another Fragment.
You can drag the image in each Listview row. It uses DragShadowBuilder and therefore can detect if you drop it into that darker gray zone in the bottom.
You can scroll up and down in the Listview to get to the other items that are overflowing right now.
As you can imagine, these three cases are difficult to capture because its hard to differentiate between the three (bc of overlaps in functionality).
I would prefer not to use onItemLongClickListener to do the drag and drop bc users usually don't think to hold their fingers down a long time to start drag and drop.
Any suggestions on how to implement this to capture all three use cases? Actually, it can be thought of as 2 use cases because if I dropped an image back into its original container, it could count as a click for me. The most complex part is to make that somehow work with scrolling up and down the Listview...
Thanks for your help in advance!
p.s. This entire view is rendered in a Fragment and clicking on a view or successfully dropping one into the gray area opens up a separate fragment.
/**
* Call this from a drag source view.
*/
#SuppressLint("NewApi")
public boolean onTouchEvent(MotionEvent ev) {
if (!mDragging) {
return false;
}
final int action = ev.getAction();
final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels);
final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels);
switch (action) {
case MotionEvent.ACTION_DOWN:
// Remember where the motion event started
mMotionDownX = screenX;
mMotionDownY = screenY;
break;
case MotionEvent.ACTION_MOVE:
//Set background color of remove comment box layout
if(((int)ev.getY() <= 50))
ImageEditingActivityNew.rl_remove.setBackgroundColor(Color.RED);
else
ImageEditingActivityNew.rl_remove.setBackgroundColor(Color.TRANSPARENT);
// Update the drag view. Don't use the clamped pos here so the dragging looks
// like it goes off screen a little, intead of bumping up against the edge.
mDragView.move((int)ev.getRawX(), (int)ev.getRawY());
// Drop on someone?
final int[] coordinates = mCoordinatesTemp;
DropTarget dropTarget = findDropTarget(screenX, screenY, coordinates);
if (dropTarget != null) {
if (mLastDropTarget == dropTarget) {
dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1],(int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
} else {
if (mLastDropTarget != null) {
mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1],(int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
}
dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1],
(int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
}
} else {
if (mLastDropTarget != null) {
mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1],
(int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
}
}
mLastDropTarget = dropTarget;
/* The original Launcher activity supports a delete region and scrolling.
It is not needed in this example.
// Scroll, maybe, but not if we're in the delete region.
boolean inDeleteRegion = false;
if (mDeleteRegion != null) {
inDeleteRegion = mDeleteRegion.contains(screenX, screenY);
}
//Log.d(TAG, "inDeleteRegion=" + inDeleteRegion + " screenX=" + screenX
// + " mScrollZone=" + mScrollZone);
if (!inDeleteRegion && screenX < mScrollZone) {
if (mScrollState == SCROLL_OUTSIDE_ZONE) {
mScrollState = SCROLL_WAITING_IN_ZONE;
mScrollRunnable.setDirection(SCROLL_LEFT);
mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
}
} else if (!inDeleteRegion && screenX > scrollView.getWidth() - mScrollZone) {
if (mScrollState == SCROLL_OUTSIDE_ZONE) {
mScrollState = SCROLL_WAITING_IN_ZONE;
mScrollRunnable.setDirection(SCROLL_RIGHT);
mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
}
} else {
if (mScrollState == SCROLL_WAITING_IN_ZONE) {
mScrollState = SCROLL_OUTSIDE_ZONE;
mScrollRunnable.setDirection(SCROLL_RIGHT);
mHandler.removeCallbacks(mScrollRunnable);
}
}
*/
break;
case MotionEvent.ACTION_UP:
//When touch up then remove comment box
if(((int)ev.getY() <= 50)) {
ImageEditingActivity.rl_imageEdit_comment.removeAllViews();
ImageEditingActivity.rl_imageEdit_comment.invalidate();
//ImageEditingActivity.mDragLayer.removeView(ImageEditingActivity.rl_imageEdit_comment);
ImageEditingActivity.addComment = true;
ImageEditingActivity.changeIconBackground("Remove");
ImageEditingActivity.editParam.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
}
if (mDragging) {
drop(screenX, screenY);
}
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
cancelDrag();
}
return true;
}
Instead of long press and hold you can use fling/swipe gesture. You can override onFling method of SimpleonGestureListner and detect fling based on the threshold.
Your requirements conflicts. I would do the next thing in your case :
Your row can be drag only with long pressed (then I would use the shadow drag)
also I would do that the view of the drop only appears when long press occurs.
override the onclick method of the ImageView that way you can click it and then call your fragment to expand
Now because you didn't override any onTouchListeners or onClick on the row you can scroll freely as long as you don't click a picture.
Currently I'm using ListView and it's working fine. But I have text in a ListView that is like a paragraph and I just want to show those 2 lines of text and make the rest of the text scrollable, but I'm having an issue that if I make the TextView scrollable inside of the ListView, then the TextView get the focus of its parent (ListView) and won't let it be scrolled.
So can I achieve this scrollable TextView functionality that won't disturb the scrolling property of the ListView?
Thank you.
I was able to do this in the following way:
Into the getView method of the ListAdapter obtain the TextView object of the line, and write
textView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
v.getParent().getParent().requestDisallowInterceptTouchEvent(true);
return false ;
}
});
when you will touch the TextView, take control of the scrolling
What you are trying to achieve really is impossible. How can the OS know that you are trying to scroll the list vs the list row paragraph? You would essentially need to scroll to the bottom of the list row paragraph before the actual list itself could scroll. This is confusing to the user, and not common UX.
I would suggest you look into ExpandableListView. It allows you to have collapsed versions of each row, in your case just 2 lines of text for each list row. When the user taps on the row, it could expand to the full paragraph form, and the list would be scrollable the whole time. There are plenty of tutorials you should be able to find online.
Nothing is Impossible yet way to do things are may b difficult. Directly this thing can`t be achieved but indirectly yes it can be achieved, and yes i achieved.
how did i achieve is a bit complex but yes will share that how did i achieve.
In a ListView when i click on Textview i block the Touch mode of the listView so that their toch method don't intercept each other, and that can be done by using requestDisallowInterceptTouchEvent(true);
this block the TouchListener of the parent (ListView).
Now when click on TextView i allow its touch listener and also setMovementMethod()
but for Movement i made a custom class and Class is Following
public class myScrollMethod extends ScrollingMovementMethod {
#Override
public void onTakeFocus(TextView widget, Spannable text, int dir) {
// TODO Auto-generated method stub
super.onTakeFocus(widget, text, dir);
}
#Override
public boolean onKeyDown(TextView widget, Spannable buffer,
int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_DOWN:
for (int i = 0, scrollAmount = getScrollAmount(widget); i < scrollAmount; i++) {
down(widget, buffer);
}
return true;
case KeyEvent.KEYCODE_DPAD_UP:
for (int i = 0, scrollAmount = getScrollAmount(widget); i < scrollAmount; i++) {
up(widget, buffer);
}
return true;
default:
return super.onKeyDown(widget, buffer, keyCode, event);
}
}
private int getScrollAmount(TextView widget) {
final int visibleLineCount = (int) ((1f * widget.getHeight()) / widget
.getLineHeight());
int scrollAmount = visibleLineCount - 1;
if (scrollAmount < 1) {
scrollAmount = 1;
}
return scrollAmount;
}
}
After that when i click on parent i enable the TouchIntercepter of the parent set true and that get hold on its parent and start scrolling.
By this way i have successfully achieved this requirement
I have a set of images. I am displaying it on the screen as a grid.
So based upon selecting each image i want to do actions.
That I have done. But one more requirement is that when we move our hand through these images then also I have to perform the same actions. That is, I will have to track on which image I have touched right now and perform the actions. How will I implement it? Does anyone have any idea? Please respond..
Try onTouch() event From View.OnTouchListener. This is called when the user performs an action qualified as a touch event, including a press, a release, or any movement gesture on the screen (within the bounds of the item).
Hope this helps.
You can set listeners to your images, i.e.
imgView.setOnTouchListener(...)
imgView.setOnFocusChangeListener(...)
or
imgView.setOnClickListener()
and then perform the action in these listeners.
If you use setOnFocusChangeListener, then you should be able to handle all cases regardless in which way you selected the image, via touch or trackball.
I got it done.
In the below code colorGV is my grid name.
Add listener to it.
colorGV.setOnTouchListener(new OnTouchClick(context));
And define onTouchClick as:
private class OnTouchClick implements OnTouchListener {
Context context;
OnTouchClick(Context context) {
this.context = context;
}
public boolean onTouch(View v, MotionEvent event) {
try {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
int x = (int) event.getX();
int y = (int) event.getY();
int position = -1;
for (int i = 0; i < colorGV.getChildCount(); i++) {
Rect ButtonRect = new Rect();
colorGV.getChildAt(i).getHitRect(ButtonRect);
if (ButtonRect.contains(x, y)) {
position = i;
break;
}
}
if (position >= 0 && prevPos != position) {
System.out.println("Position changed :" + position);
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}