Subtitles section Play video Print subtitles (enlightening music) (audience claps) - Thank you. Can you hear me? Okay, yeah. Welcome everybody, thank you for joining me today. First of all, remember to join the conversation in Twitter, #gotocph. Remember to engage so you can rate my actual session and ask questions. My name is Israel Ferrer Camacho. I'm an Android developer at Twitter. You can follow me on Twitter too, and ask me questions there too. I work on the Moments teams which is a nice experience, fullscreen experience horizontal scrolling where you can add a list of tweets, and it's a story. It doesn't have to be following a chronological order there. For those who are not an English native speaker as me, smoke and mirrors is an expression used to describe something that is obscuring or embellishing the truth. It's usually used for magicians because they do magic which is simply not real. It's just a trick, just so you know. Sorry if I spoil you guys. The reason for this title was in the last Google I/O during the RecyclerView talk, where Adam Powell and Yigit Boyar, by Adam Power and Yigit Boyar. Adam Powell actually started the talk by saying UI developers, we have a lot of things in common with magicians, and that is actually true. If you think about some of the animations that we do, and some of the components that we use in the framework, and that's the idea of this talk. I'm gonna be showing some examples in the Android framework or where we are using this smoke and mirrors to make an application just great, flawless, and smooth. After that I will start talking about one of my favorite apps in Android, and how that experience is actually built, and I will do a demo with that. So let's just start with the Android framework. So these are Timeline. Hopefully you know what is Twitter. So theoretically, anytime like you have infinite tweets, as you can see we're scrolling really fast, performance's really good by the way, so it is a problem right? The problem is we have infinite amount of views that we wanna show but we have limitation. We cannot just instantiate thousands of views at the same time. Actually, what we have really do is which is need to do this. We are showing one, two, three, four views in the screen. We don't need to actually show any or instantiate any order view. Just five at the same time, seven, eight tops if the tweets are really small but Jake likes to talk a lot so they're just big. So, anyway how we do that? We use this component called RecyclerView, the name will give you a hint of what it's actually doing, which is recycling. I wanna talk about it, how internally works because I think it's interesting. Internally, it has the linear LayoutManager, which actually recycles, which actually is responsible for measuring and positioning the item views, and as well as determining when to recycle those views that are not shown anymore. So when you try to actually show the next position, the LayoutManager is gonna go to the RecyclerView, and invoke the method getViewForPosition. When that's called, RecyclerView will go to the Adapter, and will ask for the type of that view because you can have many types of views in your RecyclerView so example if you want to have different headers or you want to have a cutout so for showing ads for example, okay. So yeah, we get the type the RecyclerView is gonna go to the RecyclePool, which is where all the views that ViewHolders that we can recycle. It will get a ViewHolder for that type. If there is none, it will go the the Adapter and then create one. But if there is one, it would just send it back to the Adapter and say hey, can you find this ViewHolder with a new data, with a data for that new position that we wanna show, the one that was showing on the animation. Once it holds the information, it just return it back to the RecyclerView, and then the LayoutManager will actually lay it out in the screen, that item. This is a really simple explanation of how the RecyclerView actually recycles views, and you can learn a lot more in the Google I/O talk that I was talking about, the one that inspired this talk. It's called RecyclerView Ins and Outs, and they talk a lot more about how to animate items, and do transitions in between the states. Another example on the Android framework is this one that I'm showing in the Twitter app. As you can see, there is a list of images, and when you click one with transition to fullscreen. Those are called Shared Element Transitions. Shared Element Transitions, they have smoke and mirrors too but first let's dig in on how it works. The way it works is the activity has an activity transition a state which persists a state of that transition, and it has like the most important part is the shared views that are gonna be transitioning from one screen to another. When activity life cycle is invoked, for example it goes through onDestroy, or onPause so it's disappearing, and then we are gonna start a new activity, it will call the activity transition state to start a new transition which will actually call to the activity transition coordinator. The ActivityTransitionCoordinator is a base class. There are actually two. There is entering and exiting coordinator, so one handles the enter transition, and one the exit transition, and that has a transition manager, and it will begin the transition. So by the default the transition is a translation but you can change that on your theme, and you can actually customize your own transitions between activities. But you must be thinking, you're talking a lot about how this works but what is the actual smoke and mirrors in transition in the shared elements. Well, think about it. When an activity is being, going to hide or to the background, and another one comes in the top, how they are actually re-enabled to do that transition from one to another. Well what they do is they hide the target activity, and they have a list of the views that are transitioning from what position they are transitioned so they pretty much in any activity do that transition, that's what they are doing. How they do that, well, they use ViewOverlay, which is used by the default transition in the framework. ViewOverlay provides a transparent layer on the top of a view, to which you can add any type of visual content, (mumbles), and that doesn't affect the layer hidden (mumbles) on the top so that means that you can pretty much animate anything, and it just works and doesn't mess with the layer hierarchy, it's great. It's gonna be your best friend forever in animations, okay. Think about that, that word of what I said. So let's imagine this, you can see there is two layers. There is the LinearLayout which has a nice image view with a Saturn illustration, and then this is the ViewOverlay. In order to make an animation, I'm gonna add that to the ViewOverlay. How do I do that? Well LinearLayout, any view has that, LinearLayout, getOverlay, and then you add to that overlay. What is gonna happen is that suddenly that ImageView is not gonna be part of the LinearLayout anymore. It's gonna be in this temporary ViewOverlay that we're gonna use for animations. When you do that, the rect of that ImageView is invalidated in the parent so that force a re-layout so you have to be careful if you depend on that view. Otherwise you will mess the whole hierarchy. But the good thing is, any type of touch or animation, touch depend on animation, it gets delegated to that ViewOverlay with that ImageView. So if you are actually, imagine you have a reference to this ImageView, and you try to do animation, it will animate even if it's not part of the LinearLayout anymore, which in this case is part of the ViewOverlay. So unfortunately, shared element transitions are real nice but there are limitations. The first one is that the user can't control the transition with a touch. It just an simple animation from one place to another so the eye of the user gets caught, and doesn't lose the context between the screens but it doesn't allow the user for example to pinch and zoom, and control the transition with a pinch and zoom, right. So that's a limitation. Another limitation is that one, actually when I was doing the code, I saw that here. The transition doesn't track the target destination so if you don't eat all the touch events while the transition is running, it looks terrible and it's just a limitation. I mean, we can simply fix that by using this TransitionListener. Thankfully, we can hear all the life cycle events of the transition so what we can do is onTransitionStart, we are just gonna eat all the touch events, pretty much. Then when the transition finishes, we'll just set the touch listener again to the right place. Easier right? Okay, so before entering into the interesting part of the talk, which is the demo, let's talk about two important attributes for an image. Consistently these are the attributes that are actually might as well be thought type of rendering outside the parent. So first of all is the ClipChildren. The ClipChildren attribute is an attribute of a ViewGroup and by default is set to true, which makes sense because it clips the children to the bounds of the parent ViewGroup, and that's exactly what we want right. By default, we want that to happen. So it looks like this. Right, you're not allowed. That's me actually, that's my avatar so I'm not allowed, my ImageView is not allowed to draw outside the parent view unless I set ClipChildren to false. In that case, I'm gonna be able to draw outside the first parent but not the second, not the following parent. So what I have to do is, spoiler alert, I have to set that to false in all the parents, which is kind of like, but that's how it works. So, yeah okay, really bad animation that I got there. It just shows the point. Okay, so ClipPadding, so ClipPadding is the same. So if you have the ClipChildren false but you have a padding, the view is still gonna clip to that padding, yes, that happens, especially with RecyclerViews, happens a lot. So just remember if suddenly a view of yours gets cut, in a RecyclerView, maybe it's because there is a padding and you didn't set that to false. So this is what it looks like. It gets clipped to the padding, and pretty much already at some point, I think a long time ago, was creating this type of utility, which allows me to go through all the parents of the view, and just set everything to false, which is not super difficult. Anyway, let's talk about one of my favorite apps, yeah this is Google Photos. Have you think about it? Do you have it? Do you like it? I think before I never had such a great UI experience in a gallery. I was actually, it's just top-notch, it's really well thought and for a long time, I was thinking, how did they pull that off? It looks so good, but let's check it out how it works actually. So there is one layer, two layers and then fullscreen, and really what I've, fullest, I pinch this so much, you can see that, I'm not really accurate, but anyway so that's how it works. It's just like super smooth, it has three levels of zoom, and then it goes to a fullscreen. As you can see, they're able, the user is able to control the transition to fullscreen, which we were saying that Shared Element Transition can't do. It's a limitation. So the way you figure out how something works in the UI, pretty much these developer options. You make them 10 times slower, which is too slow, maybe five times is fine, and you make the animation 10 times. The duration and the animation is scaled 10 times slower. Then when you do that, so this is what it looks like. Yeah, the other is just (mumbles). So with that you are able to see what is going on because one, you will understand why that's really important soon but for now and other developer option that you want enable these flames, yes I like flames. So show layout boundaries, and you should enable it all the time. It's such a beautiful UI right now. It's wonderful for your eyes. No, but really, this is really useful for developers. That is the blue line which are the clip-out, the clip bounds, whereas there is the red line, which is the optical bound, which is something into a zoom 4.3 that I actually didn't have. Never to use but maybe you need it, so check it out. And the pink area, which are the margins. These not only helps to see the alignment when you hold the screen but it will help us to feel that, I did ultimately but I wanna show if we were doing together so you see what is the trick of that will work for us, application. So now this application looks like that. It's pretty much the same but with some layout boundaries, right. Yeah this is Google I/O, we had such a great time by the way. Anyway so now, just pay attention to the video. I'm not gonna say anything, just think yourself how this is happening, and then I will go deeper into what is really happening. Okay, so I pinch and zoom, this is my, that was the concert, anyway. So three levels, zoom scrolling, going back. Are you seeing what is happening? You see, we don't see that before because it was fast enough right? And now yeah, picture zoom is more difficult once you put it at scale 10x. It's super difficult actually. You have to do it like it really, huge span in order to go to fullscreen. So what happened? Did you noticed what happened? Okay, let's cut to the real part, which is, okay this part of the zoom in. Let's get closer. Did you see that? Can you see the two different RecyclerViews. There is another invisible RecyclerView with a destination size, with a bigger size, and not only that but they're actually cross-fading between one and another. It's all smoke and mirrors. They're just tricking us but you know, that's magic guys. That's what we do, we do magic. So the lesson here is, sorry let's put it back, it's really good. Magic, magic, okay, yeah. So the lesson is anything fast enough will look good enough, and that's true for everything. When you just try 24 hours to make all your animations 10 times slower, and you are gonna see how the application suddenly doesn't make any sense any more because they all actually use that. It's fast transition, fast animation that hides all the problems in implementation, right. There is one more thing that I wasn't able to get on that video but in this video, you will figure out. When you try to zoom in to the fullscreen. Uh-oh, here we go, what is going on? It's the same image. Right so, they are kind of faking it. They're copying the same drawable maybe, and they're using it in order to do the transition to the fullscreen. So that's another trick but if you do it fast enough, and the scale is not enough, 10 times slower, then it just works. So that again, more magic. These Google Photos guys are just magicians. So fake it until you make it. Pretty much, fake it until you make it. That's what we just do all the time in the development. So I showed you how it works but now I tried to be sure that I wasn't doing it wrong. That my assumption wasn't wrong so I actually create a demo, well I created a demo, which is a simplification of Google Photos but it's pretty much it. It's like two zoom levels, and then the last one is, goes to fullscreen. So you can see all these smoke and mirrors, and you can actually see the code. It's gonna be in the GitHub repo so you can check the code, and you can actually apply all those tricks to your applications. So let's go for the demo, yes fireworks, so good. Again, fireworks. So this is the application. This is some nice drawings, by my wife, this illustration, is an illustrator. So zoom in, zoom out, okay, and then fullscreen, fullscreen, fullscreen. So how did I implemented that? So these are the layers. I have a container, a medium RecyclerView, a small RecyclerView, and a fullscreen container. So the container is gonna be the root of the whole hierarchy layout, and of course it's a FrameLayout. It's an obvious choice because we need to stack all those layers on the top of each other, and they are gonna take fullscreen. Why do I need to put a fullscreen container on that screen is because, as I said before, the transition, the shared element transition doesn't allow the user to control the transition to fullscreen but I wanna do it because that's the nice experience. So I put everything here, and you can just do that by creating your own NavigationManager, and depending on the state, you have a stack, and depending on the stack, you can just inflate the, sorry, instantiate the presenters and the view delegates that you need for each of the layers. But let's talk about the important part which is the medium RecyclerView and the small RecyclerView. From now on, I'm gonna call the small images, the small RecyclerView, and the medium, which is a terrible word, I didn't know how to say bigger than a small, which medium is fine, because it's not too big so medium is fine. So medium and small RecyclerView will be the higher level of zoom before going into fullscreen. So we are using two RecyclerViews because that's the trick that allow us to do that nice zoom in effect on the gallery but before doing that, we have to talk about some thing. Really when I pronouncing this word, nobody actually understand me so it's pivot or something like that. Otherwise you have this nice meme from my favorite episode of Friends, which was the pivot, pivot, pivot. If you don't know it, you maybe too young so you should watch it because it's really fun. It's still fun, incredible, still fun. So yeah, let's talk about pivot. So the pivot by default, it sit in the center of the view. So what happen is when you try to actually scale something it scales from center, and it goes in all directions so it looks, that's not what we want, right? What we want is to actually to scale right down. So the first thing that we have to do is said the pivot, the pivot, to (mumbles) to that top left point of the view, which is the RecyclerView because it's the one that we are scaling. That's important for scaling and rotating too. So then it looks like this, which is actually what we want. We want that type of scale. Okay, so we have two RecyclerViews, and my approach is to use two Adapters bind with the same collection back in the map. Because really they are actually using the same data. They're just the same image. They just have different size, right. It's important to set on the setup the medium one to invisible because we only want to show one first which is the small one, which is the one that is gonna be zooming in the first time. Then let's talk about touch events. There is different approaches but the approach that I like it, was to create this eat and touch dispatcher which is a class that gets all the touch events, and depending on the state of that view, we go to do, we delegate that touch to one or another class. So pretty much what I did was create this class, and then add that class to both Adapters. So how does it look like? Well, it looks like this. It has more stuff but the touch event is probably the most important part. So since both the RecyclerViews are using the same we can get the ideal RecyclerView. So in all the Moment we know what is a RecyclerView getting the touch been. For that, what we do is we have this gallery adjusts the texture which I will talk about it later. It's the scale adjust to the texture that we will use for scaling the RecyclerViews. What I do is in case the small RecyclerView is getting touched, it means there is only one way, which is it can only go the medium RecyclerView, it can only zoom in, right? So that's what it's doing. It's pretty much delegate that to the gallery adjust to the texture, which will know and will do that zooming. Then in case of the medium RecyclerView we have the same. The gallery adjust to the texture will know, will know how to do it. It has both logics. The important part here though is that we have zoom out, which goes from the medium to the small but we have zoom in or in this, actually I was kind of lazy, and I just did click but when you click, you go to the fullscreen. Then how we do that? Well we use the span. The span is the, it's actually not the correct span, it's the delta between the last span and the current span. A span is the distance between your fingers so with that if it's (mumbles) I know that it's now pinch and zoom in, it's just clicking. With that, I ask the RecyclerView, which in this case we know is the medium RecyclerView, give me the view on that coordinates, and then I just perform a click. So that's how the zoom in, zoom out between this medium RecyclerView and the small RecyclerView works. Now how do we scale them? So this is what we're missing, right? We weren't scaling, we were just sending the touch events. We're not a scaler, right. For that, we have convenient contract in the platform OnScaleGestureListener, and it has onScaleBegin and onScaleEnd. The names are pretty self-document but onScaleBegin, you just set up everything, first starting the scaling. On a scale, it's a constant stream of the scale gesture. And onScaleEnd is pretty much the user just stopped touching the screen so it just reset the position to the state that you want. So onScaleBegin, what we wanna do is show both small and medium RecyclerViews because what we wanna do as you saw in the video is add the middle of that zoom in or zoom out, we are gonna do a fade in, fade out in between both RecyclerViews, right. So both have to be visual. You can do it different but this approach works because the smoke and mirrors, it comes out the end onScaleEnd. But on the onScale is pretty much all the maths of the talk which is, while there's a gestureTolerance which applies a low plus filter to avoid the flakiness because our fingers are not that accurate so even if you think you're not moving your fingers, the screen is getting some events so there's a constant signal that can prelude a small increments and decrements on the scale. So we want to apply a low plus filter so we know that a minimum span was met, and it's a span on that same duration that will work scaling before. So that's the gestureTolerance. As I was saying, this code is all on the repo so you can actually read that method later. Then we have the scaleFactor and scaleFactorMedium, which are the scaleFactor. As you can see, on a scale give us are the texture that the texture give us a scaleFactor, and we use that as scaleFactor with a clamp function to limit the minimum and the maximum scale of both RecyclerViews. Of course, one is the inverse of the other, and the minimum and maximum, they have to match. Otherwise in the middle of the transition, they would not have the same size, and then the fade in, fade out will look terrible, right, because it will not match. So the numbers are whatever I like but you can actually modify that and make it as complex as you want. Then the outfit's the same, we want that outfit to be in the middle of the transition, half or both of the RecyclerViews, and then at the beginning, a small RecyclerView want, and the medium to see to, and at the end the other way around. So that's how the onScale works but what happen if suddenly the user stops touching in the middle of the transition? If we don't do anything else, if we leave it as it is, it's just gonna look like two RecyclerViews and half half of both of them, or you don't know, it depends right? But it will look weird, we're just getting a weirdest state. So onScaleEnd, what I wanna do to hide this implementation, this smoke and mirrors, what I want to do is just finish the transition automatically to one of the states. For that, I just use a TRANSITION_BOUNDARY which transition one that is just like I calculate a delta, and so I know how much is missing for the transition so if it's below 20 or 30%, and you can change it for whatever you want, I will assume that the user didn't wanna go fullscreen and he actually wanna go back to this small, right, that's what I do. Otherwise, it fits that both that TRANSITION_BOUNDARY, it means, okay, he wanna go to fullscreen but he was just, doing really quick the move. If you don't do that, what happens is like everything just gets stuck in one place. We don't want that because that will show the trick to the user. So with that, we have the container, the medium RecyclerView, and the small RecyclerView. But then let's talk about the fullscreen image container. The fullscreen is a black, it's a friendly art, which are black background, and that shows in a full with the image, so exactly this. So to do that, there is this requirement by a RecyclerView. When you add an item to the RecyclerView, the following, so you add one item and then another item. The second item is gonna be in a higher level base from the first one so if you try to actually scale that first item, it's gonna be drawing below the second item so it would just not look right. That's each item has an elevation in a RecyclerView. So there are different approach. This is probably the most difficult which is just create a custom LayoutManager that knows when we're clicking that item. When we're clicking one single item, we want that item to be on the top of everything, and be able to draw above all the other items, which I will not do because that's too much work when you can fake it, exactly, I like to fake things. So yeah, then you can use this one. This is middle ground which is you see, is just a callback, and then what you're gonna do is when you get that callback, you set the elevations to the rest of the items so they're in that we wanna scale, is gonna be on the top of all of them. But remember we talked about it five minutes, no, not five, 10, 12, I don't even know. At the beginning of the talk, we talked about your best friend forever in animation from now on, ViewOverlay. That's what we can use. We can just use the ViewOverlay on top of the RecyclerView for the transition for the pinch to zoom to fullscreen. Then we just add that item, that view to the frame layout, which is the fullScreenContainer. Why you wanna make it more complicated when you can do it easily, right? That's it, it's like five lines. So how does our, it's more than five lines. There are some methods there and some instruction but okay. You get the overlay from the fullScreenContainer. You're gonna clear the state of that overlay because you don't know if previously maybe another item that was zoomed in. Then you're gonna add a new item. As you can see this was in onClick. That's the perform click that we were doing before on the itemTouchDispatcher, itemTouchListenerDispatcher. Yes, that one. What we're gonna do is, we animate that itemView, and then animate to fullscreen, and what we have to do is withEndAction, which is gonna be setting the click listener so we can go back to the gallery, to the medium RecyclerView. Let's take a look at that method. So that method is actually pretty trivial too, and as you can see, of course that is more things going on like setting the background to BLACK, setting the background to TRANSPARENT or setting the visibility to GONE but pretty much the idea is you are just using the overlay to do your transitions, and then at the end of the state, when the transition is done, you add that view to the real parent where we will actually add all the logic to do, if you wanna did that image or you wanna change the name of the image of whatever gallery application does on a fullscreen image. So what we are gonna do when somebody clicks on that item on that fullscreen image again, we just have to transition back to the medium RecyclerView so we add that item again on the overlay. Then we're just gonna animate it, we're just gonna set the originX, originY and then scale it to one, which is the original size for that image. Finally, don't forget to remove the item from overlay. Otherwise it will just still be there until somebody, something on the framework decides to recycle that. That's it, that's how easy you can allow the user to interact on the transition without using the shared element transition. Just by creating layers, layers on the top of it, and just one activity. Then you get this, you get zoom in, zoom out, click listener fullscreen. Not bad for a 20 minutes or 30 minutes of talk. Again, magic guys, this is all magic. That's what we do, we do, you're development, we do magic. Magician is your new job role from now on. So let's recap, let's recap. Magic tricks that you can use for animations. ClipPadding, ClipChildren to draw over parents and paddings. That's important one. If you see something is cutting your views, it's probably one of those are not set to false. The ViewOverlay, best friend forever for animations, really. It allows you to draw over the whole layer hierarchy, and to do any type of things without messing with it. Shared element transition has, it doesn't allow the user to control the transition without events. So you can actually allow the user to do that by just creating a single activity, and then creating your own navigation, which is not, it doesn't have to be super complex. We don't need to have 300 life cycle events or anything like that. It should be just show, hide, and maybe something like animate to next, or enter, or exit the screen. That's whole idea for another talk. Then finally, fast animations hide any problem with implementation. If you do it fast enough, as we saw during the talk, it's just gonna look right, and nice, and smooth, and good anyway. So remember that, make it fast enough so the eyes are not able to see the trick, which is pretty much what magicians do. With all that, thank you. Here is the repo. You can check the code right now. You can ask me questions there too, and if you want more examples of smoke and mirrors, these great applications by Nick Butcher's Plaid has many more examples. So remember to rate my session, thank you. (audience claps)
B1 transition zoom item medium view user GOTO 2016 • Smoke & Mirrors the Magic behind Wonderful UI in Android • Israel Ferrer Camacho 69 7 colin posted on 2017/04/26 More Share Save Report Video vocabulary