Showing posts with label animation. Show all posts
Showing posts with label animation. Show all posts

Sunday, August 12, 2018

How to present view controller from left to right in iOS?

Leave a Comment

When adding a new controller to the navigation stack:

self.navigationController!.pushViewController(PushedViewController(), animated: true) 

it appears from the right:

enter image description here

How can I change the direction of animation to make it appear from the left?

8 Answers

Answers 1

You'll need to write your own transition procedure to achieve your needs.

DOCS from Apple:

https://developer.apple.com/documentation/uikit/uiviewcontrollercontexttransitioning

Article:

https://medium.com/@ludvigeriksson/custom-interactive-uinavigationcontroller-transition-animations-in-swift-4-a4b5e0cefb1e

Answers 2

Swift 4: Segue from different directions

Here is a simple extension for different segue directions.(Tested in Swift 4)

It looks like you want to use segueFromLeft() I added some other examples aswell.

extension CATransition {  //New viewController will appear from bottom of screen.  func segueFromBottom() -> CATransition {     self.duration = 0.375 //set the duration to whatever you'd like.     self.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)     self.type = kCATransitionMoveIn     self.subtype = kCATransitionFromTop     return self } //New viewController will appear from top of screen.  func segueFromTop() -> CATransition {     self.duration = 0.375 //set the duration to whatever you'd like.     self.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)     self.type = kCATransitionMoveIn     self.subtype = kCATransitionFromBottom     return self }  //New viewController will appear from left side of screen.  func segueFromLeft() -> CATransition {     self.duration = 0.1 //set the duration to whatever you'd like.     self.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)     self.type = kCATransitionMoveIn     self.subtype = kCATransitionFromLeft     return self } //New viewController will pop from right side of screen.  func popFromRight() -> CATransition {     self.duration = 0.1 //set the duration to whatever you'd like.     self.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)     self.type = kCATransitionReveal     self.subtype = kCATransitionFromRight     return self } //New viewController will appear from left side of screen.  func popFromLeft() -> CATransition {     self.duration = 0.1 //set the duration to whatever you'd like.     self.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)     self.type = kCATransitionReveal     self.subtype = kCATransitionFromLeft     return self    } } 

And here is how you implement the above extension:

    let nav = self.navigationController //grab an instance of the current navigationController     DispatchQueue.main.async { //make sure all UI updates are on the main thread.         nav?.view.layer.add(CATransition().segueFromLeft(), forKey: nil)         nav?.pushViewController(YourViewController(), animated: false)     } 

Answers 3

let obj = self.storyboard?.instantiateViewController(withIdentifier: "ViewController")as! ViewController

    let transition:CATransition = CATransition()     transition.duration = 0.3     transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)     transition.type = kCATransitionPush     transition.subtype = kCATransitionFromLeft     self.navigationController!.view.layer.add(transition, forKey: kCATransition)      self.navigationController?.pushViewController(obj, animated: true) 

Whene you use popToViewController that Time

transition.subtype = kCATransitionFromRight 

Answers 4

Ok, here's a drop-in solution for you. Add file named LeftToRightTransitionProxy.swift with the next content

import UIKit  final class LeftToRightTransitionProxy: NSObject {      func setup(with controller: UINavigationController) {         controller.delegate = self     } }  extension LeftToRightTransitionProxy: UINavigationControllerDelegate {      func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {         if operation == .push {             return AnimationController(direction: .forward)         } else {             return AnimationController(direction: .backward)         }     } }  private final class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {      enum Direction {         case forward, backward     }      let direction: Direction      init(direction: Direction) {         self.direction = direction     }      func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {         return 0.3     }      func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {         guard let toView = transitionContext.view(forKey: .to),             let fromView = transitionContext.view(forKey: .from) else {                 return         }          let container = transitionContext.containerView         container.addSubview(toView)          let initialX: CGFloat         switch direction {         case .forward: initialX = -fromView.bounds.width         case .backward: initialX = fromView.bounds.width         }         toView.frame = CGRect(origin: CGPoint(x: initialX, y: 0), size: toView.bounds.size)          let animation: () -> Void = {             toView.frame = CGRect(origin: .zero, size: toView.bounds.size)         }         let completion: (Bool) -> Void = { _ in             let success = !transitionContext.transitionWasCancelled             if !success {                 toView.removeFromSuperview()             }             transitionContext.completeTransition(success)         }         UIView.animate(             withDuration: transitionDuration(using: transitionContext),             animations: animation,             completion: completion         )     } } 

And here's how you can use it:

final class ViewController: UIViewController {      let animationProxy = LeftToRightTransitionProxy()      override func viewDidLoad() {         super.viewDidLoad()          animationProxy.setup(with: navigationController!)     } } 

This solution provides animation for both forward and backward (push and pop) directions. This can be controlled in navigationController(_:animationControllerFor:from:to:) method of your LeftToRightTransitionProxy class (just return nil to remove animation).

If you need this behaviour for specific subclass of UIViewController put appropriate checks in navigationController(_:animationControllerFor:from:to:) method:

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {     if operation == .push && toVC is DetailViewController {         return AnimationController(direction: .forward)     } else if operation == .pop && toVC is ViewController {         return AnimationController(direction: .backward)     }     return nil } 

Answers 5

This may help you

let nextVc  = self.storyboard?.instantiateViewController(withIdentifier: "nextVc")     let transition = CATransition()     transition.duration = 0.5     transition.type = kCATransitionPush     transition.subtype = kCATransitionFromLeft     transition.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseInEaseOut)     view.window!.layer.add(transition, forKey: kCATransition)     self.navigationController?.pushViewController(nextVc!, animated: false) 

Answers 6

If you want to learn how to do custom transitions (i.e. presenting from right to left) then this is a pretty good tutorial for setting them up.

The key things you need to do are set up a transitioning delegate, a custom presentation controller, and a custom animation controller.

Answers 7

you could use a third party library, you can search them in github.comor cocoacontrols.com as navigation Drawer

In my case I use this https://github.com/CosmicMind/Material#NavigationDrawer

others https://www.cocoacontrols.com/search?q=Drawer

enter image description here

Answers 8

You can present your controller from any direction Check the Gif Simply follow this github link there its is already mentioned how to do https://github.com/shaktiprakash099/iOSTransition

INSTALLLATION GUIDE

Add this to ur Podfile

pod 'iOSTransition',:git => 'https://github.com/shaktiprakash099/iOSTransition.git' ,:tag => '0.0.1' 


import the ioSTransition library in the controller in which you are presenting controller

import iOSTransition 


Then Declare a slideTransioningManager varible as below

lazy var slideTransioningDelegate = SlideInPresentationManager() 


Then specify your slidetransioning direction by this while prsenting any controller also dont forget to mention the modal presentaionstyle top custom

override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "leftSegueId"{ let leftVc = segue.destination  as! SlidefromLeftController leftVc.transitioningDelegate = slideTransioningDelegate slideTransioningDelegate.disableCompactHeight = false slideTransioningDelegate.direction = .left leftVc.modalPresentationStyle = .custom } else if segue.identifier == "rightSegueId"{ let rightVc = segue.destination  as! SlidefromRightController rightVc.transitioningDelegate = slideTransioningDelegate slideTransioningDelegate.disableCompactHeight = false slideTransioningDelegate.direction = .right rightVc.modalPresentationStyle = .custom } else { let bottomVc = segue.destination  as! SlideFromBottomController bottomVc.transitioningDelegate = slideTransioningDelegate slideTransioningDelegate.disableCompactHeight = false slideTransioningDelegate.direction = .bottom bottomVc.modalPresentationStyle = .custom } } 
Read More

Monday, July 2, 2018

Apply animation sequentially to multiple views

Leave a Comment

I have an activity with 3 views (buttonViews) in a vertical linear layout. I am generating (inflating) these views dynamically. I want to apply an animation such that, on activity start, the first buttons slide in -> 100 ms delay -> second button slide in -> 100 ms delay -> Third button slide in.

Attempt

I tried implementing it in this way:

private void setMainButtons() {     ArrayList<String> dashboardTitles = DashboardUtils.getDashboardTitles();     ArrayList<Integer> dashboardIcons = DashboardUtils.getDashboardIcons();      final ViewGroup root = findViewById(R.id.button_container);      for (int i = 0; i < (dashboardTitles.size() < dashboardIcons.size() ? dashboardTitles.size() : dashboardIcons.size()); i++){         final View buttonView = DashboardButtonInflater.getDashboardButton(root, dashboardTitles.get(i), dashboardIcons.get(i), this);         if (buttonView == null) continue;         buttonView.setOnClickListener(this);         root.addView(buttonView);         animateBottomToTop(buttonView, (long) (i*50)); // Calling method to animate buttonView     } }  //The function that adds animation to buttonView, with a delay. private void animateBottomToTop(final View buttonView,long delay) {     AnimationSet animationSet = new AnimationSet(false);     animationSet.addAnimation(bottomToTop);     animationSet.addAnimation(fadeIn);     animationSet.setStartOffset(delay);     buttonView.setAnimation(animationSet); } 

Result:

The above method waits for the total delay of all the views and at the end, aminates all the views together. I can guess the culprit here is the thread. The dealy is actually stopping the UI thread from doing any animation. I could be wrong though.

I also tried running the animation code inside

new Thread(new Runnable(){...}).run() 

but that didn't work either.

Expectations:

Can somebody help me achieve the one-by-one animation on buttonView? Thank you.

4 Answers

Answers 1

Animations are statefull objects, you should not use the same instance multiple times simultaneously. In your case the bottomToTop and fadeIn animations are shared between the animation sets. When the set starts (initialize() is called) it will set the start offset of its children.

For example the method could look like :

//The function that adds animation to buttonView, with a delay. private void animateBottomToTop(final View buttonView,long delay) {     AnimationSet animationSet = new AnimationSet(false);     // create new instances of the animations each time     animationSet.addAnimation(createBottomToTop());     animationSet.addAnimation(createFadeIn());     animationSet.setStartOffset(delay);     buttonView.setAnimation(animationSet); } 

Answers 2

The problem might be easily solved with Transitions API. Having declared a root layout with this xml:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:id="@+id/content_frame"   android:layout_width="match_parent"   android:layout_height="match_parent"   android:orientation="vertical"/> 

Then inside activity:

class MainActivity : AppCompatActivity() {      lateinit var content: LinearLayout     private var counter = 0      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContentView(R.layout.activity_main)          content = findViewById(R.id.content_frame)         // wait this view to be laid out and only then start adding and animating views         content.post { addNextChild() }     }      private fun addNextChild() {         // terminal condition         if (counter >= 3) return         ++counter          val button = createButton()         val slide = Slide()         slide.duration = 500         slide.startDelay = 100         slide.addListener(object : TransitionListenerAdapter() {             override fun onTransitionEnd(transition: Transition) {                 addNextChild()             }         })         TransitionManager.beginDelayedTransition(content, slide)         content.addView(button)     }      private fun createButton(): Button {         val button = Button(this)         button.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)         button.text = "button"         return button     }  } 

This chunk of code will result in following output:

You can adjust animation and delay times respectively.


If you want following behavior:

Then you can use following code:

class MainActivity : AppCompatActivity() {      lateinit var content: LinearLayout      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContentView(R.layout.activity_main)          content = findViewById(R.id.content_frame)         content.post { addChildren() }     }      private fun addChildren() {         val button1 = createButton()         val button2 = createButton()         val button3 = createButton()          val slide1 = Slide()         slide1.duration = 500         slide1.addTarget(button1)          val slide2 = Slide()         slide2.duration = 500         slide2.startDelay = 150         slide2.addTarget(button2)          val slide3 = Slide()         slide3.duration = 500         slide3.startDelay = 300         slide3.addTarget(button3)          val set = TransitionSet()         set.addTransition(slide1)         set.addTransition(slide2)         set.addTransition(slide3)          TransitionManager.beginDelayedTransition(content, set)         content.addView(button1)         content.addView(button2)         content.addView(button3)     }      private fun createButton(): Button {         val button = Button(this)         button.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)         button.text = "button"         return button     } } 

Answers 3

Create method, which will accept Any number of Animation to invoke one after another. Just as example.

private void playOneAfterAnother(@NonNull Queue<Animation> anims) {      final Animation next = anims.poll();       /* You can set any other paramters,       like delay, for each next   Playing view, if any of course */       next.addListener(new AnimationListener() {             @Override             public void onAnimationEnd(Animator a) {                 if (!anim.isEmpty()) {                     playOneAfterAnother(anims);                 }             }             @Override             public void onAnimationStart(Animator a) {             }             @Override             public void onAnimationCancel(Animator a) {             }             @Override             public void onAnimationRepeat(Animator a) {             }         });      next.play(); } 

Or with delay for animations, it's easy too.

private void playOneAfterAnother(@NonNull Queue<Animation> anims,                    long offsetBetween, int nextIndex) {      final Animation next = anims.poll();       /* You can set any other paramters,       like delay, for each next   Playing view, if any of course */       next.setStartOffset(offsetBetween * nextIndex);      next.play();       if (!anim.isEmpty()) {          playOneAfterAnother(anims,                offsetBetween, nextIndex +1);      }  } 

Answers 4

Probably, what you need to use is AnimatorSet instead of AnimationSet. The AnimatorSet API allows you to choreograph animations in two ways: 1. PlaySequentially 2. PlayTogether using the apis:

AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playSequentially(anim1, anim2, anim3, ...); animatorSet.playTogether(anim1, anim2, anim3, ...); 

You can further add delays to your animation using

animatorSet.setStartDelay(); 

Visit the complete API docs here https://developer.android.com/reference/android/animation/AnimatorSet

Hope this helps!

Read More

Wednesday, March 28, 2018

Animating In and Out with CSS

Leave a Comment

I have a menu which displays over the top of the current page once the hamburger icon is pressed which uses Glamor for CSS.

The menu animates in from the right of the screen and works perfectly, however i'm struggling to get it to animate out once anywhere in the Menu is pressed.

The animation is written (animateOut) but I need help with the code in flicking between animating in and out depending on the click:

  • Hamburger menu clicked -> menu slides in from the right.
  • Anywhere in the menu container is clicked -> menu slides out to the right.

HamburgerMenu.js

CSS

const cssHamburgerMenuIcon = css({     position: 'absolute',     height: 20,     width: 20,     right: 20,     marginTop: 20, })  const animateIn = css.keyframes({      '0%': {         transform: 'translateX(100%)'     },     '100%': {         transform: 'translateX(0%)'     } })  const animateOut = css.keyframes({      '0%': {         transform: 'translateX(0%)'     },     '100%': {         transform: 'translateX(100%)'     } })  const cssHamburgerMenu = css({     display: 'flex',     position: 'absolute',     flexDirection: 'column',     height: '100%',     width: 250,     right: 0,     top: 0,     zIndex: 1,     color: 'white',     backgroundColor: hamburgerGrey,     fontFamily: 'Century Gothic',     fontSize: '19px',     // animation     animation: `${animateIn} 0.5s`, })  const cssHamburgerList = css({     listStyleType: 'none',     lineHeight: '47px', })  const cssHamburgerListItem = css({  }) 

"CODE"

class HamburgerMenu extends Component {     constructor(props) {     super(props)     this.state = {         menuVisible: false,     } }      render() {         const menuVisible = this.state.menuVisible          return(             menuVisible ?             <div className={cssHamburgerMenu} onClick={() =>this.setState({ menuVisible: false })}>                               <ul className={cssHamburgerList}>                     <li className={cssHamburgerListItem}>Home</li>                     <li className={cssHamburgerListItem}>News</li>                     <li className={cssHamburgerListItem}>About us</li>                     <li className={cssHamburgerListItem}>More</li>                 </ul>             </div>             : (             <img                  className={cssHamburgerMenuIcon}                 src={HamburgerMenuIcon}                 onClick={() => this.setState({ menuVisible: true})                 }             />               )         )     } }     export default HamburgerMenu 

2 Answers

Answers 1

I suggest another approach:

  1. Set the menu's default translateX to 100%

  2. Create a class (i.e. open) which has translateX set to 0%

  3. Set the menu's transition property to "transition: all 0.5s ease-in-out;"

  4. Just add or remove the (open) class when needed to open/close the menu

Answers 2

i would suggest using bootstrap because its easier

Read More

Monday, March 26, 2018

Trigger anime.js animation when element enters viewport

Leave a Comment

I'm trying to run an anime.js when an image or element enters the viewport, but i cant seem to get it working. Im trying it with waypoints.js

This is what I have so far, its the 'this' part im having troubles with i think.

$('img').waypoint(function() {         var CSStransforms = anime({           targets: this,           translateX: 250,           scale: 2,           rotate: '1turn'           });             }, {                 offset: '100%'             }); 

2 Answers

Answers 1

You need to target the elements with this.element instead, Here's a working example:

CodePen Demo

Per your question, you would modify it to the following:

jQuery(document).ready(function(){     $('img').waypoint(function() {         var CSStransforms = anime({             targets: this.element,             translateX: 250,             scale: 2,             rotate: '1turn'         });     }, {             offset: '100%'     }); }); 

Answers 2

You need to change the

targets : this to targets: this.element

Read More

Wednesday, January 17, 2018

Kivy: what is the proper method for animating images with canvas?

Leave a Comment

I am not fully understanding how to use canvas correctly for images with animations.

See the attached snippet, where I load an animated icon into an Image and do both: (1) add_widget the Image (2) create a Rectangle canvas instruction with a texture = Image's texture

The Image animates The Rectangle texture does not

I have read through all of the Kivy manual and read through Image and Canvas and I get the idea that Image is a nice high level class with all of this image animation handling and Canvas is more of a raw low-level drawing canvas.

So here is my question - what is the Kivy-correct architecture for handling animations on a Canvas? I looked at Animation but that seems for more matrix-like animations such as translation, scaling, rotation.

Here is what I am doing now: I have game with large map window and then a bunch of game UX in helper windows The game UX helper windows I do all the kivy layouts and such and use generally Images and so my icons are animating nicely

However in the game map, I am using canvas:

Drawing all of my game objects using this paradigm:

r=Rectangle(texture=some_Image.texture) map.canvas.add(r) 

When the world needs to be re-drawn:

1) map.canvas.clear()

2) draw all of the stuff in their new positions and states (to be faster, I should just track the dirty objects and locations and just draw those, but to be honest I am getting fantastic fps even with this nuclear-level clear on each draw)

This is of course a lot faster and lighter weight than creating and destroying hundreds of widget-classes - what map canvas is for - right?

But the problem is that my icons with animations in a zip file are not animating

Q: Am I thinking of canvas wrong? Should I instead be adding an Image for each of my game objects instead? (And take advantage of all the animated image support?)

from kivy.uix.relativelayout import RelativeLayout from kivy.uix.image import Image from kivy.app import App from kivy.graphics import Rectangle   class MainApp(App):     def __init__(self, **kwargs):         super().__init__(**kwargs)         self.root = RelativeLayout()          # use any zip file of an animated image         self.animated_icon = Image(source='factory_icon.zip')          # If I add an Image, the icon animates         self.root.add_widget(self.animated_icon)          # If I add the Image's texture on to a Rectangle instruction, no animation         r = Rectangle(texture=self.animated_icon.texture, size=(100, 100), pos=(100, 100))         self.root.canvas.add(r)      def build(self):         return self.root   if __name__ == '__main__':     MainApp().run() 

1 Answers

Answers 1

Image.texture property changes in time. It schedules internally methods to update it as the animation goes. This change doesn't propagate to your rectangle because you created it with texture value captured at a very certain point in time, between updates. Consider this example (I use a .gif file for the animation, but the principle should be the same):

from kivy.uix.relativelayout import RelativeLayout from kivy.uix.image import Image from kivy.app import App from kivy.graphics import Rectangle   class MainApp(App):     def __init__(self, **kwargs):         super(MainApp, self).__init__(**kwargs)         self.root = RelativeLayout()          animated_icon = Image(source='test.gif')         animated_icon.bind(texture=self.update_texture)          self.r = Rectangle(texture=animated_icon.texture, size=(500, 255), pos=(100, 100))         self.root.canvas.add(self.r)      def update_texture(self, instance, value):         self.r.texture = value      def build(self):         return self.root   if __name__ == '__main__':     MainApp().run() 

Here I bind my own update_texture method to image's texture property so every time it changes I can update the rectangle accordingly.

Read More

Tuesday, December 26, 2017

Why calling setNeedsUpdateConstraints isn't needed for constraint changes or animations?

Leave a Comment

Readings:

From this answer:

This is what the accepted answer suggests to animate your view changes:

_addBannerDistanceFromBottomConstraint.constant = 0  UIView.animate(withDuration: 5) {     self.view.layoutIfNeeded() } 

Why do we call layoutIfNeeded when we aren't changing the frames. We are changing the constraints, so (according to this other answer) shouldn't we instead be calling setNeedsUpdateConstraints?

Similarly this this highly viewed answer says:

If something changes later on that invalidates one of your constraints, you should remove the constraint immediately and call setNeedsUpdateConstraints

Observations:

I actually did try using them both. Using setNeedsLayout my view animates correctly to the left

import UIKit  class ViewController: UIViewController {      override func viewDidLoad() {         super.viewDidLoad()     }      @IBAction func animate(_ sender: UIButton) {          UIView.animate(withDuration: 1.8, animations: {             self.centerXConstraint.isActive = !self.centerXConstraint.isActive             self.view.setNeedsLayout()             self.view.layoutIfNeeded()         })     }      @IBOutlet weak var centerYConstraint: NSLayoutConstraint!     @IBOutlet var centerXConstraint: NSLayoutConstraint! } 

However using setNeedsUpdateConstraints doesn't animate, It just moves the view rapidly to the left.

import UIKit  class ViewController: UIViewController {      override func viewDidLoad() {         super.viewDidLoad()     }      @IBAction func animate(_ sender: UIButton) {          UIView.animate(withDuration: 1.8, animations: {         self.centerXConstraint.isActive = !self.centerXConstraint.isActive             self.view.setNeedsUpdateConstraints()             self.view.updateConstraintsIfNeeded()             })     }              @IBOutlet weak var centerYConstraint: NSLayoutConstraint!     @IBOutlet var centerXConstraint: NSLayoutConstraint! } 

If I don't want animation then using either of view.setNeedsLayout or view.setNeedsUpdateConstraints move it to the left. However:

  • with view.setNeedsLayout, after my button is tapped, my viewDidLayoutSubviews breakpoint is reached. But the updateViewConstraints breakpoint is never reached. This leaves me baffled as to how the constraints are getting updated...
  • with view.setNeedsUpdateConstraints, after the button is tapped my updateViewConstraints breakpoint is reached and then the viewDidLayoutSubviews breakpoint is reached. This does make sense, the constraints are updated, then the layoutSubviews is called.

Questions:

Based on my readings: if you change constraints then for it to become effective you MUST call setNeedsUpdateConstraints, but based on my observations that's wrong. Having the following code was enough to animate:

self.view.setNeedsLayout() self.view.layoutIfNeeded() 

WHY?

Then I thought maybe somehow under the hoods it's updating the constraints through other means. So I placed a breakpoint at override func updateViewConstraints and override func viewDidLayoutSubviews but only the viewDidLayoutSubviews reached its breakpoint.

So how is the Auto Layout engine managing this?

4 Answers

Answers 1

setNeedsUpdateConstraints will update the constraints that will be changed based on a change you have made. For example if your view has a neighboring view with which there a constraint of horizontal distance, and that neighbor view got removed, the constraint is invalid now. In this case you should remove that constraint and call setNeedsUpdateConstraints. It basically makes sure that all your constraints are valid. This will not redraw the view. You can read more about it here.
setNeedsLayout on the other hand marks the view for redrawing and putting it inside animation block makes the drawing animated.

Answers 2

A Playground

AutoLayout Playground with a button and a view. Any time you're suffering start with a Playground!

This is wrong

For starters... what is the view's horizontal position once the X constraint is disabled? You've left it ambiguous. Check out my Playground and how I animate from the left-side to the centre such that you can just keep tapping the button and play bounce the view. Reading the coverage of updating constraints and layouts it only makes sense to me to perform the animation in the layout phase.

@IBAction func animate(_ sender: UIButton) {      UIView.animate(withDuration: 1.8, animations: {         self.centerXConstraint.isActive = !self.centerXConstraint.isActive         self.view.setNeedsLayout()         self.view.layoutIfNeeded()     }) } 

This is also wrong

@IBAction func animate(_ sender: UIButton) {      UIView.animate(withDuration: 1.8, animations: {     self.centerXConstraint.isActive = !self.centerXConstraint.isActive         self.view.setNeedsUpdateConstraints()         self.view.updateConstraintsIfNeeded()         }) }     

In essence you're saying, set up this animation by suggesting you do it and then you force it. So there is no need for both.

`setNeeds...()` - is a suggestion  `...IfNeeded()` - is an imperative (AKA Now!) 

Here you seem my formulation. In the layout pass establish the Starting layout before the animation block and the ending layout in the block. Then iOS will correctly animate between the two points. Note - I don't leave any ambiguity along the horizontal axis.

@objc func animate(sender: AnyObject) {     guard let innerView = self.innerView,         let innerViewLeadingConstraint = self.innerViewLeadingConstraint,         let innerViewCenterXConstraint = self.innerViewCenterXConstraint else {         return     }      self.view.layoutIfNeeded()     UIView.animate(withDuration: 1.8, animations: {         if self.view.constraints.contains(innerViewCenterXConstraint) {             self.view.removeConstraint(innerViewCenterXConstraint)             self.view.addConstraint(innerViewLeadingConstraint)         } else {             self.view.removeConstraint(innerViewLeadingConstraint)             self.view.addConstraint(innerViewCenterXConstraint)         }         self.view.layoutIfNeeded()     }) } 

As described at ObjC.io

The first step – updating constraints – can be considered a “measurement pass.” It happens bottom-up (from subview to super view) and prepares the information needed for the layout pass to actually set the views’ frame. You can trigger this pass by calling setNeedsUpdateConstraints. Any changes you make to the system of constraints itself will automatically trigger this. However, it is useful to notify Auto Layout about changes in custom views that could affect the layout. Speaking of custom views, you can override updateConstraints to add the local constraints needed for your view in this phase.

The second step – layout – happens top-down (from super view to subview). This layout pass actually applies the solution of the constraint system to the views by setting their frames (on OS X) or their center and bounds (on iOS). You can trigger this pass by calling setNeedsLayout, which does not actually go ahead and apply the layout immediately, but takes note of your request for later. This way you don’t have to worry about calling it too often, since all the layout requests will be coalesced into one layout pass. To force the system to update the layout of a view tree immediately, you can call layoutIfNeeded/layoutSubtreeIfNeeded (on iOS and OS X respectively). This can be helpful if your next steps rely on the views’ frame being up to date. In your custom views you can override layoutSubviews/layout to gain full control over the layout pass. We will show use cases for this later on.

Autolayout Guide

This further clarifies the rules of AutoLayout when changing size.

Instead of immediately updating the affected views’ frames, Auto Layout schedules a layout pass for the near future. This deferred pass updates the layout’s constraints and then calculates the frames for all the views in the view hierarchy.

You can schedule your own deferred layout pass by calling the setNeedsLayout method or the setNeedsUpdateConstraints method.

The deferred layout pass actually involves two passes through the view hierarchy:

  • The update pass updates the constraints, as necessary
  • The layout pass repositions the view’s frames, as necessary

Mysteries of Auto-Layout

There are a pair of 2015 WWDC talks you should watch.

Answers 3

This is a common misunderstanding among iOS developers.

Here's one of my "golden rules" for Auto Layout:

Don't bother about "updating constraints".

You never need to call any of these methods:

  • setNeedsUpdateConstraints()
  • updateConstraintsIfNeeded()
  • updateConstraints()
  • updateViewConstraints()

except for the very rare case that you have a tremendously complex layout which slows down your app (or you deliberately choose to implement layout changes in an atypical way).

The Preferred Way to Change Your Layout

Normally, when you want to change your layout, you would activate / deactivate or change layout constraints directly after a button tap or whichever event triggered the change, e.g. in a button's action method:

@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {     toggleLayout() }  func toggleLayout() {     isCenteredLayout = !isCenteredLayout      if isCenteredLayout {         centerXConstraint.isActive = true      } else {         centerXConstraint.isActive = false     } } 

As Apple puts it in their Auto Layout Guide:

It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. Deferring these changes to a later method makes the code more complex and harder to understand.

You can of course also wrap this constraint change in an animation: You first perform the constraint change and then animate the changes by calling layoutIfNeeded() in the animation closure:

@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {     // 1. Perform constraint changes:     toggleLayout()     // 2. Animate the changes:     UIView.animate(withDuration: 1.8, animations: {         view.layoutIfNeeded()     } } 

Whenever you change a constraint, the system automatically schedules a deferred layout pass, which means that the system will recompute the layout in the near future. No need to call setNeedsUpdateConstraints() because you just did update (change) the constraint yourself! What needs to be updated is the layout i.e. the frames of all your views, not any other constraint.

The Principle of Invalidation

As previously stated, the iOS layout system usually doesn't react immediately to constraint changes but only schedules a deferred layout pass. That's for performance reasons. Think of it like this:

When you go shopping groceries, you put an item in your cart but you don't pay it immediately. Instead, you put other items in your cart until you feel like you got everything you need. Only then you proceed to the cashier and pay all your groceries at once. It's way more efficient.

Due to this deferred layout pass there is a special mechanism needed to handle layout changes. I call it The Princpile of Invalidation. It's a 2-step mechanism:

  1. You mark something as invalid.
  2. If something is invalid, you perform some action to make it valid again.

In terms of the layout engine this corresponds to:

  1. setNeedsLayout()
  2. layoutIfNeeded()

and

  1. setNeedsUpdateConstraints()
  2. updateConstraintsIfNeeded()

The first pair of methods will result in an immediate (not deferred) layout pass: First you invalidate the layout and then you recompute the layout immediately if it's invalid (which it is, of course).

Usually you don't bother if the layout pass will happen now or a couple of milliseconds later so you normally only call setNeedsLayout() to invalidate the layout and then wait for the deferred layout pass. This gives you the opportunity to perform other changes to your constraints and then update the layout slightly later but all at once (→ shopping cart).

You only need to call layoutIfNeeded() when you need the layout to be recomputed right now. That might be the case when you need to perform some other calculations based on the resulting frames of your new layout.

The second pair of methods will result in an immediate call of updateConstraints() (on a view or updateViewConstraints() on a view controller). But that's something you normally shouldn't do.

Changing Your Layout in a Batch

Only when your layout is really slow and your UI feels laggy due to your layout changes you can choose a different approach that the one stated above: Rather than updating a constraint directly in response to a button tap you just make a "note" of what you want to change and another "note" that your constraints need to be updated.

@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {     // 1. Make a note how you want your layout to change:     isCenteredLayout = !isCenteredLayout     // 2. Make a note that your constraints need to be updated (invalidate constraints):     setNeedsUpdateConstraints() } 

This schedules a deferred layout pass and ensures that updateConstraints() / updateViewConstraints() will be called during the layout pass. So you may now even perform other changes and call setNeedsUpdateConstraints() a thousand times – your constraints will still only be updated once during the next layout pass.

Now you override updateConstraints() / updateViewConstraints() and perform the necessary constraint changes based on your current layout state (i.e. what you have "noted" above in "1."):

override func updateConstraints() {     if isCenteredLayout {         centerXConstraint.isActive = true      } else {         centerXConstraint.isActive = false     }      super.updateConstraints() } 

Again, this is only your last resort if the layout is really slow and you're dealing will hundreds or thousands of constraints. I have never needed to use updateConstraints() in any of my projects, yet.

I hope this make things a little clearer.

Additional resources:

Answers 4

I will try to explain it simply:

The first thing to remember is that updating constraints does cause the layout of views to be updated immediately. This is for performance reasons as laying everything out can take time so it 'makes note' of changes that need to take place then does a single layout pass.

Taking that one step further you can then not even update constraints when something affecting them changes but just flag that the constraints need to be updated. Even updating the constraints themselves (without laying out the views) can take time and the same ones could change both ways (i.e. active and inactive).

Now considering all that what setNeedsUpdateConstraints() does is to flag that the constraints for a view need to be re-calculated BEFORE the next layout pass because something about them has changed it doesn't make any constraint changes of affect the current layout at all. Then you should implement your own version of the updateConstraints() method to actually make the required changes to the constraints based on the current app state, etc.

So when the system decides the next layout pass should occur anything that has had setNeedsUpdateConstraints() called on it (or the system decides needs updating) will get its implementation of updateConstraints() called to make those changes. This will happen automatically before the laying out is done.

Now the setNeedsLayout() and layoutIfNeeded() are similar but for control of the actual layout processing itself.

When something that affects the layout of a view changes you can call setNeedsLayout() so that that view is 'flagged' to have it's layout re-calculated during the next layout pass. So if you change constraints directly (instead of perhaps using setNeedsUpdateConstraints() and updateConstraints()) you can then call setNeedsLayout() to indicate that the views layout has changed and will need to be re-calculated during the next layout pass.

What layoutIfNeeded() does is to force the layout pass to happen then and there rather than waiting for when the system determines it should next happen. It's that the forces the re-calculation of the layouts of views based on the current sate of everything. Note also that when you do this fist anything that has been flagged with setNeedsUpdateConstraints() will first call it's updateConstraints() implementation.

So no layout changes are made until the system decides to do a layout pass or your app calls layoutIfNeeded().

In practice you rarely need to use setNeedsUpdateConstraints() and implement your own version of updateConstraints() unless something is really complex and you can get by with updating view constraints directly and using setNeedsLayout() and layoutIfNeeded().

So in summary setNeedsUpdateConstraints doesn't need to be called to make constraint changes take affect and in fact if you change constraints they will automatically take affect when the system decides it's time for a layout pass.

When animating you want slightly more control over what is happening because you don't want an immediate change of the layout but to see it change over time. So for simplicity let's say you have an animation that takes a second (a view moves from the left of the screen to the right) you update the constraint to make the view move from left to right but if that was all you did it would just jump from one place to another when the system decided it was time for a layout pass. So instead you do something like the following (assuming testView is a sub view of self.view):

testView.leftPositionConstraint.isActive = false // always de-activate testView.rightPositionConstraint.isActive = true // before activation UIView.animate(withDuration: 1) {     self.view.layoutIfNeeded() } 

Let's break that down:

First this testView.leftPositionConstraint.isActive = false turns off the constraint keeping the view in the left hand position but the layout of the view is not yet adjusted.

Second this testView.rightPositionConstraint.isActive = true turns on the constraint keeping the view in the right hand position but again the layout of the view is not yet adjusted.

Then you schedule the animation and say that during each 'time slice' of that animation call self.view.layoutIfNeeded(). So what that will do is force a layout pass for self.view every time the animation updates causing the testView layout to be re-calculated based on it's position through the animation i.e. after 50% of the animation the layout will be 50% between the stating (current) layout and the required new layout.

Thus doing that the animation takes affect.

So in overall summary:

setNeedsConstraint() - called to inform the system that the constraints of a view will need to be updated because something affecting them has changed. The constraints are not actually updated until the system decides a layout pass is needed or the user forces one.

updateConstraints() - this should be implemented for views to update the constraints based on the apps state.

setNeedsLayout() - this informs the system that something affecting the layout of a view (constraints probably) have changed and the layout will need to be re-calculated during the next layout pass. Nothing happens to the layout at that time.

layoutIfNeeded() - performs a layout pass for the view now rather than waiting for the next system scheduled one. At this point the view and it's sub views layouts will actually be re-calculated.

Read More

Sunday, December 3, 2017

How to prevent recalling my css animation after using ':active' selector?

Leave a Comment

I would like to add a bouncing animation to my button. Button should enter the screen with this animation. It works. But after that I added an :active selector.

#button:active{  transform: translateX(20px); } 

And I doesn't work. It just ignores this selector. But I figured out that after adding an animation name to this selector it works. Only then but the problem is that it repeats my bouncing animation as well. It can be any name. Even a name of an animation which doesn't exist. For example:

#button:active{  transform: translateX(20px);  animation-name: not_existing_animation; } 

And that's why I need help. I made a fiddle to let you better see my problem: https://jsfiddle.net/gfd2pjbz/3/

3 Answers

Answers 1

I found a solution about this animation issue. I don't know is it work for you. But I found few coding issue in your Jsfiddle.

First codding issue.

You haven't flow the W3C rules. button is a closing tag element. It's not none closing tag element like <img /> <br /> etc.

Second codding issue.

You have to forgot to write position direction CSS property. position: fixed | absolute | sticky need to set left | right | top | bottom direction.

I tested your fiddle many times why not :active pseudo-class not work after clicked. Problem found from your first animation. animation and bounceInDown classes are contain the transform property. Your animation will not work until you remove the animation and bunceInDown classes. So I need to write a function for remove those classes.

$(function(){     $('#button').on('click', function(){         $(this).removeClass('animated bounceInDown');     }); }); 

When I removed those classes I seen button is disappeared because of #button opacity: is 0;. So I need opacity: 1; in #button.

$(function(){     $('#button').on('click', function(){         $(this).addClass('opacity-visible');     }); }); 

Now found an another issue. Issue is first click :active animation not working. Because of the first click didn't allow transform property until animation classes are removed. Then need add a class when removing those animation classes. After added new class the :active animation will work.

$(function(){     $('#button').on('click', function(){         $(this).addClass('active');     }); }); 

Now need to set a timeOut function for remove the active class for button back to original place for next clicked animation. Now I can write all function together.

$(function(){     $('#button').on('click', function(){     $(this).addClass('active opacity-visible');     $(this).removeClass('animated bounceInDown');     setTimeout(function(){         $('#button').removeClass('active');     }, 2000);   }); }); 

Checked the snipped. I hope it will help you to perform the best solution.

setTimeout( function(){  $("#button").addClass("animated bounceInDown");  }, 1000);    $(function(){  	$('#button').on('click', function(){    	$(this).addClass('active opacity-visible');      $(this).removeClass('animated bounceInDown');      setTimeout(function(){      	$('#button').removeClass('active');      }, 2000);    });  });
*:focus{      outline: none !important;  }  *{      -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;  }  #button {    position: fixed;    background-color: green;    border: 2px solid rgba(0, 0, 0, 0.15);    border-radius: 4px;    color: white;    cursor: pointer;    height: 20%;    left: 0;    width: 20%;    top: 0;    opacity: 0;  }    #button:active{    background-color: red;    transform: translateX(50%) !important;   /* animation-name: not_existing_animation; */  }  #button.opacity-visible{    opacity: 1;    transition: transform 0.3s ease-in-out 0s;  }  #button.active{    background-color: black;    transform: translateX(50%) !important;  }    /*!   * animate.css -http://daneden.me/animate   * Version - 3.5.2   * Licensed under the MIT license - http://opensource.org/licenses/MIT   *   * Copyright (c) 2017 Daniel Eden   */    .bounceInDown {    animation-name: bounceInDown;    opacity: 1!important;  }      .animated {    animation-duration: 1s;    animation-fill-mode: both;  }    @keyframes bounceInDown {    from, 60%, 75%, 90%, to {      animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);    }      0% {      opacity: 0;      transform: translate3d(0, -3000px, 0);    }      60% {      opacity: 1;      transform: translate3d(0, 25px, 0);    }      75% {      transform: translate3d(0, -10px, 0);    }      90% {      transform: translate3d(0, 5px, 0);    }      to {      transform: none;    }  }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>  <button id="button">Click Me</button>

I suggest you to don't write :active css for this type of animation. More specification here on MDN.

Answers 2

I found a super cool solution for you.

First see the preview: https://codepen.io/ziruhel/pen/aVjGMY

Separate initial opacity to a class and add this class to your button.

Like:

<button id="button" class="visibility"/> 

And CSS:

.visibility {   opacity: 0; } 

Now remove the animation and your desire transform when button is :active by this code:

#button:active {   transform: translate3d(20px, 0, 0);   /* transform: translateX(20px); you can also use this */   animation-name: none;  } 

It will now translate to right, but bouncing still remain. To remove this bouncing do this:

$(document).on("click", "#button", function() {   $(this).removeClass("animated bounceInDown visibility"); });  

It will remove animation that you added when first load or initialize.

Answers 3

You could use a Promise to just remove the bouncing class. Check also the minor css modifications in the snippet below.

var p = new Promise(function(resolve, reject) {    var $timeout = setTimeout(function() {      document.getElementById("button").classList.add("animated", "bounceInDown");    }, 1000);    if ($timeout) {      resolve($timeout);    } else {      reject('Failure!');    }  });  p.then(function(response) {    if (response) {      setTimeout(function() {        document.getElementById("button").classList.remove("bounceInDown");        console.log("Yay! finished");      }, 1900);    }    }).catch(function() {    console.log("Something went wrong");  });
#button {    position: fixed;    height: 20%;    width: 20%;    opacity: 0;  }    button.animated:active,  button.animated:focus {    transform: translateX(20px);    background-color: red;  }    .bounceInDown {    animation-name: bounceInDown;    opacity: 1!important;    animation-fill-mode: both;    animation-duration: 1s;  }    .animated {    background-color: green;    opacity: 1!important;    transition: transform 2s, background-color 1s;  }    @keyframes bounceInDown {    from,    60%,    75%,    90%,    to {      animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);    }    0% {      opacity: 0;      transform: translate3d(0, -3000px, 0);    }    60% {      opacity: 1;      transform: translate3d(0, 25px, 0);    }    75% {      transform: translate3d(0, -10px, 0);    }    90% {      transform: translate3d(0, 5px, 0);    }    to {      transform: none;    }  }
<button id="button" />

Read More

Tuesday, November 7, 2017

Android best practices for fragment animations

Leave a Comment

I have activity with two tabs. Both tab uses different fragments. When a particular event occur like user click on item, I am opening another fragment in same activity.

I know how to add fragment dynamically and I also know how to animate it.

Here, how I added fragment to frameLayout in my activity:

transaction.setCustomAnimations(R.animator.object_slide_in_up, R.animator.activity_hold) transaction.add(R.id.flSellerHome, fragment) transaction.commit() 

Everything works fine in emulator and newer phones. I have tested with emulator with api 25, it works fine now flickering occur, When I am testing it with real device with api 23 it flicker little, so it doesn't affect, after then when I tested it with api 19, it flickers too much.

So my question is any best practise for doing animation.

Notes

  • My third fragment which is dynamically added contains recylerview with around 20 items from local db, and I also done db fatching in background thread.

  • No loads on main thread. recyclerview is also simple with one Image and three texts.

  • Image is also loaded using Glide and also I have override function of glide**

any help is appreciated..

4 Answers

Answers 1

I found problem is with recyclerview data updation.

I am loading data in background thread, but when notifying recyclerview, it stucks for small amout of time.

So what I have done is. I have delayed data load for same amount of time which is for animation.

I don't know it is good idea or bad idea. But it is too late for my project so I used this workaround.

Answers 2

The problem is with the RecyclerView which can update.

Try refering this link

Answers 3

I wanted to write comment to your answer, but I dont have enough rep, but I want try to advice you something, so sorry for this.

I am loading data in background thread, but when notifying recyclerview, it stucks for small amout of time.

Try to start loading, which you did, after click and show simple ProgressBar, and when data is loaded, hide progress bar, set info to your fragment and then show fragment. Then you will have all data wich you need in fragment when it is attached and can put it (data) to your adapter.

It have to look good.

Answers 4

If it occurs only for API 19 and lower, it may be a lack of performance from the device.

Maybe the problem do not come from Fragments and Animations, but from another functionality which slow down the UI thread. By functionality, I mean the way your fragments communicate together. Do they share a huge amount of data ? Or heavyweight datas ? If so, you should use Async Tasks or Threads.

Good luck !

Read More

Sunday, November 5, 2017

React-Native why are the animations not linear and from the released place?

Leave a Comment

I am trying to build a simple drag-and-drop with animation when releasing the piece to its original square

enter image description here

The goal is simply to drag coins and, when releasing them, they go back to their cells. But the animations for the pieces return are a bit strange. For example if you drag the red coin into the bottom-right cell, then the animation starts from the bottom-left cell and does not go into a straight line !

This is the code of the page, which can be directly integrated in your RN app, if you have the same package.json as the following one :

import React, { Component } from 'react'; import { StyleSheet, View, Animated, PanResponder, Easing } from 'react-native'; import _ from 'underscore';  class Square {     constructor(value, origin, cellsSize) {         this.value = value;         this.pan = new Animated.ValueXY();         this.cellsSize = cellsSize;         this.boardSize = 3 * this.cellsSize;         this.minXY = this.cellsSize * (0.5);         this.maxXY = this.cellsSize * (1.5);         this.midXY = this.cellsSize;         this.origin = origin;         this.constrainedX = this.pan.x.interpolate({             inputRange: [this.minXY, this.midXY, this.maxXY],             outputRange: [this.minXY, this.midXY, this.maxXY],             extrapolate: 'clamp',         });         this.constrainedY = this.pan.y.interpolate({             inputRange: [this.minXY, this.midXY, this.maxXY],             outputRange: [this.minXY, this.midXY, this.maxXY],             extrapolate: 'clamp',         });          const x = parseInt(this.cellsSize * (0.5 + this.origin.file));         const y = parseInt(this.cellsSize * (0.5 + this.origin.rank));          this.pan.setValue({ x, y });         this.panResponder = this._buildPanResponder();     }      get valueString() {         return this.value;     }      get panRef() {         return this.pan;     }      get panResponderRef() {         return this.panResponder;     }      _buildPanResponder() {         return PanResponder.create({             onStartShouldSetPanResponder: () => true,             onPanResponderGrant: (event, gestureState) => {                 this.pan.setOffset({ x: this.pan.x._value, y: this.pan.y._value });             },             onPanResponderMove: (event, gestureState) => {                 this.pan.setValue({ x: gestureState.dx, y: gestureState.dy });             },             onPanResponderRelease: (event, gesture) => {                 const nativeEvent = event.nativeEvent;                  const origX = parseInt(this.cellsSize * (this.origin.file + 0.5));                 const origY = parseInt(this.cellsSize * (this.origin.rank + 0.5));                  Animated.timing(                     this.pan,                     {                         toValue: { x: origX, y: origY },                         duration: 400,                         delay: 0,                         easing: Easing.linear                     }                 ).start();                  this.pan.flattenOffset()             }         });     } }  export default class TestComponent extends Component {      constructor(props) {         super(props);          this.cellsSize = 100;          this.squares = [             new Square('red', { file: 1, rank: 0 }, this.cellsSize),             new Square('green', { file: 0, rank: 1 }, this.cellsSize),             new Square('blue', { file: 1, rank: 1 }, this.cellsSize),         ];     }      renderACoin(value, file, rank) {         if (value) {             let style;             switch (value.valueString) {                 case 'red': style = styles.redCoin; break;                 case 'green': style = styles.greenCoin; break;                 case 'blue': style = styles.blueCoin; break;             }              const randomKey = parseInt(Math.random() * 1000000).toString()              return (                 <Animated.View key={randomKey} style={StyleSheet.flatten([style,                     {                         left: value.constrainedX,                         top: value.constrainedY,                     }])}                     {...value.panResponderRef.panHandlers }                 />             );         }     }      renderAllCoins() {         return _.map(this.squares, (currSquare) => {             return this.renderACoin(currSquare, currSquare.origin.file, currSquare.origin.rank);         });     }      render() {          return (             <View style={styles.topLevel}>                 <View style={StyleSheet.flatten([styles.board])}                     ref="boardRoot"                 >                     <View style={StyleSheet.flatten([styles.whiteCell, {                         left: 50,                         top: 50,                     }])} />                     <View style={StyleSheet.flatten([styles.blackCell, {                         left: 150,                         top: 50,                     }])} />                     <View style={StyleSheet.flatten([styles.blackCell, {                         left: 50,                         top: 150,                     }])} />                     <View style={StyleSheet.flatten([styles.whiteCell, {                         left: 150,                         top: 150,                     }])} />                      {this.renderAllCoins()}                  </View>             </View>          );     } }  const styles = StyleSheet.create({     topLevel: {         backgroundColor: "#CCFFCC",         flex: 1,         justifyContent: 'center',         alignItems: 'center',         flexDirection: 'row',     },     board: {         width: 300,         height: 300,         backgroundColor: "#FFCCFF",     },     whiteCell: {         width: 100,         height: 100,         backgroundColor: '#FFAA22',         position: 'absolute',     },     blackCell: {         width: 100,         height: 100,         backgroundColor: '#221122',         position: 'absolute',     },     greenCoin: {         width: 100,         height: 100,         position: 'absolute',         backgroundColor: '#23CC12',         borderRadius: 50,     },     redCoin: {         width: 100,         height: 100,         position: 'absolute',         backgroundColor: '#FF0000',         borderRadius: 50,     },     blueCoin: {         width: 100,         height: 100,         position: 'absolute',         backgroundColor: '#0000FF',         borderRadius: 50,     }, }); 

This is the package.json I am using

{     "name": "test",     "version": "0.0.1",     "private": true,     "scripts": {         "start": "node node_modules/react-native/local-cli/cli.js start",         "test": "jest"     },     "dependencies": {         "react": "16.0.0-beta.5",         "react-native": "0.49.3",         "underscore": "^1.8.3"     },     "devDependencies": {         "babel-jest": "21.2.0",         "babel-preset-react-native": "4.0.0",         "jest": "21.2.1",         "react-devtools-core": "^2.5.2",         "react-test-renderer": "16.0.0-beta.5"     },     "jest": {         "preset": "react-native"     } } 

Each Square is implemented thanks to the Square class, which holds the origin square, the drag and drop pan responder and pan animated value. The drag and drop animation are constrained to the cells thanks to two x/y interpolators.

This is the Expo Snack application.

My guess is that the strange animation behaviour is caused by the interpolators, or some value I forgot to set to the pan animatedXY value, but I can't be sure.

1 Answers

Answers 1

Not from the released place

This is because of the way your offsets are resolved. Your toValue coordinates are correct when no offset is applied to them, so you should start the animation after offsets have been flattened. Otherwise you'll start off (before flattenOffset is called) going from the point of release to the wrong end point, and when offsets are flattened that "corrects" the end coordinate but the start point will now be wrong. You can see this more clearly if you slow the animation right down and put the flattenOffset call inside a setTimeout so it happens mid-animation.

To fix, just move the flattenOffset() call to before start().

  onPanResponderRelease: (event, gestureState) => {     const nativeEvent = event.nativeEvent;      const origX = parseInt(this.cellsSize * (this.origin.file + 0.5));     const origY = parseInt(this.cellsSize * (this.origin.rank + 0.5));      // Our animated path should be calculated without an offset, as our     // origX and origY are both un-offset, so flattenOffset() before start()     this.pan.flattenOffset();      Animated.timing(       this.pan,       {         toValue: { x: origX, y: origY },         duration: 400,         delay: 0,         easing: Easing.linear       }     ).start();   } 

Non-linear

This becomes more obvious once the issue above is resolved and you can see what's happening. It's simply because your pan is animating from the point of release back to the circle's origin, but the circle itself is constrained. So, if your point of release is outside the constrained area, you'll see the circle creep horizontally or vertically along the edge of the constrained area, as close as it can be to pan, until the pan value moves inside the box, where the circle can follow it.

What to do about that depends on your desired behaviour. Assuming you don't care how far outside the constrained area the pan was released, and you just want the circle to animate linearly from where it appears back to its origin, then the simplest thing to do is set your pan value to the constrained version of itself before beginning the animation:

  onPanResponderRelease: (event, gestureState) => {     const nativeEvent = event.nativeEvent;      const origX = parseInt(this.cellsSize * (this.origin.file + 0.5));     const origY = parseInt(this.cellsSize * (this.origin.rank + 0.5));      // Our animated path should be calculated without an offset, as our     // origX and origY are both un-offset, so flattenOffset() before start()     this.pan.flattenOffset();      // Act as if we have released from the centre of where the circle appears     // on screen, rather than potentially outside the constrained area     this.pan.setValue({ x: this.constrainedX.__getValue(), y: this.constrainedY.__getValue() });      Animated.timing(       this.pan,       {         toValue: { x: origX, y: origY },         duration: 400,         delay: 0,         easing: Easing.linear       }     ).start();   } 

As you can see, this uses the "private" __getValue() method as a convenient way to use the already-constrained values. If you wanted to avoid this you'd have to use the coordinates within gestureState and apply your own constraining logic - unfortunately RN doesn't expose a way to use its interpolation logic on a a non-animated value.

Read More

Thursday, October 19, 2017

CSS animation bug in Safari

Leave a Comment

I have a CSS animation with a delay and I pause it during the delay. It works as expected on Firefox and Chrome, the "Hello" does not move. However on Safari, the animation jumps to the last frame. Why and how to fix please?

function test() {    var timeout = 1000;    setTimeout(function() {      document.getElementById('animation').style.animationPlayState = 'paused';    }, timeout);  }    document.addEventListener("DOMContentLoaded", test);
#animation {    animation: test 2s linear 2s;  }    @keyframes test {    to {      transform: translateY(100px);    }  }
<div id="animation">    Hello (this text should not move)  </div>

If I remove the 2s delay, set the duration to 4s, and add a keyframe with transform:none, I can make this simple example work. However my real case has multiple animations that are synchronized with delays.

2 Answers

Answers 1

The Safari behaviour is only buggy when timeout is set to a value smaller than the animation delay. So, a workaround is to set the initial state to paused via animation-play-state and then control it via JS, as shown below:

function test() {    let el = document.getElementById("animation");    let timeout = 1000;        // Get the delay. No luck with el.style.animationDelay    let delay =      window        .getComputedStyle(el)        .getPropertyValue("animation-delay")        .slice(0, -1) * 1000;      // Only resume and later pause when timeout is greater than animation delay    if (timeout > delay) {      el.style.animationPlayState = "running";      setTimeout(function() {        el.style.animationPlayState = "paused";      }, timeout);    }  }    document.addEventListener("DOMContentLoaded", test);
#animation {    animation: test 2s linear 3s;    animation-play-state: paused; /* Pause it right after you set it */  }    @keyframes test {    to {      transform: translateY(100px);    }  }
<div id="animation">    Hello (this text should not move)  </div>

Try different timeout values to see it working. Can't say why this is happening though. Looks like a bug to me. Tested on OS X El Capitan 10.11.6 / Safari 11.0 (11604.1.38.1.7).

Codepen demo

Answers 2

This is not an answer to the problem. However, if you remove the animation delay, pausing and restarting the animation works as it should. It seems then the animation delay is what is causing the problem. Perhaps rather than relying on css to handle the delay, programmatically control animation delay with javascript.

See below pausing and running the animation

function test() {    var timeout = 1000;    setTimeout(function() {      document.getElementById('animation').style.animationPlayState ='paused';      document.getElementById('animation').style.webkitAnimationPlayState ='paused';    }, timeout);    setTimeout(function() {      document.getElementById('animation').style.animationPlayState='running';      document.getElementById('animation').style.webkitAnimationPlayState ='running';    }, timeout * 2);  }    document.addEventListener("DOMContentLoaded", test);
#animation {      -webkit-animation: test 2s linear;          animation: test 2s linear;  }    @-webkit-keyframes test {    to {      -webkit-transform: translateY(100px);          transform: translateY(100px);    }  }    @keyframes test {    to {      -webkit-transform: translateY(100px);          transform: translateY(100px);    }  }
<div id="animation">    Hello (this text should not move)  </div>

Read More

Tuesday, September 19, 2017

Animating react-native-svg dash length of a <Circle />

Leave a Comment

Hey everyone I'm trying to achieve effect similar to: https://kimmobrunfeldt.github.io/progressbar.js (circle one)

I was able to successfully animate some svg elements before using setNativeProps approach, but it is failing for me this time with dash length, below is a gif demonstrating current behaviour (circle is change from full to semi full when it receives new props):

enter image description here

Essentially I am trying to animate this change instead of it just flicking in, below is full source for this rectangular progress bar, basic idea is that is uses Circle and strokeDasharray in order to show circular progress, it receives currentExp and nextExp as values for characters experience in order to calculate percentage left before they reach next lvl.

Component uses pretty standard set of elements, besides few dimension / animation and colour props from stylesheed and styled-components library for styling.

NOTE: project is importing this library from expo.io but it's essentially react-native-svg

import React, { Component } from "react"; import PropTypes from "prop-types"; import styled from "styled-components/native"; import { Animated } from "react-native"; import { Svg } from "expo"; import { colour, dimension, animation } from "../Styles";  const { Circle, Defs, LinearGradient, Stop } = Svg;  const SSvg = styled(Svg)`   transform: rotate(90deg);   margin-left: ${dimension.ExperienceCircleMarginLeft};   margin-top: ${dimension.ExperienceCircleMarginTop}; `;  class ExperienceCircle extends Component {   // -- prop validation ----------------------------------------------------- //   static propTypes = {     nextExp: PropTypes.number.isRequired,     currentExp: PropTypes.number.isRequired   };    // -- state --------------------------------------------------------------- //   state = {     percentage: new Animated.Value(0)   };    // -- methods ------------------------------------------------------------- //   componentDidMount() {     this.state.percentage.addListener(percentage => {       const circumference = dimension.ExperienceCircleRadius * 2 * Math.PI;       const dashLength = percentage.value * circumference;       this.circle.setNativeProps({         strokeDasharray: [dashLength, circumference]       });     });     this._onAnimateExp(this.props.nextExp, this.props.currentExp);   }    componentWillReceiveProps({ nextExp, currentExp }) {     this._onAnimateExp(currentExp, nextExp);   }    _onAnimateExp = (currentExp, nextExp) => {     const percentage = currentExp / nextExp;     Animated.timing(this.state.percentage, {       toValue: percentage,       duration: animation.duration.long,       easing: animation.easeOut     }).start();   };    // -- render -------------------------------------------------------------- //   render() {     const { ...props } = this.props;     // const circumference = dimension.ExperienceCircleRadius * 2 * Math.PI;     // const dashLength = this.state.percentage * circumference;     return (       <SSvg         width={dimension.ExperienceCircleWidthHeight}         height={dimension.ExperienceCircleWidthHeight}         {...props}       >         <Defs>           <LinearGradient             id="ExperienceCircle-gradient"             x1="0"             y1="0"             x2="0"             y2={dimension.ExperienceCircleWidthHeight * 2}           >             <Stop               offset="0"               stopColor={`rgb(${colour.lightGreen})`}               stopOpacity="1"             />             <Stop               offset="0.5"               stopColor={`rgb(${colour.green})`}               stopOpacity="1"             />           </LinearGradient>         </Defs>         <Circle           ref={x => (this.circle = x)}           cx={dimension.ExperienceCircleWidthHeight / 2}           cy={dimension.ExperienceCircleWidthHeight / 2}           r={dimension.ExperienceCircleRadius}           stroke="url(#ExperienceCircle-gradient)"           strokeWidth={dimension.ExperienceCircleThickness}           fill="transparent"           strokeDasharray={[0, 0]}           strokeLinecap="round"         />       </SSvg>     );   } }  export default ExperienceCircle; 

UPDATE: Extended discussion and more examples (similar approach working for different elements) available via issue posted to react-native-svg repo: https://github.com/react-native-community/react-native-svg/issues/451

3 Answers

Answers 1

It is actually quite simple when you know how SVG inputs work, one of the problems with react-native-SVG (or SVG inputs, in general, is that it doesn't work with angle), so when you want to work on a circle you need to transform angle to the inputs which it takes, this can be done, by simply writing a function such as (you necessarily don't need to memorize or totally understand how transformation works, this is the standard):

result

function polarToCartesian(centerX, centerY, radius, angleInDegrees) {         var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;          return {             x: centerX + (radius * Math.cos(angleInRadians)),             y: centerY + (radius * Math.sin(angleInRadians))         };     } 

Then you add another function which can give you the d props in the right format: function describeArc(x, y, radius, startAngle, endAngle){

        var start = polarToCartesian(x, y, radius, endAngle);         var end = polarToCartesian(x, y, radius, startAngle);          var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";          var d = [             "M", start.x, start.y,             "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y         ].join(" ");          return d;     } 

Now it is great, you have the function (describeArc) which gives you the perfect parameter you need to describe your path (an arc of a circle): so you can define the PATH as:

<AnimatedPath d={_d} stroke="red" strokeWidth={5} fill="none"/> 

for example, if you need an arc of a circle of radius R between 45 degrees to 90 degrees, simply define _d as:

_d = describeArc(R, R, R, 45, 90); 

now that we know everything about how SVG PATH works, we can implement react native animation, and define an animated state such as progress:

import React, {Component} from 'react'; import {View, Animated, Easing} from 'react-native'; import Svg, {Circle, Path} from 'react-native-svg';  AnimatedPath = Animated.createAnimatedComponent(Path);  class App extends Component {     constructor() {         super();         this.state = {             progress: new Animated.Value(0),         }     } componentDidMount(){         Animated.timing(this.state.progress,{             toValue:1,             duration:1000,          }).start() }        render() {         function polarToCartesian(centerX, centerY, radius, angleInDegrees) {             var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;              return {                 x: centerX + (radius * Math.cos(angleInRadians)),                 y: centerY + (radius * Math.sin(angleInRadians))             };         }          function describeArc(x, y, radius, startAngle, endAngle){              var start = polarToCartesian(x, y, radius, endAngle);             var end = polarToCartesian(x, y, radius, startAngle);              var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";              var d = [                 "M", start.x, start.y,                 "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y             ].join(" ");              return d;         }          let R = 160;         let dRange = [];         let iRange = [];         let steps = 359;         for (var i = 0; i<steps; i++){             dRange.push(describeArc(160, 160, 160, 0, i));             iRange.push(i/(steps-1));         }           var _d = this.state.progress.interpolate({             inputRange: iRange,             outputRange: dRange         })          return (             <Svg style={{flex: 1}}>                 <Circle                     cx={R}                     cy={R}                     r={R}                     stroke="green"                     strokeWidth="2.5"                     fill="green"                 />                 {/*       X0  Y0               X1   Y1*/}                 <AnimatedPath d={_d}                       stroke="red" strokeWidth={5} fill="none"/>              </Svg>         );     }     }      export default App; 

This simple component will work as you want

  • At the top of the componet, we write,

AnimatedPath = Animated.createAnimatedComponent(Path);

  because Path which is imported from react-native-svg is not native react-native component and we turn it to animated by this.

  • at constructor we defined progress as the animated state which should change during animation.

  • at componentDidMount the animation process is started.

  • at the beginning of render method, the two functions needed to define SVG d parameters are declared (polarToCartesian and describeArc).

  • then react-native interpolate is used on this.state.progress to interpolate the change in this.state.progress from 0 to 1, into change in d parameter. However, there are two points here that you should bear in mind:

       1- the change between two arcs with different lengths is not linear, so linear interpolation from angle 0 to 360 does not work as you would like, as a result, it is better to define the animation in different steps of n degrees (i used 1 degrees, u can increase or decrease it if needed.).

       2- arc cannot continue up to 360 degrees (because it is equivalent to 0), so it is better to finish animation at a degree close to but not equal to 360 (such as 359.9)

  • at the end of the return section, the UI is described.

Answers 2

If you aren't tied to the svg library, I think you could checkout this library: https://github.com/bgryszko/react-native-circular-progress, it might be a much simpler way to achieve what your looking for.

Answers 3

Another absolute great library for animating svg's is https://maxwellito.github.io/vivus/ This is standalone without dependencies and easy to use.

Maybe this fits your needs?

Read More

Wednesday, August 9, 2017

Animate top and bottom dimensions of a view

Leave a Comment

I need to 2 two things with a view:

  1. Move top dimension to the very top of the window
  2. Move bottom dimension to the very bottom of the window.

In short, I need the view to cover the 100% of the parent view.

Translation animation didn't work because It moves the view but it doesn't increase the size

Scale animation works but it stretch the content of the view and I don't want that. I want to increase the visible area, not to stretch the content to fit the new dimensions.

What's the correct way to do this?

6 Answers

Answers 1

That can be easily achieved with Transitions API.

With Transitions API you do not take care of writing animations, you just tell what you want the end values be and Transitions API would take care of constructing animations.

Having this xml as content view (a view in the center of the screen):

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/root"     android:layout_width="match_parent"     android:layout_height="match_parent">      <View         android:id="@+id/view"         android:layout_width="120dp"         android:layout_height="80dp"         android:layout_gravity="center"         android:background="@color/colorAccent" />  </FrameLayout> 

In activity:

override fun onCreate(savedInstanceState: Bundle?) {     super.onCreate(savedInstanceState)     setContentView(R.layout.item)      val root = findViewById(R.id.root) as ViewGroup     val view = findViewById(R.id.view)      view.setOnClickListener {          // After this line Transitions API would start counting the delta         // and will take care of creating animations for each view in `root`         TransitionManager.beginDelayedTransition(root)          // By default AutoTransition would be applied,         // but you can provide your transition with the second parameter          // val transition = AutoTransition()         // transition.duration = 2000         // TransitionManager.beginDelayedTransition(root, transition)          // We are changing size of the view to match parent         val params = view.layoutParams         params.height = ViewGroup.LayoutParams.MATCH_PARENT         params.width = ViewGroup.LayoutParams.MATCH_PARENT          view.requestLayout()     } } 

Here's the output:

Platform's Transitions API (android.transition.TransitionManager) is available from API 19, but support libraries backport the functionality upto API 14 (android.support.transition.TransitionManager).

Answers 2

You can try using ValueAnimator as shown in this answer: https://stackoverflow.com/a/32835417/3965050

Note: I wanted to write this as a comment, but I don't have the reputation. This should not be considered as a full answer.

Answers 3

I like to keep everything as simple as it can be.

so my suggestion would be using a android Animating Layout Changes

Here is a sample:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:animateLayoutChanges="true"     android:animationCache="true">      <TextView         android:id="@+id/textView"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:text="@string/app_name"         android:background="@color/colorPrimary"         android:layout_gravity="center" />  </LinearLayout> 

MainActivity.java

public class MainActivity extends AppCompatActivity {      TextView textView;      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);          textView = (TextView) findViewById(R.id.textView);     }      @Override     protected void onResume() {         super.onResume();          new Handler().postDelayed(new Runnable() {             @Override             public void run() {                  View view = getWindow().getDecorView();                  int height = getWindow().getDecorView().getHeight();                 int width = getWindow().getDecorView().getWidth();                 textView.setLayoutParams(new LinearLayout.LayoutParams(width, height));                  LayoutTransition layoutTransition = ((ViewGroup) textView.getParent()).getLayoutTransition();                 layoutTransition.enableTransitionType(LayoutTransition.CHANGING);             }         }, 2000);     } } 

Answers 4

animateLayoutChanges="true" in the parent xml 

+

.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 

does the trick most of the times and it won't stretch the existing child views

Answers 5

Using ConstraintLayout with ConstrainSet should match your need in the most efficient way.

public class MainActivity extends AppCompatActivity {     ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint Set     ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set     ConstraintLayout mConstraintLayout; // cache the ConstraintLayout     boolean mOld = true;      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         Context context = this;         mConstraintSet2.clone(context, R.layout.state2); // get constraints from layout         setContentView(R.layout.state1);         mConstraintLayout = (ConstraintLayout) findViewById(R.id.activity_main);         mConstraintSet1.clone(mConstraintLayout); // get constraints from ConstraintSet     }      public void foo(View view) {         TransitionManager.beginDelayedTransition(mConstraintLayout);         if (mOld = !mOld) {             mConstraintSet1.applyTo(mConstraintLayout); // set new constraints         }  else {             mConstraintSet2.applyTo(mConstraintLayout); // set new constraints         }     } } 

Source https://developer.android.com/reference/android/support/constraint/ConstraintSet.html

All you need is to define a second layout.xml with your expanded constraints and apply the second ConstraintSet to your view or activity when necessary.

Answers 6

ValueAnimator anim = ValueAnimator.ofInt(viewToIncreaseHeight.getMeasuredHeight(), -100); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {     @Override     public void onAnimationUpdate(ValueAnimator valueAnimator) {         int val = (Integer) valueAnimator.getAnimatedValue();         ViewGroup.LayoutParams layoutParams = viewGroup.getLayoutParams();         layoutParams.height = val;         viewGroup.setLayoutParams(layoutParams);     } }); anim.setDuration(DURATION); anim.start();  
Read More