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

Monday, January 19, 2015

Partial SlidingPaneLayout

Gmail has an interesting UI on tablet:

The side pane is always visible, showing icons when collapsed, cross fading to more details when expanded. How is it implemented?

My first observation is that the main pane slides when the side pane expands, so I know it is not a NavigationDrawer. Let's try a SlidingPaneLayout.

SlidingPaneLayout

<android.support.v4.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <TextView
      android:layout_width="240dp"
      android:layout_height="match_parent"
      android:background="@color/blue"
      android:text="@string/pane_1"/>
  <TextView
      android:layout_width="400dp"
      android:layout_height="match_parent"
      android:layout_weight="1"
      android:background="@color/light_blue"
      android:text="@string/pane_2"/>
</android.support.v4.widget.SlidingPaneLayout>

Looks good, except the main pane turns gray. Fortunately we can change the fade color to transparent.

SlidingPaneLayout layout = (SlidingPaneLayout) 
    findViewById(R.id.sliding_pane_layout);
layout.setSliderFadeColor(Color.TRANSPARENT);

Partial side pane

Now I want to make the side pane partially visible when collapsed. Took me a while (plus a shower) to figure that out, but once I did it was really simple: add margin to the main pane.

<android.support.v4.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <TextView
      android:layout_width="240dp"
      android:layout_height="match_parent"
      android:background="@color/blue"
      android:text="@string/pane_1"/>
  <TextView
      android:layout_width="400dp"
      android:layout_height="match_parent"
      android:layout_weight="1"
      android:layout_marginLeft="64dp"
      android:background="@color/light_blue"
      android:text="@string/pane_2"/>
</android.support.v4.widget.SlidingPaneLayout>

With the margin, the side pane peeks from below when collapsed.

Cross fade

Finally, the cross fade. I replaced the side pane with FrameLayout, the bottom view being the full pane and the top view being the partial pane.

<com.sqisland.android.CrossFadeSlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <FrameLayout
      android:layout_width="240dp"
      android:layout_height="match_parent"
      android:background="@color/purple">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/full"/>
    <TextView
        android:layout_width="64dp"
        android:layout_height="match_parent"
        android:background="@color/blue"
        android:text="@string/partial"/>
  </FrameLayout>
  <TextView
      android:layout_width="400dp"
      android:layout_height="match_parent"
      android:layout_weight="1"
      android:layout_marginLeft="64dp"
      android:background="@color/light_blue"
      android:text="@string/pane_2"/>
</com.sqisland.android.CrossFadeSlidingPaneLayout>

Then I subclass SlidingPaneLayout to cross fade between the partial pane and the full pane on slide. To do that, I need to get the two panes.

@Override
protected void onFinishInflate() {
  super.onFinishInflate();

  if (getChildCount() < 1) {
    return;
  }

  View panel = getChildAt(0);
  if (!(panel instanceof ViewGroup)) {
    return;
  }

  ViewGroup viewGroup = (ViewGroup) panel;
  if (viewGroup.getChildCount() != 2) {
    return;
  }
  fullView = viewGroup.getChildAt(0);
  partialView = viewGroup.getChildAt(1);

  super.setPanelSlideListener(crossFadeListener);
}

Since SlidingPaneLayout already has the convention of specifying the side pane vs main pane by position, I also look for the partial pane and full pane by position. The first child of the SlidingPaneLayout is the side pane, its first child is the full pane, second child is the partial pane. I stash them in the fields fullView and partialView, which are used in the cross-fade listener.

private SimplePanelSlideListener crossFadeListener 
    = new SimplePanelSlideListener() {
  @Override
  public void onPanelSlide(View panel, float slideOffset) {
    super.onPanelSlide(panel, slideOffset);
    if (partialView == null || fullView == null) {
      return;
    }

    partialView.setVisibility(isOpen() ? View.GONE : VISIBLE);
    partialView.setAlpha(1 - slideOffset);
    fullView.setAlpha(slideOffset);
  }
};

Here, I change the alpha of the partial pane and the full pane depending on the slide offset. Since I don't want the partial pane to react to touch events when the layout is open, I set the partial pane to View.GONE. The same logic needs to be applied onLayout because devices with sufficient width (e.g. tablets) may start with the layout opened.

@Override
protected void onLayout(
    boolean changed, int l, int t, int r, int b) {
  super.onLayout(changed, l, t, r, b);

  if (partialView != null) {
    partialView.setVisibility(isOpen() ? View.GONE : VISIBLE);
  }
}

Here we go, a partially shown side pane that cross fades into a different view when expanded. Enjoy!

Source: https://github.com/chiuki/sliding-pane-layout

Monday, October 20, 2014

First look at AnimatedVectorDrawable

Android Lollipop introduced a lot of sweet new classes. The one that caught my eye is AnimatedVectorDrawable, and I decided to check it out right away.

Example from Documentation

The documentation included an example, so I created the files as instructed: res/drawable/vectordrawable.xml, res/drawable/avd.xml, res/anim/rotation.xml and res/anim/path_morph.xml

Now what? There are many ways to use a Drawable. How about in a TextView?

<TextView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/example_from_documentation"
  android:drawableBottom="@drawable/avd"/>

No animation yet. Let's start it.

for (Drawable drawable : textView.getCompoundDrawables()) {
  if (drawable instanceof Animatable) {
    ((Animatable) drawable).start();
  }
}

And voilĂ ! Animation. But what is it? Let's step it through.

res/drawable/vectordrawable.xml

<vector
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:height="64dp"
  android:width="64dp"
  android:viewportHeight="600"
  android:viewportWidth="600" >
  <group
    android:name="rotationGroup"
    android:pivotX="300.0"
    android:pivotY="300.0"
    android:rotation="45.0" >
    <path
      android:name="v"
      android:fillColor="#000000"
      android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
  </group>
</vector>

This is the VectorDrawable. The path defines a triangle, and the group rotates it by 45 degrees.

res/drawable/avd.xml

<animated-vector
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:drawable="@drawable/vectordrawable" >
  <target
    android:name="rotationGroup"
    android:animation="@anim/rotation" />
  <target
    android:name="v"
    android:animation="@anim/path_morph" />
</animated-vector>

Next we have avd.xml, which rotates the group and morphs the path.

res/anim/rotation.xml

<objectAnimator
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:duration="6000"
  android:propertyName="rotation"
  android:valueFrom="0"
  android:valueTo="360" />

res/anim/path_morph.xml

<set
  xmlns:android="http://schemas.android.com/apk/res/android">
  <objectAnimator
    android:duration="3000"
    android:propertyName="pathData"
    android:valueFrom="M300,70 l 0,-70 70,70 0,0   -70,70z"
      android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
    android:valueType="pathType"/>
</set>

The rotation starts from 0 degrees. But since our drawable is initially rotated at 45 degrees, there is a sudden jump when the animation starts. It takes 6000ms to reach 360 degrees. Meanwhile, the path morphs from a triangle to a rectangle in 3000ms, half the time. So, at 180 degrees, the morph is complete.

Phew, that was not obvious at all. To understand what was happening, I split the rotation and the path morph to observe them separately.

Clock

After trying the example, I wanted to make my own, to see if I can come up with some simple animations that makes sense. I like the idea of animating different parts of the VectorDrawable, and made a clock.

The hours arm rotates from 9 o'clock to 5 o'clock while the minutes arm goes around from 0 to 60 minutes. The duration of rotations are set differently to give them different speeds.

Smiling face

Next I played with path morph. What is a path morph that makes sense? Let's make a sad face into a happy face!

Points to note

  1. The <vector> tag must have android:height and android:width to define the intrinsic width and height of the VectorDrawable. Your app will crash if you skip them.
  2. If your VectorDrawable has a smaller android:height or android:width than when you use them say in an ImageView, your graphic will look pixelated.
  3. To morph from one path to another, the paths must be compatible. They need to have the exact same length of commands, and exact same length of parameters for each command.

I am very excited about VectorDrawable and AnimatedVectorDrawable. Scalable graphics FTW!

Source code: https://github.com/chiuki/animated-vector-drawable

Monday, June 24, 2013

The Importance of Being Whimsical

What makes a good app? Sure, it should do what it claims to do, intuitively and efficiently. But that is just the base line. How do you stand out from the sea of apps? You need to go above and beyond, and whimsy may just be the secret ingredient you need.

Tumblr

At the keynote at AnDevCon Boston, Chris Haseman and Zack Sultan showed us how they designed and implemented the Tumblr app. They shared many tips and tricks to make your app look good and work well, but the recurring theme is to delight your user.

There are many whimsical touches to Tumblr, one being the pull-to-refresh animation:

Tumblr could have gone with a loading text or a standard spinny, but this custom animation brings the app a notch above others. It entices users to refresh more, increasing engagement. This is the power of whimsy.

Welcome animations

Animation is a great way to add whimsy to your app. At AltWWDC, Ben Johnson showcased many different apps with effective animations.

This is the welcome page of the Just Landed app. The subtle movement of the plane and the clouds hints to the users that there is more to come, encouraging them to try it out. You can argue that this is gratuitous, nothing more than eye candy. It adds no functionality to the app. But apps are not just about functionality. You need to connect with your users, make them feel good using your app. A delightful welcome page sets the stage for the rest of the app, and helps them stay engaged.

Reward your users

What’s happening here? You’re in the Zappos app, thought these sneakers look good, and pressed the "add to cart" button. A cat floated out, dropping the shoes into your cart Mary-Poppins style. What’s your reaction? Cool, I want to see that again. How? By adding more stuff to your cart! A whimsical gesture that directly increases the bottom line. I doubt anyone can call that gratuitous.

Whimsy everywhere

So you want to be whimsical. What to do? Whimsy hinges on unexpectedness, so it's a bit of an oxymoron to provide a formula. Fortunately, you have already taken the first step - awareness. Once you start paying attention, you will see whimsy everywhere, adding them to your repertoire, ready to inspire your own.

The other day I found whimsy in the most boring place of all - airplane safety video.

The video caught my eyes with a teeny tiny suitcase that got stowed under the seat. I found myself looking forward to more funny shots, and paying attention throughout. If you graph the whimsical moments, you can see that they frontload them to set the tone:

Once you are hooked, they get down to business and show you some serious content. Just before you get bored, they sprinkle a bit of whimsy again, keeping you on your toes, watching the whole video looking for more.

Unpredictable delights

It was said that we are addicted to emails because they are like slot machines. Most of the time you get nothing, but once in a while you get something awesome, and so you keep playing, in hope of hitting jackpot. Whimsy does the same trick to your app. It delights users in unpredictable ways, keeping them engaged, and separates your app from the herd.

Monday, January 16, 2012

Android: Pendulum Animation

I am working on the splash screen for Monkey Write, and I would like the monkey to be swinging on a vine. In Android you can specify all kinds of animations with xml, so it took me a little bit of time to figure out all the perimeters. Here is my final swinging.xml:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
  android:fromDegrees="15"
  android:toDegrees="-15"
  android:pivotX="50%"
  android:pivotY="0%"
  android:duration="2000"
  android:repeatMode="reverse"
  android:repeatCount="-1"
/>

I am using a pivoted rotation to implement the swinging effect. The animation goes from 15 degrees to -15 degrees, but instead of pivoting at the center, I want the pivot point to be at the top-middle. This is achieved by pivotX="50%" and pivotY="0%". For non-stop animation, I use repeatCount="-1".

It was fairly straightforward figure out all those parameters. But the monkey swings from the left to right, then jumps back to the left before swinging again. To make him swing back and forth, I need to set the repeatMode to reverse. Now I have a properly swinging monkey!

I have uploaded my code here: http://github.com/chiuki/android-pendulum-animation. Instead of a monkey, I use xml to specify a pendulum, which seems appropriate for the animation.