I'm working on an Android game.
I'm wondering if drag and drop is simply the wrong approach to tackle this. The effect I'm looking for is to have a button that when the user long-presses, will initiate a drag and drop effect with the drop-shadow being a target.
This custom drag shadow would persist until the user releases the target shadow. I have the custom shadow working and am responding to drag events. What I am not sure about is how to make this whole thing work without actually moving the button they are initiating the drag from.
Is it as simple as somehow not passing the originating View (button) to the shadow builder?
EDIT
Added definition of touch listener I'm using to initiate the drag.
private final class MyTouchListener implements OnTouchListener {
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
ClipData data = ClipData.newPlainText("", "");
//DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDrag(data, new TargetDragShadowBuilder(view), view, 0);
view.setVisibility(View.INVISIBLE);
return true;
} else {
return false;
}
}
}
I am setting the visibility here, though to be honest I'm frankly trying to follow the tutorial here:
http://www.vogella.com/tutorials/AndroidDragAndDrop/article.html
Perhaps this is the wrong approach. I'm basically looking to do the following steps:
1) Detect a drag event starting with a button
2) Without altering the button in any way, I want a custom drop shadow of a picture I specify (with a transparency) to appear under the user's finger as they drag across the screen.
Eventually I need to figure out how to make the shadow flash, or linger for a few seconds before disappearing and also to get the location where the user released their finger. It looks like from the docs I can get that from ACTION_DRAG_LOCATION...?
There is an official API Guide on how to implement Drag and Drop in Android. Unfortunately solution proposed by bonnyz does not comply to this guideline - adding a temporary view to indicate a drag operation is somewhat a heavy-weight solution.
Answering your question:
Is it as simple as somehow not passing the originating View (button) to the shadow builder?
The documentation says:
View.DragShadowBuilder(View)
This constructor accepts any of your application's View objects. The constructor stores the View object in the View.DragShadowBuilder object, so during the callback you can access it as you construct your drag shadow. It doesn't have to be associated with the View (if any) that the user selected to start the drag operation.
So, the view you pass there is not used to represent the shadow. The shadow is what you draw on Canvas within onDrawShadow() method implementation.
Basically, the shadow is just a graphics that follows the finger while being dropped. It is not a view and does not have to look like any view in the activity. It is arbitrary graphics that provides an idea to the user what is being dragged and dropped.
Here's my suggestion:
1) When user longPress on the button, get the absolute coordinates of the button:
int coord[] = new int[2];
button.getLocationOnScreen(coord);
2) Create the "ShadowView" which need to be shown under the finger while the user drags (as you like)
3) Attach the ShadowView to the Window (leaving others layout inalterated):
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
View shadowView = /..shadow view.../
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = coord[0]; //add offset if needed
params.y = coord[1];
windowManager.addView(shadowView, params);
4) Update params using the user dragging position.
params.x = // drag X - halfsize of the view's width
params.y = // drag Y - halfsize of the view's height
windowManager.updateViewLayout(shadowView, params);
5) When the user stop dragging, remove the ShadowView from the Window:
windowManager.removeViewImmediate(shadowView);
Issue was that I was setting the view invisible (facepalm) in response to the touch listener. Furthermore my shadow was passing the view to super() making the view come along for the ride. Combined effect was the appearance of the view being dragged away and lost forever. Fixed by removing visibility call and removing the view from super class in customer dropshadowbuilder.
Related
I'm programmatically adding a PopupView which contains an EditText field to my Activity, which is vertically and horizontally centered on the screen. When the keyboard opens, I want the PopupView to move up, so it is still centered on the visible screen/activity part.
My code:
EditText e = new EditText(super.getContext());
PopupWindow popup = new PopupWindow(e, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
popup.setBackgroundDrawable(new ColorDrawable(Color.WHITE));
popup.setOutsideTouchable(true);
popup.setFocusable(true);
popup.showAtLocation(this, Gravity.CENTER, 0, 0);
I've tried many things with windowSoftInputMode for the Activity; I've tried to setSoftInputMode(mode) on the popup - but none of my approaches have worked. Neither my layout nor the Popup change their position when the keyboard opens. (I only want my popup but not the layout to change, though, just pointing it out).
Also the code is placed in a LinearLayout class, in case you are wondering why I'm using this as a View.
Easier to get Android to do all the heavy lifting for you.
Just use:
popup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
So after much research, I finally found a way to accomplish that.
The code for creating the PopupWindow and making it being displayed in the vertical and horizontal center stays the same:
PopupWindow popup = new PopupWindow(
popupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
/** ... **/
popup.showAtLocation(this, Gravity.CENTER, 0, 0);
Then the only thing you need is a Listener for the Keyboard (or more general: For Window Height changes). This was actually easier than I thought - and it didn't require any special access like an Activity-object or similar. Even in my independent View-class which only knows the Context (which I didn't want to cast), I was able to accomplish that. Everything you need is only one View-object which has already been added to the layout.
// You can call this method on any view that is added to the layout:
final View root = this.getRootView();
root.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
Rect r = new Rect();
root.getWindowVisibleDisplayFrame(r);
// Calculate the difference between the original height and the new height
int heightDiff = r.height() - root.getHeight();
// Now update the Popup's position
// The first value is the x-axis, which stays the same.
// Second value is the y-axis. We still want it centered, so move it up by 50% of the height
// change
// The third and the fourth values are default values to keep the width/height
popup.update(0, heightDiff / 2, -1, -1);
}
});
For reference:
Listening to window height changes
Only downside:
This solution may not work when you add a PopupView while the Keyboard is already opened. But in my case, this isn't an expectable scenario anyway.
It's easy to add layout transitions with this attribute:
android:animateLayoutChanges="true"
However, the animation you get does not create a pleasing user experience. When elements are added to the layout (I'm using a simple vertical LinearLayout) or change from gone to visible there's a 2-stage process that I think is rather annoying. First, room is prepared for the new element (everything else is pushed down). Then when there's enough room, the new view fades into existence. Likewise, when a view is removed or changes from visible to gone, first it fades out, then the room claimed by it gradually shrinks to zero.
I would really like a way to change the animation to what I really think is the natural way to do it: When adding a view its height gradually changes from zero to its full size, so that first you see just the top, without ever changing the alpha. When removing a view its height gradually changes to its full size to zero, so that near the end of the animation you see just the top, without ever changing the alpha.
How can I accomplish this in Android? (Note: the user can tap on several buttons together and cause several elements to appear / disappear in quick succession, before the animation for the other views ended - or even make something appear while it's still appearing).
Another question that this is perhaps not the place to ask: why isn't this the default?
(And if it's possible, can a slightly different behavior be specified in which first just the bottom of the view appears, rather than the top, like the new view slides down from under the one above it?)
You have to write your own animator and set it.
Code:
final ViewGroup profileParent = (ViewGroup) view.findViewById(R.id.profileParent);
LayoutTransition transition = new LayoutTransition();
Animator appearingAnimation = ObjectAnimator.ofFloat(null, "translationY", 600/*profileParent.getHeight()*/, 0);
appearingAnimation.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator anim) {
View view = (View) ((ObjectAnimator) anim).getTarget();
view.setTranslationY(0f);
}
});
Animator disappearingAnimation = ObjectAnimator.ofFloat(null, "translationY", 0, 600/*profileParent.getHeight()*/);
appearingAnimation.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator anim) {
View view = (View) ((ObjectAnimator) anim).getTarget();
view.setTranslationY(0f);
}
});
transition.setAnimator(LayoutTransition.APPEARING, appearingAnimation);
transition.setDuration(LayoutTransition.APPEARING, 300);
transition.setStartDelay(LayoutTransition.APPEARING, 0);
transition.setAnimator(LayoutTransition.DISAPPEARING, disappearingAnimation);
transition.setDuration(LayoutTransition.DISAPPEARING, 300);
transition.setStartDelay(LayoutTransition.DISAPPEARING, 0);
profileParent.setLayoutTransition(transition);
I want to make a floating layout appears on all screens of any apps on the phone, and I can make actions on this layout besides other app running beside this layout can receive it's own actions and events
And here is an app which do what I want https://play.google.com/store/apps/details?id=com.ninja.sms
Here is the approach which I have worked on:
I used WindowManager to draw the layout on and a service to manage this layout.
I used the following library https://github.com/t0mm13b/TouchSoftly but it has some problems
1. The layout doesn't receive actions or touch events, the actions goes to the views under the one which drawn by this library.
2. The layout disappear when the activity which launched the service killed.
So, I have made some customization on it in the following snippet
_layOutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT);
_layOutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT);
_layOutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
_layOutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
if (_layOutParams != null) {
Log.d(TAG, "onCreate() - Got _layOutParams!");
_layOutParams.gravity = Gravity.RIGHT | Gravity.TOP;
} else
Log.d(TAG, "onCreate() - _layOutParams is null! :(");
_hudPageView = _layOutInflater.inflate(R.layout.service_hudpageview, null);
The result is that : the layout already drawn and can get touch event but the Home, Back and Recent apps buttons doesn't work or have any effect on any app while the layout appear on the screen.
So still doesn't achieve what I want.
And Here is the source code and repository which I am working on, so u can review my source code
https://github.com/mmelsabry/FloatingLayout
I know there is another question here Floating widget / Overlay on Android launcher
but it doesn't help
Try using TYPE_SYSTEM_OVERLAY instead of TYPE_SYSTEM_ALERT.
This has to be a very simple fix, but I can't seem to figure it out. I have got my program to change the background color to change onClick, and onTouch with ACTION_DOWN and ACTION_UP. But I need it to change the color when touching the screen and going in and out of the TextView. I need it to function like a mouseOver/mouseOut event. Is there a way to do this on android? Or am I stuck with onTouch, where the action has to start from within the TextView?
Right now I set the onTouchListener on the TextView itself. Should I set it somewhere else and then check if the x and y are within the Textview? Or is there another event listener I should be using? I am new to Android, any help would be appreciated. Thanks.
Mike
I would implement the OnTouchListener on your application and then on the onTouch method, keep checking if the current position of the touch event is within the bounds of the bounding box of the view. If it is, apply the new background and if it isn't apply the original one.
Since all the views implement the setBackgroundColor I didn't do any casting to TextView but the example should suffice, at least as a starting point to develop your application further.
The full code for this is the following:
public class Main extends Activity implements OnTouchListener{
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//Set the listener for the parent/container view
findViewById(R.id.cont).setOnTouchListener(this);
//Get a hold of the view and create the Rect for the bounds
View target = findViewById(R.id.target);
Rect b = new Rect();
//Get the bounding box of the target view into `b`
target.getGlobalVisibleRect(b);
}
public boolean onTouch(View v, MotionEvent event) {
//Check if it's within the bounds or not
if(b.contains((int)event.getRawX(),(int) event.getRawY())){
target.setBackgroundColor(0xffff0000);
}else{
target.setBackgroundColor(0xff000000);
}
//You need to return true to keep on checking the event
return true;
}
}
As for the user interface for the previous code, it's just a linear layout with an ID cont and a view (a TextView in your case) with an ID target. The rest is totally by default so there is no point in me pasting it here. Note I only tested this on an emulator and ymmv when trying it on real devices, but as far as I can think of, it should be fine.
Relevant documentation:
getGlobalVisibleRect method
onTouchListener
There is apparently a bug in Android which breaks View.bringToFront.
If I have Views arranged in a GridView and want to bring a View to front by calling bringToFront(), it may get moved to the bottom right position in the GridView.
Something shady is going on there, so I can't use bringToFront(). When I don't call it, everything works fine. But the Views overlap - if I use scale animation to scale up a View, it's partially obscured by the Views to the bottom and to the right.
I've checked out bringToFront's source and it calls parent.bringChildToFront(...)
it's this method
public void bringChildToFront(View child) {
int index = indexOfChild(child);
if (index >= 0) {
removeFromArray(index);
addInArray(child, mChildrenCount);
child.mParent = this;
}
}
it apparently removes the View from itself! Without mercy! Is a ViewGroup so dumb that it can't manage Z-indexes in any other way that shuffling controls around? Obviously when GridView removes its child and then adds it back again, it gets placed at the end of the Grid!
Is there anything I can do to show a View on top of others without resorting to some hacking? One thing that comes to my mind is to create another View above the GridView, which will appear to be above the one I'm trying to bringToFront(), but I don't like this solution.
apparently I missed the android:clipChildren="false" property in GridView. It solves my problem.
Have you tried making the View focusable and just calling requestFocus()?
Calling bringToFront() changes the ordering in the GridView. Try creating an ImageView with an image of the view you want to animate and animate that instead.
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
final ImageView imageView = new ImageView(getActivity());
imageView.setImageBitmap(bitmap);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(view.getWidth() , view.getHeight());
imageView.setLayoutParams(params);
rootview.addView(imageView);
Add an animation listener to your animation and remove the ImageView at the end of the animation.
From API 21 you can call:
view.setZ(float)
If you target API above 21, you can simply add the attribute android:translationZ="xxx dp" to your XML.
Please note that if you add elevation in views like cardview, you go into the Z axis. And if you want a view come to foreground using this way, you just have to make it higher than the elevation you set.
Example : 6 dp elevation in cardview will require a 7dp in the attribute translationZ of the view you want foreground.