Smoke and Mirrors: The Magic Behind Wonderful UI in Android

 

New Features in Realm Java

We recently released version 3.4 of Realm Java today, and with it introduced reverse relationship queries and sync progress listeners. Read on for all the details!

Introduction

Smoke and mirrors is an expression usually used to describe what magicians do, which is obscuring or embellishing the truth. They don’t do magic, they fake it and make us believe that it’s true.

This thought came from last year’s Google I/O where Adam Powell, an Android developer at Google, said: Anyone who works on UI development has a lot in common with stage magicians. Stage magicians use misdirection, and other sorts of tricks on stage to make you think that you’re seeing something that you’re not.

Android UI developers are magicians. We make wonderful experiences that look perfect, but in reality they are tricks that we do on the UI to make the users believe that they are wonderful.

UI developers have a lot in common with magicians. I don’t think we are all aware of it. Here I will show you some examples of “smoke and mirrors” in the Android Framework in one of my favorite applications, Google Photos, through our demo.

I want to open your mind about creating experiences for your users. Hopefully after this talk, you will have some more ideas on how to figure out what tricks other apps are using, and how to use the same tricks in your own applications.

Android Framework

I work at Twitter. This is a timeline (see video). Theoretically, timelines have infinite tweets. Does that mean that we need an infinite amount of views to render the timeline? No, because that would have memory limitations. Instead we use a component called RecyclerView.

RecyclerView

When you scroll down, there is a new view appearing from the bottom. It has these components: LayoutManager, RecyclerView, Adapter, and RecyclePool.

The LayoutManager, which knows how to measure and position a view in the screen, is going to call the RecyclerView get view for that position. That position is the new one that’s appearing in the bottom. The RecyclerView is going to get the view type for that new position.

You can have an infinite amount of view types. We have different view types in a tweet: a tweet, an image, video, URL, a card. Depending on that view type, the adapter will say, “this is the view type that we have to render now.”

With that type, RecyclerView will go to RecyclePool, which has a list of views that we can reuse. Then it will give you the View Holder for that view type. In case there is none, the Adapter will create one; if there is one, it would find the new View Holder information. It will return up, and LayoutManager will render it.

If you want to learn more about RecyclerView, check out Google I/O 2016 “RecyclerView Ins and Outs.”

Shared Element Transitions

Let’s look at another example on Twitter that uses some of the Android components with smoke and mirrors.

This is what happens when you click on an image: it opens a full screen gallery. If there are multiple images, you can slide in and out. Here we use the Shared Element Transition. It has an ActivityTransitionState, which persists the state for the transition.

Shared Element Transitions has a list of shared views from the origin and destination because we’re trying to transition from one activity to another activity with an animation. Usually, you want to animate the common views in between screens. In this case, the common view is the photo. That’s what the ActivityTransitionState will be in charge of. When that activity’s being destroyed, the activity lifecycle is going to be, “now you have to start a transition.”

That’s going to be done by the ActivityTransitionCoordinator. It will start enter and exit from the activity lifecycle. The ActivityTransitionCoordinator is going to begin a transition. It will cast that ActivityTransitionState on the new activity that it’s creating. It’s faking the old activity and transition, that image, and then finally rendering the final screen. To this aim, the Android Framework uses ViewOverlay.

ViewOverlay

A ViewOverlay is your best friend for animations. The ViewOverlay provides a transparent layer on the top of all the views. You can add any visual content (an image or any view). Then this doesn’t affect the layout. It sits on top of it. It’s not going to bother the layout hierarchy. That’s helpful for transitions and animations.

Imagine you have this LinearLayout, which is an ImageView. You want to transition from that small image to a full screen image. You can get the ViewOverlay of the LinearLayout, and add to that ViewOverlay, that ImageView.

When you do that, first, the view will be removed from the parent, and the rectangle is invalidated. You have to be careful; something is going to move in layer hierarchy depending on your constraints. That means it forces a re-layout once, and then since the overlay is on the top, you can now do anything you want. You can move it around, and nothing else is going to happen.

This is an interesting use of the ViewOverlay with a Shared Element Transition. This is the old version of the Play Store. They fixed this, but when you click to an application, you go to full screen. Then if you’re scrolling when you come back, since they’re not following the destination, you can scroll and then it’s going to transition to the previous position, which is not there anymore because you scrolled. There is a bunch of limitations in the framework.

One of the limitations of the Shared Element Transition is that the user cannot control the transition with a touch. It’s a transition that it animates but it’s not controlled by user. You can say, go from point A to point B. The user is not going to be in control. It’s the system that is doing the animation.

The second limitation is that the transition doesn’t track the target destination; if you are not careful about the touch events, this can happen. In order to avoid that, there is this TransitionListener. You get all these events - Start, End, Cancel, Pause, Resume. When you come back again on Resume of that activity, you can disable all of the touch events that happened in that activity that is coming back, and that issue is fixed.

private View.OnTouchListener touchEater = new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return true;  }
};

Important Attributes

ClipChildren

ClipChildren is an attribute of a view group and it is by default true. It’s clipping the children to the bounds of their parent. That makes sense, you don’t want the children to draw normally. By default, you don’t want the children to draw outside the parent.

This is what it looks like by default (see video).

<?xml version="1.0" encoding="utf-8"?>  
<FrameLayout
    android:id="@+id/parent"  xmlns:android="http://schemas.android.com/apk/res/android"  android:layout_width="match_parent"  android:layout_height="match_parent"  android:padding="@dimen/activity_vertical_margin">  
    <FrameLayout
        android:layout_width="300dp"  
        android:layout_height="300dp"  
        android:background="@color/colorAccent"  
        android:clipChildren="false">  
        <FrameLayout
            android:layout_width="200dp"  
            android:layout_height="200dp"  android:background="@color/colorPrimary">  
            <ImageView
                android:id="@+id/imageview"  
                android:layout_width="100dp"  
                android:layout_height="100dp"  
                android:src="@drawable/profile"/>
        </FrameLayout>  
    </FrameLayout>
</FrameLayout>

That’s the default behavior of any view when you try to animate the children inside the parent. It clips to the bound of the parent. If you set that to false, you are able to draw outside the parent. But if you have multiple parents, you have to set all the parents to ClipChildren="false". Otherwise, you are still bounding to the following parent.

That’s good to know because, if you want to animate things around the screen, in the hierarchy, you want to be able to draw outside the parent bounds.

ClipPadding

The same happens with a ClipPadding. If you disable the ClipChildren, but you have ClipPadding, you will clip to the padding of the parent (see video). You still have to set ClipPadding to false. Then you are able to draw outside the parent even if it has padding.

Utils

You can have a loop going through all the parents, parent views, and set the ClipChildren to false or true depending on what you want to do.

public static void disableParentsClip(@NonNull View view) {  
    while (view.getParent() != null &&
            view.getParent() instanceof ViewGroup) {  
        ViewGroup viewGroup = (ViewGroup) view.getParent();
        viewGroup.setClipChildren(false);
        viewGroup.setClipToPadding(false);
        view = viewGroup;
    }  
}

public static void enableParentsClip(@NonNull View view) {
    while (view.getParent() != null &&
            view.getParent() instanceof ViewGroup) {
        ViewGroup viewGroup = (ViewGroup) view.getParent();
        viewGroup.setClipChildren(true);
        viewGroup.setClipToPadding(true);
        view = viewGroup;
    }

Google Photos

Let’s talk about one of my favorite apps. I love photography, so one of the applications that I use pretty much everyday is Google Photos. The Google Photos experience in Android is top-notch. How did they do this?

I wanted to figure out how Google Photos delivers this experience. It’s a multi-level zoom, and in the third level of zooming, you go to full screen. The Shared Element Transition doesn’t allow the user to handle the transition with a touch but, in this case, the user is in control of the transition all the way. That’s what we want.

To create this user experience, we have a bunch of developer options in our devices: Transition Animation Scale, and Animation Duration Scale. You can turn them to a scale of 10x, which is slow. That may help us to figure out what is happening on the Google Photos.

But maybe you don’t think that would be useful for figuring out UIs from other applications… but it is. If you enable it, you get all the layout boundaries, which means margins. The blue lines are the clip bounds. The print areas have their margins. The red lines are the optical bounds. It was included in 4.3 (I don’t think I ever used it, but you could check it on their specs).

It’s good for development, to make sure that everything is lined up correctly and appears as expected in the design; it shows you the lines where every view on the screen sits.

Lesson: Almost Anything Fast Enough Will Look Good

Let’s see how Google Photos looks after enabling all these developer options. There are some boundaries (see video). Let’s see what happens with animation with this new and slow animation.

Now we are able to see things going on. That multi-zoom level is (it looks like) two RecyclerViews. Then, in the middle of the transition on the scaling, it’s cross-fading. (zooming in) You see the boundaries. You can also see that is the destination RecyclerView appearing, but it’s empty; and then at some point in the middle of that transition, it cross-fades to the new one. It’s magic.

Lesson 1: Almost anything fast enough will look good enough. Our eyes are not that sharp.

A way to be sure that this is the reality in the application is by using the Layout Inspector. You plug in the device, and then you get all the Google Photos activities. You check the activity you want, which in this case is going to be LocalPhotosActivity. Then you get all the layout hierarchy in the screen (see video).

It’s fantastic because you can hover, and you can see how it highlights the view on the screen. This tool is fantastic to figure out even how your application layouts are staying in the screen. It pauses the view in the exact same time you generate this Layout Inspector view. It’s great to see what is going on in the middle of animation, when something is not rendering as you would expect for your code.

As we see there are two RecyclerViews. From the top level of zoom, before the images go to full screen, there is this other thing; behind that is the same image that you are trying to bring in to full screen. Let’s get closer: they are making another trick (see video). They are not using the second RecyclerView for that. Maybe they’re using a ViewOverlay.

They can do it in many different ways but you always try to do things that worked, so maybe ViewOverlay’s good enough. Then when you transition to the full screen, you can copy that image to the new layout hierarchy in the screen.

Lesson 2: Fake it until you make it.

Demo

Google Photos is a fantastic application. I was trying to figure out how to do it. Then I used all the tools that I showed you before, and I realized how it works.

This demo is a simplification; Google Photos is way more complex. In my demo, all the images will have the same size so I don’t have to crop or to scale or anything. It works well because they are all square images. But that’s all math, that’s not a problem - that’s not the user experience. The idea is trying to make the same experience in a demo (real quick, in less than 50 minutes).

You can check the code for this “Smoke and Mirrors” application. It goes down two levels, and then to full screen. Similar to the experience of Google Photos. How does it work?

I created one screen, one activity. You didn’t need more activities because we want one screen, and then being able to intercept all the touch events from the user, and from there, handling the logic of the navigation. This application is condensing three screens: the zoom level one, zoom level two, and full screen.

They are doing it because they probably wanted that smooth behavior of the scaling with the touch, and the user being able to transition from one another. That doesn’t work with three activities. You need one activity to handle that logic in one screen altogether (because that’s how Android Framework works).

I have a layer hierarchy with everything in it. I have a container that contains the two RecyclerViews, and the full screen container which you cannot see, but that blue layer is a full screen container. I have all the layers in the same activity. The container is the root of the whole hierarchy. Obviously you need a frame layer because you need to be able to move everything around freely.

Pivot is important on the scaling and rotating of views in Android. The default pivot point on a view is on the center of the view. If you try to scale these RecyclerView from 0-0, the pivot default, that’s a scaling in all directions. But we want to scale right and down. To do that, we have to set another pivot. We want to scale from the top left point of the RecyclerView. Then it works, but we need to do something else of course.

/**
* Sets the x location of the point around which the view * is {@link #setRotation(float) rotated} and
* {@link #setScaleX(float) scaled}.
* By default, the pivot point is centered on the object.
*/

smallRecyclerView.setPivotX(0);
smallRecyclerView.setPivotY(0);

mediumRecyclerView.setPivotX(0);  
mediumRecyclerView.setPivotY(0);

This scale now makes sense; that’s what you expect when you are zooming. That is two RecyclerViews: the smallRecyclerView, which is a small size images, and the mediumRecyclerView, which is the second level of zoom.

We need to have two adapters. Both adapters can be backed up by the same collection because it’s the same data. We are rendering the image in a bigger size, but the data is the same.

smallRecyclerView.setAdapter(smallAdapter);  

mediumRecyclerView.setAdapter(mediumAdapter);  

mediumRecyclerView.setVisibility(INVISIBLE);

Then we’re going to show this smallRecyclerView; the mediumRecyclerView has to set the visibility to invisible. Now we have two RecyclerViews on the screen. One is invisible, and we know that we are setting the pivot in the right coordinates so it’s going to scale properly but now we have to handle the touch right, and scale.

That is logic, that is the behavior there which is at the middle of the transition between a small and mediumRecyclerView: cross-fade them. Then from the mediumRecyclerView to full screen container, animate that to full screen and fade the background to black.

There are many approaches. The approach that I find simplest:

ItemTouchListenerDispatcher dispatcher =
      new ItemTouchListenerDispatcher(this,
      galleryGestureDetector, fullScreenGestureDetector);

smallRecyclerView.addOnItemTouchListener(onItemTouchListener);

mediumRecyclerView.addOnItemTouchListener(onItemTouchListener);

ItemTouchListenerDispatcher is going to be the single entry point to handle the touch behavior. Both RecyclerViews are going to use the same ItemTouchListenerDispatcher; it’s a class that extends the RecyclerView on ItemTouchListener.

Inside that TouchListener, I’m going to have the behavior, saying I’m now in a smallRecyclerView. If the user scales up, then I start scaling the image with the smallRecyclerView, and then in the middle of that scaling, I cross-fade to the mediumRecyclerView, which is going to go to the original size. You don’t want to load in the smallRecyclerView the full-blown image, because that’s not going to scale memory-wise. That’s why you do that cross-fade.

Let’s see how this is implemented.

public class ItemTouchListenerDispatcher implements RecyclerView.OnItemTouchListener {
  ...
      @Override
      public void onTouchEvent(RecyclerView rv, MotionEvent e) {
          currentSpan = getSpan(e);  
          switch (rv.getId()) {
              case R.id.mediumRecyclerView: {  
                  if (currentSpan < 0) {
                      galleryGestureDetector.onTouchEvent(e);  
                  } else if (currentSpan == 0) {
                      final View childViewUnder = rv.findChildViewUnder(e.getX(), e.getY());  
                      if (childViewUnder != null) {
                          childViewUnder.performClick();  
                      }
                  }
                  break;  
              }
              case R.id.smallRecyclerView: {      
                  galleryGestureDetector.onTouchEvent(e);
                  break;  
              }
              default: {
                  break;  
              }
        }
    }  
...

First, we have a switch case. Let’s start with the smallRecyclerView id. For a touch event, we use galleryGestureDetector. When it’s a mediumRecyclerView receiving the touch, there are two cases: where the user is trying to scale down to the smallRecyclerView, or where the user is trying to scale up to the full screen. If currentSpan is minor than zero, it’s scaling down. Zero means it’s clicking on an image in the mediumRecyclerView, and it went full screen.

It will find that childView under that position, and perform a click in that item. When it performs a click in that item, it’s going to do that transition to full screen. Now we’re handling the touch event but we’re still not doing the scaling, and we’re not doing the transition to full screen. You see how everything builds up, and it’s still not working.

Remember the span. The span is negative. The span is the current span which is the span before and now (from the last touch and the next one). There is a method up called getSpan, and it’s doing that delta between the span of the previous touch and the span of this touch. If it’s negative, it is scaling down. If it’s zero, it means it’s clicking.

Scaling with Gestures

We want to do this scaling animation (see video). Now we are able to detect the touch, and we are able to see if it’s going to scale down or scale up. We just have to do that scaling action.

We do it with this fantastic onScaleGestureListener interface in the framework, which gives you enough information: onScaleBegin, which means that scaling is happening; onScaleEnd, which means the scale has ended so the user is not pinching and zooming anymore; and onScale, which means the user is still pinching and zooming.

public interface OnScaleGestureListener {  
  /**
    * Responds to scaling events for a gesture in progress.  * Reported by pointer motion.
    */
  public boolean onScale(ScaleGestureDetector detector);

  /**
    * Responds to the beginning of a scaling gesture. Reported by  
    * new pointers going down.
    */
  public boolean onScaleBegin(ScaleGestureDetector detector);

  /**
    * Responds to the end of a scale gesture. Reported by existing  
    * pointers going up.

    * @param detector The detector reporting the event - use this to  
    *          retrieve extended info about event state.
    */
    public void onScaleEnd(ScaleGestureDetector detector);  
}

OnScaleBegin, we want to make both the RecyclerViews visible because now at some point, we’re going to set the data and the adapter of the mediumRecyclerView, and then we’ll cross-fade them.

@Override
public boolean onScaleBegin(@NonNull ScaleGestureDetector detector) {
    mediumRecyclerView.setVisibility(View.VISIBLE);  smallRecyclerView.setVisibility(View.VISIBLE);  
    return true;
}

Then during the scale, the interesting part is this gestureTolerance method, which applies a low pass filter because what happen is when you do touch handling, it flickers.

@Override
public boolean onScale(@NonNull ScaleGestureDetector detector) {
    if (gestureTolerance(detector)) {  
        //small
        scaleFactor *= detector.getScaleFactor();
        scaleFactor = Math.max(1f, Math.min(scaleFactor, SMALL_MAX_SCALE_FACTOR));  
        isInProgress = scaleFactor > 1;
        smallRecyclerView.setScaleX(scaleFactor);  smallRecyclerView.setScaleY(scaleFactor);

        //medium
        scaleFactorMedium *= detector.getScaleFactor();
        scaleFactorMedium = Math.max(0.8f, Math.min(scaleFactorMedium, 1f));  mediumRecyclerView.setScaleX(scaleFactorMedium);  mediumRecyclerView.setScaleY(scaleFactorMedium);

        //alpha
        mediumRecyclerView.setAlpha((scaleFactor - 1) / (0.25f));  smallRecyclerView.setAlpha(1 - (scaleFactor - 1) / (0.25f));
    }
    return true;  
}

You get tons of data that are not consistent between each other. Suddenly it looks like it’s scaling up but then it’s a little bit of scaling down. That’s because our fingers are not precise. You need to apply a gestureTolerance, which gets the scale factor in the ScaleGestureDetector and makes sure that it’s over a certain amount of span.

The smallRecyclerView and the mediumRecyclerView are related - the scale is related. We’re going to use two clamp functions. Clamp functions limit on a minimum value and a maximum value. We will do it for both, in inverse. Therefore when the small is in the original size, the medium is going to be in that size too. Then it matches the animation.

We get the relationship between both images’ sizes. That’s what we use for the max value in the scale small factor both, and we do the same for the minimum value in the medium scale factor. Don’t forget that we need to cross-fade them, otherwise the user is going to see two images all the time. We need to set alphas. We do the same. On the 50% of that transition, we start to cross-fade the smallRecyclerView, and then the other one is the inverse.

The scale factor is the value that comes from the clamp function. If you substitute that for the maximum value of the smallRecyclerView, and the minimum value of the mediumRecyclerView, you will see that it makes sense, and one becomes alpha, zero, and the other alpha, one. That’s the scaling and the cross-fading.

If the user stops in the middle of the transition, for example halfway to the mediumRecyclerView or halfway to the smallRecyclerView, it will show both RecyclerViews with 40, 60 alpha. You’re forced to go to an end state.

@Override
public void onScaleEnd(@NonNull ScaleGestureDetector detector)
{
    if (IsScaleInProgress()) {
        if (scaleFactor < TRANSITION_BOUNDARY) {  
            transitionFromMediumToSmall();  
            scaleFactor = 0;
            scaleFactorMedium = 0;
        } else {
            transitionFromSmallToMedium();  
            scaleFactor = SMALL_MAX_SCALE_FACTOR;  scaleFactorMedium = 1f;
        }  
    }
}

There’s a transition boundary that you can define - 20, 80. If it’s 20% way to the mediumRecyclerView. If it’s on the other way around, it goes to small. Depending on that, we finish the transition from medium to small or from small to medium. That’s how we do magic.

Recycler View Elevation

We have two zoom levels. We are missing the full screen image. This is a touch, it’s not a pinch to zoom. You can do it in many ways; that is a problem though. Its child in the RecyclerView has an elevation which is greater than the child before.

If you try to scale a child in a RecyclerView, it will draw over the previous children but it will not draw over the following, over the next children. There is a bunch of ways to do it.

You can create a custom LayoutManager. But it’s complicated. The LayoutManager knows where to render, how, and when. It knows about horizontal or vertical scrolling. It’s complex to implement.

You can always implement some of the methods, but usually it shouldn’t be the first approach. That is this callback that you get when the child is drawing; what you could do is changing. When you get the click, you can change everybody, the old child’s elevation, and then that child could be on the top of everything. But you have to remember to reset it before going back to the mediumRecyclerView or the state is going to be weird. That makes everything more complicated too because you have to keep that state.

Fake it until you make it: ViewOverlay. That’s easy, that works. But remember to remove that content from the ViewOverlay or at some point (and you don’t have control over that), the Android Framework is going to recycle it, and that view will go back to the parent, and then the animation will look weird.

Transition to Full Screen

@Override
public void onClick(@NonNull final View itemView) {
    ViewGroupOverlay overlay = fullScreenContainer.getOverlay();
      overlay.clear();
      overlay.add(itemView);  
      fullScreenContainer.setBackgroundColor(TRANSPARENT);  
      fullScreenContainer.setVisibility(View.VISIBLE);  itemView.animate()
              .x(DELTA_TO_CENTER_X).y(DELTA_TO_CENTER_Y)  
              .scaleX(DELTA_SCALE).scaleY(DELTA_SCALE)
              .withEndAction(setTransitionToRecyclerView()).start();
          }  
}

On the click of that item, we get the fullScreenContainer overlay, which is the frame layout that we used as full screen container. Then, we remember to clear the overlay. Always clear the overlay when you’re going to use it. This is used by the Android Framework sometimes too.

There could be things in that overlay, depending on how things are done. You add that new itemView, which is in this case, an image view. Then you start the animation because, in this case, it’s not pinch and zoom - it’s an animation to that full screen.

At the end of that action, you have to set the transition back. If the user clicks back at some position, then he has to go to the mediumRecyclerView. Remember to do that or the user will be stuck in the full screen forever.

This is the transition to the RecyclerView from the full screen to the mediumRecyclerView.

private Runnable setTransitionToRecyclerView() {  
    return new Runnable() {
        @Override
        public void run() {
            fullScreenContainer.setBackgroundColor(BLACK);  fullScreenContainer.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    overlay.add(itemView);  fullScreenContainer.setBackgroundColor(TRANSPARENT);  itemView.animate().x(originX).y(originY)
                            .scaleY(1).scaleX(1).withEndAction(  
                            new Runnable() {
                                @Override
                                public void run() {
                                    overlay.remove(itemView);
                                    fullScreenContainer.setVisibility(View.GONE);  
                                }
                            }).start();
                    }  
                });
            }  
        };
}

Again, you add an itemView. Then, you’re going to animate to the origin of the child. You’re going to scale it to the original. You’re going to scale it to the original size of that image because it’s full screen. That image grew; you have to turn it back to the original position with the size that fits on the RecyclerView.

At the end, remember to remove that element from the overlay of the full screen container. Then I start the animation (see video), similar to Google Photos.

Magic Tricks

  • ClipPadding, ClipChildren: to draw over parents and paddings.
  • ViewOverlay, your best friend forever for animations.
  • Single activity allows the user to control the transition with a touch. Remember, that is a limitation on the shared element transition from the Android Framework.
  • Fast animations to hide implementation details.

If you want to know more, there is this fantastic application: Nick Butcher’s Plaid.

Q & A

Q: Before this, I went for using LayoutManager, and we gave everything a name in case it got stuck inside. It’s hard to take it back. Do you suggest using ViewOverlay instead? Is there any scenario where ViewOverlay cannot handle that, and then you have to go back to the custom layout?

Israel: ViewOverlay is great for temporary transitions in animations, only temporary though. If you want something to be around - like if the end state of the transition has to be around - remember to copy that over the right layer hierarchy that is going to stay in the screen.

It’s a temporary view on the top of the whole hierarchy that helps you to do animations and transitions and other cool stuff. You can get this demo code, and then don’t remove the clear, and you will see that at some point, it’s going to disappear and come back to the origin. It’s only a temporary helper for animations.

Q: Looking at your reverse engineering of the Photos app, that would suggest that if I had one activity and views inside of those, that gives me maximum control over animation. I’m going to need the activity transitions that they added on there. Would you agree that if I want to have a fancy app that you would have one activity, and then views inside of it?

Israel: If you want the user to have control of the transitions, that’s the only technical way to do it.

If I work at Twitter, and I work on the Moments team, and if you go to your profile, that is My Moments. Inside My Moments, you can create your moment. When you create your moment, it’s one activity, and it has three screens.

We have a navigation manager so it’s a single activity with many, many screens inside. For that, we have a navigation manager. I built that. It was because at the beginning, we wanted the user to transition with pinch and zoom between the different screens because Moments is more visual.

It made more sense that it was something similar to Google Photos. But if you don’t need the user to control the whole interaction between different screens, then you don’t need to do single activity right. You can do it always though. I mean it gives you that feature, and then it gives you the feature that what you can inject each screen, and each screen is not going to be an activity.

Maybe it’s easy to test because you don’t depend much on the Android Framework. At the end, there is no perfect solution. You’re the one that has to put the reasoning behind some solution. I’m not saying do single activity.

Q: But you’re doing it.

Israel: In some cases, not in the whole Android application. Not in the whole Twitter Android application. I want to make that clear because there was some cool animations that we wanted the user to have full control of it.

Q: ViewOverlay is since API 18? Is there a way to support the API before 18?

Israel: You can always implement it yourself. You can always append a FrameLayout on top of everything and do it yourself. Maybe we have to wait. Twitter for Android is not, it supports minimum API 16. We had to pretty much implement it ourselves. We don’t use a ViewOverlay.

Q: A quick note on that. There’s a good app by Chuck Case. Is it old? When he does that transition from one activity to another, the element transition? Implementing it manually or if you look at that, it’s very well explained. It’s a pain but you can get to code, and he explains the detail of the geometry.

Israel: I think that was before the Shared Element? That’s probably what they used. You can go to the code base and see. I looked at the code, and it’s pretty understandable. You can replicate that on your own application if you needed. Or right now, we have Shared Element Transition, and we can customize the transitions. We can pretty much do whatever, if we don’t care about the user interaction.


Israel Ferrer Camacho

Israel Ferrer Camacho

Israel has been developing Android apps since Cupcake. He is really interested in building reusable, testable and maintainable code, without forgetting to delight users with Material design experiences. Israel is currently working as Android developer at Twitter.

Transcribed by Sandra Sanchez-Roige
Edited by Curtis Chen