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

Friday, October 10, 2014

Pan-CJK font on Ubuntu

Ubuntu supports Chinese out of the box, kind of:

See those rectangles? Some characters are not rendered, but enough are, so I left it like that for months. But it was starting to annoying me, and I finally looked into it. Turned out some fonts are missing. I installed ttf-arphic-ukai, which got rid of the rectangles, but I didn't like the way it looked. ttf-arphic-uming was not satisfactory either. As I agonized over which of the two to pick, I remembered that Google and Adobe announced a Pan-CJK font a while ago. Here is how to install it on Ubuntu:

  1. Download the zip file from the Adboe github page.
  2. Unzip it to a temporary directory.
  3. Copy the OTF directory to /usr/share/fonts/. I created a subdirectory opentype/source-han-sans, but you can put them anywhere you want.

Restart your browser, and voilĂ , all the characters are rendered in a beautiful font:

Wednesday, October 8, 2014

Localizing the globe

You have probably seen this at the beginning of a movie:

I am always caught by surprise when the globe stops spinning, expecting to go further. Watching it closely, I realized why: it stops at the Americas. I grew up in Hong Kong, where world maps look like this:

What's special about it? The center of the map is the Pacific Ocean. In my mind, that is the focal point of the globe, so I felt unsatisfied when the Universal globe stops spinning at the Americas.

I found it ironic that the globe, being the obvious icon for a global society, is actually usually localized. And today I found a delightful case of a localized globe - the notification icon in the top right corner of Facebook:

I never paid much attention to it, but my friend Corey is visiting Sweden, and noticed that her icon changed from showing the Americas:

To Europe, Africa and Asia:

Very subtle, but very cool!

Sunday, August 3, 2014

GDG Boulder

I moved from California to Colorado this May, and want to get more involved with the local tech community. As an Android developer, especially as a Google Developers Expert, my natural choice is to help organize GDG Boulder.

Last Thursday we met for Lightning talks and I/O Show and Tell.

We had two speakers for lightning talks:


Ian Douglas on SendGrid's partnership with Google.


Robert Kluin on Go.

After that, the floor was open for I/O Show and Tell. People who went to Google I/O brought their Android Wear watches, Cardboard, Android TV and devices with Android L preview. Everybody got to play with the new toys and share their thoughts. It was great fun.


Immersed in Cardboard

Next, on August 21, our meetup is going to feature mind-controlled robots. Join us!

Tuesday, June 24, 2014

GetSET App Inventor Workshop

I have always wanted to try App Inventor, so when my friend asked me if I'd be interested to teach a workshop on mobile programming for GetSET in Santa Clara, I immediately said yes. But thing got a bit more complicated when I decided to move to Colorado. I wasn't sure I would be able to run the workshop until I gave a talk at Mills College and met Renee there, who volunteered to be my TA.

Preparation

There were a few things we need to do to prepare for the workshop:

  1. Come up with a lesson plan
  2. Make sure we have Android devices
  3. Set up the workstations in the computer lab

Renee helped with all three. She was in Prof Ellen Spertus' class, who wrote a book on App Inventor. Ellen said that a good workshop format is demo-walkthough-explore, meaning that I would first code up an app in front of the whole class, explaning basic concepts. Then I would talk through a simple tutorial, with everyone following on their own computer. Finally it would be free time. The students could build whatever app they want. I thought this was a great format, getting more and more hands-on.

For the devices, Ellen lent me a few Nexus 7 tablets for the workshop, and Renee took care of the pick up and drop off. Finally, the compuer lab setup, which was the most complicated part. The workstations in the computer lab did not have wifi, so we had to use USB connection. GetSET was hosted by Santa Clara University, and only admins can install software. They asked me to send the list of software to be installed, but we needed to verify that the workstation could connect to the tablet after installation, so this could not be done remotely. Fortunaely Renee was able to drive down to Santa Clara for the initial installation. At that point we did not have access to the Mills tablets yet, so I borrowed a Nexus 7 from a friend to leave with the lab technician to repeat the installation across all the lab computers.

Workshop

Yesterday was the big day. The workshop was 9am to noon, and Renee and I showed up at 8am to get everything ready, with two more volunteers. To our horror, the tablets were not connecting to the workstations. We had a few theories: the driver was installed under admin mode, but we ran the workshop in user mode. The Nexus 7 for the initial set up was first generation, but the Mills tablets were newer. In any case, we could not use the workstations. We ended up using laptops from the volunteers, which meant 4 computers, 10 students. Not too bad a ratio.

With all that taken care of, we were ready to make apps! I demoed a raffle app that say the name of the winner:

Then we walked through Hello Purr together. There was much laughter as the tablets meowed.

Finally it was free time. The girls really enjoyed making apps, and did not want to leave when I announced a bathroom break in the middle! Different teams made different apps. Some added doodling capabilities to the cat, some made whack-a-mole, some made a pirate ship go get gold, some made text-to-speech for Spanish. It was great fun.

Next steps

I really enjoyed running this workshop. App Inventor is a great way to introduce basic programming concepts because it is lightweight to set up, and running apps on the kids' phones made it much more personal. I am not sure how to graduate them from App Inventor to Android, though. App Inventor only has a subset of the Android capabilities, and right now there is no way to convert an App Inventor project to Java+XML for Android, so they would have to rewrite the whole app if they wanted to step beyond the App Inventor functionalities.

Have you taught App Inventor before? What do you do when your students want to step up the game?

Adventures of the Mind

Public speaking has led to my different adventures for me. I gave an Ignite talk on hackathons at Google I/O 2012, sharing the stage with my former boss Peter Norvig. He was mentoring at Adventures of the Mind two years ago, which expanded to include a hackathon this year. He introduced me to the organizers to help run the hackathon, and that was how I got to mentor almost 200 brilliant high school students last week.

Hackathon


Kick off

I ran the hackathon with Mayank Jain, who has been organizing hackathons for high school students under Pilot. The Adventures of the Mind edition followed the same format as Pilot. Students can choose whatever language and platform they want, and go through with team formation, workshops, coding and pitching in 24 hours. I was not sure if that would work, especially since most of the students had no coding experience, and we only had 10 hours. Mayank assured me that previous events went quite well, and students were able to achieve quite a bit in a short amount of time. So we kept the Pilot format.

We started the hackathon with workshops. Cheston from mashery led an HTML5 one, and I led an Android one. We emailed instructions ahead of time asking the students to set up Xcode for iOS, Android Studio for Android, or a text editor for html/javascript on that laptop, and go through some tutorials. But naturally some people did not get a chance to do it, others struggled with installation problems. Many people had Java problems on their Windows computers, but I wasn't able to help them since I don't have Windows, and was not familiar with the failure cases.

After a while I made a radical decision: I switched to App Inventor. I was going to teach App Inventor the week after Adventures of the Mind anyway, so I already had a lesson plan. That went much more smoothly since the setup was much simpler: no install, no drivers. With that, the students were ready to write their own apps.

During the day the mentors went around to help the students, but we did not have enough mentors: only 5 mentors, and almost 200 kids. A batch of mentors dropped out last minute, and it was very difficult to find replacement on such short notice, especially since the hackathon was on a Tuesday. As a result I was only able to help a few teams, and felt very bad about the other teams that I could not help.

Despite all the struggles, the students really wowed us during the presentation. Here is a sample of their apps:


#feedtheworld: a website to direct recycling money to charities

Mind Scan: barcode scanner to exchange contact information

TravelPilot: recommend an itinerary for any destination

Overall the hackathon was rather well. A lot of students came up with wonderful ideas, learned to code, and demoed their app. But some teams could not finish their app because of technical hurdles, not knowing if their idea would be complicated to implement, and lack of guidance in general. If I were to do this again I would make sure we have a lot more mentors.

Sessions

The hackathon was just one part of this week-long program. There was a lot of interesting sessions. Here are some highlights:


Danny Oppenheimer on perception

Jini Kim on fixing healthcare.gov

Jennifer Shahade playing chess against 15 students at the same time

Nancy Segal and her twin research

Outings

We also got to visit quite a few interesting places in Los Angeles:


Autry Museum

LA Zoo

Gamble House. I love all the different wood texture.

JPL: we got to see the mission control room!

My first summer camp

It was really cool to hang out with all these smart kids, visit interesting places, and also get to know the other mentors. Come to think of it, this is actually my very first summer camp! I made many new friends, just like the students :)


With Nancy Segal

With Shahara Ahmad-Llewellyn

With Amy Tan, her husband, and their dogs.

Monday, June 2, 2014

NavigationDrawer creates fragment twice on rotation

I have an app with a navigation drawer. Everything was peachy until I rotated the screen, and noticed that the views are not preserving their states. Digging deeper I realized onCreateView for my fragment was called twice, the second time without saved state. What did I do wrong?

Logging

To isolate the problem, I created another app, generated the activity from the Android Studio template, and just added a single logging line:

@Override
public View onCreateView(LayoutInflater inflater, 
    ViewGroup container, Bundle savedInstanceState) {
  Log.i("sqisland", "onCreateView. Saved state? " 
    + (savedInstanceState != null));
  View rootView = inflater.inflate(
    R.layout.fragment_main, container, false);
  return rootView;
}

And indeed, onCreateView called twice on rotation, once with saved state and once without:

I/sqisland(24201): onCreateView. Saved state? true
I/sqisland(24201): onCreateView. Saved state? false

More logging

I enabled debugging on the FragmentManager to get more information:

FragmentManager.enableDebugLogging(true);

Time to read the tea leaves, and there are lots of it. I picked out the relevant part:

Saved state of NavigationDrawerFragment
               {41ea1a38 #0 id=0x7f07003e}
Saved state of PlaceholderFragment
               {41f027a8 #1 id=0x7f07003d}
Freeing fragment index NavigationDrawerFragment
                       {41ea1a38 #0 id=0x7f07003e}
Freeing fragment index PlaceholderFragment
                       {41f027a8 #1 id=0x7f07003d}

On rotation, the FragmentManager saves the states of the NavigationDrawerFragment and PlaceholderFragment, then frees them.

Instantiated fragment NavigationDrawerFragment
                     {41f90658 #0 id=0x7f07003e}
restoreAllState: active #0: NavigationDrawerFragment
                            {41f90658 #0 id=0x7f07003e}
Instantiated fragment PlaceholderFragment
                      {41f91018 #1 id=0x7f07003d}
restoreAllState: active #1: PlaceholderFragment
                            {41f91018 #1 id=0x7f07003d}

Then it instantiates new fragments for the new activity, and restores the saved states.

moveto CREATED: PlaceholderFragment
                {41f91018 #1 id=0x7f07003d}
moveto CREATED: NavigationDrawerFragment
                {41f90658 #0 id=0x7f07003e}

Commit: BackStackEntry{41ed3088}
   mName=null mIndex=-1 mCommitted=false
   Operations:
     Op #0: REPLACE PlaceholderFragment
                    {41f849c8 id=0x7f07003d}

Next it moves the fragments to onCreate. There is a commit because onCreate in NavigationDrawerFragment calls selectItem, which triggers the activity to instantiate a new PlaceholderFragment.

@Override
public void onNavigationDrawerItemSelected(int position) {
  // update the main content by replacing fragments
  FragmentManager fragmentManager = getSupportFragmentManager();
  fragmentManager.beginTransaction()
      .replace(R.id.container, 
               PlaceholderFragment.newInstance(position + 1))
      .commit();
}
Run: BackStackEntry{41ed3088}
OP_REPLACE: adding=PlaceholderFragment
                   {41f849c8 id=0x7f07003d} 
            old=NavigationDrawerFragment
                {41f90658 #0 id=0x7f07003e}

Now the FragmentManager runs the transcation, looking for fragment id 0x7f07003d for replacement, which is R.id.container. It skipped the NavigationDrawerFragment because the id does not match.

OP_REPLACE: adding=PlaceholderFragment
                   {41f849c8 id=0x7f07003d} 
            old=PlaceholderFragment
                {41f91018 #1 id=0x7f07003d}

Next it found the PlaceholderFragment which it restored from rotation, with id 0x7f07003d. Bingo! This is the fragment to replace.

remove: PlaceholderFragment
        {41f91018 #1 id=0x7f07003d} nesting=0
Freeing fragment index PlaceholderFragment
                      {41f91018 #1 id=0x7f07003d}
add: PlaceholderFragment
     {41f849c8 id=0x7f07003d}
Allocated fragment index PlaceholderFragment
                         {41f849c8 #1 id=0x7f07003d}
moveto CREATED: PlaceholderFragment
                {41f849c8 #1 id=0x7f07003d}

Replacing a fragment means removing it and adding it back. Afterwards, the FragmentManager moves the fragment into onCreate. This is why I see onCreate called twice, once with saved state, and once without.

The fix

A quick summary:

  1. Device was rotated. FragmentManager saves state for all fragments, then frees them.
  2. New activity. New fragments get instantiated, with states restored.
  3. Fragments get moved into onCreate.
  4. onCreate of NavigationDrawerFragment triggers a replace transaction.
  5. The replace transaction removes the restored PlaceholderFragment with a new one, which has no saved state.

To prevent the second PlaceholderFragment from being created, we need to tell NavigationDrawerFragment not to trigger a replace transaction when it is restored from saved state. I added a parameter to selectItem:

private void selectItem(
    int position, boolean fromSavedInstanceState) {
  mCurrentSelectedPosition = position;
  if (mDrawerListView != null) {
    mDrawerListView.setItemChecked(position, true);
  }
  if (mDrawerLayout != null) {
    mDrawerLayout.closeDrawer(mFragmentContainerView);
  }
  if (mCallbacks != null) {
    mCallbacks.onNavigationDrawerItemSelected(
      position, fromSavedInstanceState);
  }
}

When the selectItem is called from onCreate, fromSavedInstanceState = (savedInstanceState != null). When it is called from the ListView OnItemClickListener, it is false.

The activity only commit the replace transaction if needed:

@Override
public void onNavigationDrawerItemSelected(
    int position, boolean fromSavedInstanceState) {
  if (!fromSavedInstanceState) {
    // update the main content by replacing fragments
    FragmentManager fragmentManager = getSupportFragmentManager();
    fragmentManager.beginTransaction()
        .replace(R.id.container, 
                 PlaceholderFragment.newInstance(position + 1))
        .commit();
    }
}

Don't forget to change the signature of the interface:

public static interface NavigationDrawerCallbacks {
  void onNavigationDrawerItemSelected(
    int position, boolean fromSavedInstanceState);
}

Done, right? Not so fast. This indeed removed the second creation of PlaceholderFragment, but the action bar title is not updated. Here is why:

@Override
public void onAttach(Activity activity) {
  super.onAttach(activity);
  ((MainActivity) activity).onSectionAttached(
      getArguments().getInt(ARG_SECTION_NUMBER));
}

We are updating the title onAttach, but when the fragment is restored by the FragmentManager, it is attached before activity onCreate is called, so the title gets overwritten by the app title. Instead, we want to update the title after activity onCreate is called.

Time to consult the fragment lifecycle:

  1. onAttach(Activity) called once the fragment is associated with its activity.
  2. onCreate(Bundle) called to do initial creation of the fragment.
  3. onCreateView(LayoutInflater, ViewGroup, Bundle) creates and returns the view hierarchy associated with the fragment.
  4. onActivityCreated(Bundle) tells the fragment that its activity has completed its own Activity.onCreate().
  5. onViewStateRestored(Bundle) tells the fragment that all of the saved state of its view hierarchy has been restored.
  6. onStart() makes the fragment visible to the user (based on its containing activity being started).
  7. onResume() makes the fragment interacting with the user (based on its containing activity being resumed).

onActivityCreated it is:

@Override
public void onActivityCreated(Bundle savedInstanceState) {
  super.onActivityCreated(savedInstanceState);
  ((MainActivity) getActivity()).updateTitle(
    getArguments().getInt(ARG_SECTION_NUMBER));
}

I renamed onSectionAttached to updateTitle as well:

public void updateTitle(int number) {
  switch (number) {
    case 1:
      mTitle = getString(R.string.title_section1);
      break;
    case 2:
      mTitle = getString(R.string.title_section2);
      break;
    case 3:
      mTitle = getString(R.string.title_section3);
      break;
  }
}

Yay, everything works!

Did you read all the tea leaves with me? Wow. Go get yourself a nice drink. You deserve it.