MotionLayout swipe without snapping to states? - android

I've noticed that all the MotionLayouts so far always snap to one of their end states, even if you drag a view halfway through the animation, stop, and let go. What I'm trying to achieve is to have the motionlayout only act on user interaction and momentum. If the user lets go in the middle of an animation it should stop completely once momentum runs out and not keep interpolating until an explicitly defined state, which is the default behaviour.
Lists, scrollviews, coordinatorlayouts all support this "halfway" stop, so why not motionlayout?
So does anyone knows how to freeze the progress of a motionlayout between states? It should work for any position on the timeline so I don't want to add extra states, as it would require a 100 of them.

Add an onTouchUp to your OnSwipe. stop is probably what you want. decelerate seems to be buggy. And stop is more of a deceleration anyways if it's connected to a scrolling view since the view will provide its own deceleration.
<OnSwipe
app:dragDirection="dragUp"
app:onTouchUp="stop"
app:touchAnchorId="#id/scrollView"
app:touchAnchorSide="top" />
Found this here: https://mikescamell.com/motionlayoutquickie-ontouchup/

You can set the position with motionLayout.setProgress() method, and do it or when the transition is changing or when you release your finger - on MotionEvent.ACTION_UP in the OnTouch method.
First variant:
motionLayout.setTransitionListener(new MotionLayout.TransitionListener() {
// Other methods
#Override
public void onTransitionChange(MotionLayout motionLayout, int i, int i1, float v) {
// v == motionLayout.getProgress();
motionLayout.setProgress(v);
}
// Other methods
});
Second variant:
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
motionLayout.setProgress(motionLayout.getProgress());
}

Related

Fresco ZoomableDraweeView: How to disable swipes when not fully zoomed out?

In an Android photo viewing app, I want to allow users to:
Zoom into a photo, drag to see its details, unzoom.
Swipe to go to the next photo.
Implementation attempt using ZoomableDraweeView in Facebook's Fresco library:
private fun init(imageUri: Uri?) {
val hierarchy = GenericDraweeHierarchyBuilder.newInstance(resources)
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
.setProgressBarImage(ProgressBarDrawable())
.setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
.build()
zoomableDraweeView!!.hierarchy = hierarchy
zoomableDraweeView!!.setAllowTouchInterceptionWhileZoomed(false)
zoomableDraweeView!!.setIsLongpressEnabled(false)
zoomableDraweeView!!.setTapListener(DoubleTapGestureListener(zoomableDraweeView))
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
.setUri(imageUri)
.setControllerListener(loadingListener)
.build()
zoomableDraweeView!!.controller = controller
Problem: When I zoom in, lift the fingers, then try to unzoom, this gets misinterpreted as a swipe and I am randomly sent to the next picture.
What am I doing wrong? How to disable swipes when zoomed in (or any better UX solution)?
I specifically call setAllowTouchInterceptionWhileZoomed(false), whose javadoc says: "If this is set to true, parent views can intercept touch events while the view is zoomed. For example, this can be used to swipe between images in a view pager while zoomed."
I tried to execute the swipe action only when zoomableDraweeView.getZoomableController().isIdentity() is false, but that does not always prevent unintended swipe. In particular, when I fully zoom out, often swipe accidentally happens, maybe because isIdentity() has been updated by the time I release all fingers. getScaleFactor() has the same issue. Another issue is that this solution only allows me to drag the zoomed picture with two fingers, dragging with one finger has no effect.
I thought about writing my own DraweeController and surface the zoom level (and ignore swipes when zoomed) but the base classes do not seem to contain any zoom level information.
By the way, here is how I detect swipes:
open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
private inner class GestureListener :
GestureDetector.SimpleOnGestureListener() {
override fun onFling(
event1: MotionEvent,
event2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
try {
val diffX: Float = event2.x - event1.x
if (abs(diffX) > abs(diffY)) {
if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) >
SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
goToTheNextPhoto() // Swipe detected.
(full code on GitHub if needed)
The issue is not in the Fresco library or in the ZoomableDraweeView. The problem is in the orchestrating of the overall UX in your activity. It is not hard though, and you were on the right track with the zoomableDraweeView.getZoomableController().isIdentity() and setAllowTouchInterceptionWhileZoomed.
There are several ways to overcome it, starting from the custom ZoomableController and ending by exchanging your swipes detector with a proper ViewPager2(RecyclerView based)(or even two as you have vertical and horizontal swipes) with snapping and its own callbacks and animations. But the solution will be similar in all the cases - using everything where(and when) it is supposed to be used. It may require a bit of refactoring of your overall approach.
The first way to fix it is via setAllowTouchInterceptionWhileZoomed, which is not working for you because you apply your swipe detector directly to the ZoomableDraweeView rather than to its parent. Thus your view parent cannot handle swipes because you told it not to via setAllowTouchInterceptionWhileZoomed, but your ZoomableDraweeView can, and it does. Thus making you ZoomableDraweeView parent handle swipes rather than the view itself should do the trick.
Secondly, the ZoomableController is AnimatedZoomableController thus, it performs animation, and the animation has a duration(in this case, it depends on the pinch gesture velocity) - in the case when you were zooming out, and your swipe detector was changing the image - the animation was still ongoing, but the transformation matrix was already back to identity - thus the issue.
To fix this, you have to consider animation duration. I think even a simple 200ms delay should fix it in most cases. Also, I would suggest having an onTransformChanged method of the ZoomableDraweeView view(making a custom view) overridden, checking for the identity matrix in it, and exposing it to the activity in some way - this way, you will be sure that the transformation has already happened(the animation delay is still relevant in this case), and you can easily enable your own swipe detection.
The first method is preferable and more "clean," and the second one is "hacky," but its minimal implementation should be very fast.
I haven't tried to refactor it so I assume some additional work may be required.
Hope it helps.

How to undo changes made by animate()?

Can one undo the changes he made on View properties using animate() on it?
In particular, how to undo changes made using animate().yBy(x)?
Note that I tried using animate().yBy(-x) and it works most of the times, but there are times that for some reason animate().yBy(x) seem not to be completed correctly (especially when the fragment pauses and then resumed) so animate().yBy(-x) is over-moving the view.
I'm looking for a way to make the View reset its properties to the way they were before I changed them using animate().
xBy() and yBy() animations affect the translationX and translationY properties. You can get the current values of those properties via getTranslationX() and getTranslationY(). So, to undo the previous animations, multiply the current property values by -1 and animate those. Or if you are seeking a "smash cut" jump (no animation), just call setTranslationX(0) or setTranslationY(0).
By using interpolator we can inverse the animation:
public class InverAnim implements Interpolator {
#Override
public float getInterpolation(float paramFloat) {
return Math.abs(paramFloat -1f);
}
}
On your animation you can set, new interpolator:
myAnimation.setInterpolator(new InverAnim());

[LibGDX]Delay between button presses

Im making a very simple game and have started making each menu. There will be 3 menus in a row - Main menu - Playher select Menu - Gamemode Menu but the problem i am facing atm is adding a delay between the touches. the code i have uses Sprites for the buttons and detects whether a touch point is within the bounding rectangle.
Like so:
if(Gdx.input.isTouched()){
cam.unproject(PlayerMenuTP.set(Gdx.input.getX(), Gdx.input.getY(), 0));
if(Sprites[1].getBoundingRectangle().contains(PlayerMenuTP.x, PlayerMenuTP.y)){
Game.ChangeScreen(2);
}
}
This works for the first menu but since the buttons are in the exact same position on each menu if you hold you finger on the screen i skips through all the menus.
I don't know how to modify this to wait until the previous touch is no longer occurring then continue to scan for this one.
Any help will be appreciated.
Thanks
I strongly suggest you to use Scene2D for UI related stuff, it's not hard to implement or learn, and its very easy to mantain.
Anyway if you want to keep going in that way, you can do this:
Make your screens implement InputProcessor.
Set Gdx.input.setInputProcessor() to that screen (in the show() create() or the name of the method called when the screen is set)
Override touchUp()
Replace your code with
#Override public boolean touchUp (int screenX, int screenY, int pointer, int button) {
cam.unproject(PlayerMenuTP.set(screenX, screenY, 0));
if (Sprites[1].getBoundingRectangle().contains(PlayerMenuTP.x, PlayerMenuTP.y)){
Game.ChangeScreen(2);
}
return true;
}

Using animation on a ViewPager and setFillAfter

I have a ViewPager which I need to move as a whole on button press. I use an animation for this.
When I press it, I translate the 'x' for it. I use setFillAfter(true) to keep the new position.
But when I change the page of the ViewPager, it jumps back to the original x-position!
I only saw this issue on Android 4.1, with Android 4.0 there is no problem! So it looks like some kind of regression in Android.
I attached a testproject where I could reproduce the issue without all my other stuff around it. I think it is best if you want to help me figure this out to import the project in your Eclipse and see it for yourself.
I also added to video's, one on my HTC One X where I see the issue, and the other on a tablet with Android 4.0, where the issue is not there.
I have been desperately looking to fix this ugly side effect, but no luck till now...
(Sorry for the big movie files...)
Video of Android 4.0 without the side effect
Video Android 4.1 with the side effect
the project where you can reproduce the issue with
Edit:
I added the following:
#Override
public void onAnimationEnd(Animation animation) {
RelativeLayout.LayoutParams lp = (android.widget.RelativeLayout.LayoutParams) myViewPager.getLayoutParams();
if (!i)
lp.setMargins(300,0,0,0);
else
lp.setMargins(0,0,0,0);
myViewPager.setLayoutParams(lp);
}
After that it stays at the correct position, but it 'flickers' quickly, like the animation is still showing at the end and when I change the margin, it still shows the offset it had after animation. Then it jumps to the correct position.
The main problem seems to be incorrect choice of animation type. You see, View Animation as a tool is not intended to be used with complex interactive objects like ViewPager. It offers only low-cost animation of the drawing place of views. The visual behaivior of the animated ViewPager in response to user-actions is undefined and should not be relied on.
Ugly flicks, when you substitute a "gost" with the real object are only natural.
The mechanism, that is intended to use in your case since API 11 is specialized property animator built in Views for optimized performance: ViewPropertyAnimator, or not specialized, but more versatile ObjectAnimator and AnimatorSet.
Property animation makes the View to really change its place and function normally there.
To make project, to use, say, ViewPropertyAnimator, change your listener setting to this:
btn.setOnClickListener(new OnClickListener() {
boolean b = false;
#Override
public void onClick(View v) {
if(b) {
myViewPager.animate().translationX(0f).setDuration(700);
}
else {
myViewPager.animate().translationX(300f).setDuration(700);
}
b=!b;
}
});
If you want to use xml configuration only, stick to |ObjectAnimator and AnimatorSet. Read through the above link for further information.
In case, you are anxious to support pre-Honeycomb devices, you can use Jake Warton's NineOldAndroids project. Hope that helps.
That's because the Animation's setFillAfter(true) doesn't actually change the position or any attributes of the View; all it does is create a Bitmap of the view's drawing cache and leaves it where the animation ends. Once the screen is invalidated again (ie. changing the page in the ViewPager), the bitmap will be removed and it will appear as if the View is returning to it's original position, when in fact it was already there.
If you want the View to retain it's position after the animation has finished, you need to actually adjust the View's LayoutParams to match your desired effect. To achieve this, you can override the onAnimationEnd method of the Animation, and adjust the LayoutParams of the View inside there.
Once you adjust the LayoutParams, you can remove your call to setFillAfter(true) and your View will actually stay where you expect it to.
Regarding the flicker issue:
I have encountered this issue before, and it stems from the possibility of the onAnimationEnd() call not syncing up with the next layout pass. Animation works by applying a transformation to a View, drawing it relative to its current position.
However, it is possible for a View to be rendered after you have moved it in your onAnimationEnd() method. In this case, the Animation's transformation is still being applied correctly, but the Animation thinks the View has not changed its original position, which means it will be drawn relative to its ENDING position instead of its STARTING position.
My solution was to create a custom subclass of Animation and add a method, changeYOffset(int change), which modifies the y translation that is applied during the Animation's applyTransformation method. I call this new method in my View's onLayout() method, and pass the new y-offset.
Here is some of my code from my Animation, MenuAnimation:
/**
* Signal to this animation that a layout pass has caused the View on which this animation is
* running to have its "top" coordinate changed.
*
* #param change
* the difference in pixels
*/
public void changeYOffset(int change) {
fromY -= change;
toY -= change;
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float reverseTime = 1f - interpolatedTime;
float dy = (interpolatedTime * toY) + (reverseTime * fromY);
float alpha = (interpolatedTime * toAlpha) + (reverseTime * fromAlpha);
if (alpha > 1f) {
alpha = 1f;
}
else if (alpha < 0f) {
alpha = 0f;
}
t.setAlpha(alpha);
t.getMatrix().setTranslate(0f, dy);
}
And from the View class:
private int lastTop;
// ...
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// the animation is expecting that its View will not be moved by the container
// during its time period. if this does happen, we need to inform it of the change.
Animation anim = getAnimation();
if (anim != null && anim instanceof MenuAnimation) {
MenuAnimation animation = (MenuAnimation) anim;
animation.changeYOffset(top - lastTop);
}
// ...
lastTop = top;
super.onLayout(changed, left, top, right, bottom);
}
Crucero has it right about setFillAfter not adjusting params post invalidation. When the view is re-layed out (which'll happen the pass after it's invalidated), its layout params will be the ones that always applied, so it should go back to the original position.
And Jschools is right about onAnimationEnd. Strongly encourage you to step through the source code with a debugger, where you'll instructively discover that an update is made that affects the drawn position of the view after onAnimationEnd is fired, at which point you've actually applied the layout params, hence the flicker caused by doubled up offset.
But this can be solved quite simply by making sure you relayout at the right time. You want to put your re-positioning logic at the end of the ui message queue at the time of animation end so that it is polled after the animation but before laying out. There's nowhere that suggests doing this, annoyingly, but I've yet find a reason in any release of the SDK reason why (when doing this just once and not incorrectly using ui thread) this shouldn't work.
Also clear the animation due to another issue we found on some older devices.
So, try:
#Override
public void onAnimationEnd(final Animation animation) {
myViewPager.post(new Runnable() {
#Override
public public void run() {
final RelativeLayout.LayoutParams lp = (android.widget.RelativeLayout.LayoutParams) myViewPager.getLayoutParams();
if (!someBooleanIPresume)
lp.setMargins(300,0,0,0);
else
lp.setMargins(0,0,0,0);
myViewPager.setLayoutParams(lp);
myViewPager.clearAnimation();
}
}

clearAnimation() and onAnimationEnd() in android

I am using multiple animations and playing animations one after other. Currently using onAnimationEnd() to play animations one after other. In case of onTouch, I need to stop the animation and need to set different bitmap to imageview in touch location. Currently using below code but facing problems in case of clearAnimation().
#Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_UP:
imageArray[g_animCount - 1].clearAnimation();
break;
default:
break;
}
return true; // indicate event was handled
}
#Override
public void onAnimationEnd(Animation animation) {
layout.removeView(imageArray[g_animCount - 1]);
if ( (g_animCount < count))
{
startNextAnimation();
}
else
{
g_animCount = 0;
isBegin = false;
}
}
Problems and queries:
After clear animation, I could see image again at the beginning location, how to keep it at touch location ? tried setFillAfter(true) but no use.
How to define onAnimationEnd() in case to play animations one after other ? do we need to remove imageview?
Without clearAnimation() I do not have any problems but it does not solve my problem. Kindly correct my code.
1) to keep the ImageView to the new location you have to specify the new coordinates.
You can do it in this way:
MarginLayoutParams l = new MarginLayoutParams(v.getLayoutParams());
l.leftMargin = left;
l.topMargin = top;
v.setLayoutParams(l);
v is your ImageView, left and top are the coordinates (x,y) to place the view on the screen. The parent should be a RelativeLayout.
2) it's better to start all the animation in sequence avoid calling new start in the callback methods (onAnimationEnd, etc). You don't need to remove the view if you have to use it later in the flow
My reply may be a bit out of date, but better later than never.
I also noticed this nasty bug: when you call cancelAnimation, the AnimationListener.onAnimationEnd is called, however the view stays and there is no "legal" ways to hide it (tried remove view from layout, set visibility to View.INVISIBLE, even negative marginTop and marginBottom - nothing works). The bug is present with Andoid 2.3 and Android 4.0. It doesn't turn up with KitKat, presumably has been fixed.
Here's the good news. I've found a work around with ImageView: in AnimationListener.onAnimationEnd I say animationView.setImageDrawable(null) and the view disappears. Hopefully this will help someone.

Categories

Resources