Thursday, December 17, 2009

MusicDroid - Audio Player Part III

Introduction

In part 1 and 2 we created a simple media player, but there was no way to do anything except play a selected song. Now we must create some kind of interface to control the music. I am not a great graphical designer so the controls might be a little bland for now, but it demonstrates how to control layouts using RelativeLayouts and ImageViews, as well as animating the image views. It also demonstrates how to create a transparent activity.

Click here to download the full source.

Layout for 4-way controls

For the controls menu that is pictured we will use 4 image views, in 2 relative layouts. Here is controls.xml:


      <?xml version="1.0" encoding="utf-8"?>
      <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent">

       

          <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

              android:layout_width="170dip"

              android:layout_height="170dip"

              android:layout_centerVertical="true"

              android:layout_centerHorizontal="true">

         

              <ImageView id="@+id/pause"

                  android:layout_alignParentTop="true"

                  android:layout_centerHorizontal="true"

                  android:layout_width="50dip"

                  android:layout_height="50dip"

                  android:src="@drawable/menupause" />

         

              <ImageView id="@+id/skipb"

                  android:layout_alignParentLeft="true"

                  android:layout_centerVertical="true"

                  android:layout_width="50dip"

                  android:layout_height="50dip"

                  android:src="@drawable/menuskipb" />

             

              <ImageView id="@+id/skipf"

                  android:layout_alignParentRight="true"

                  android:layout_centerVertical="true"

                  android:layout_width="50dip"

                  android:layout_height="50dip"

                  android:src="@drawable/menuskipf" />

         

              <ImageView id="@+id/stop"

                  android:focusable="true"

                  android:layout_alignParentBottom="true"

                      android:layout_centerHorizontal="true"

                  android:layout_width="50dip"

                  android:layout_height="50dip"

                  android:src="@drawable/menustop" />

       

          </RelativeLayout>

      </RelativeLayout>

The first Relative layout fills the full width and height of the screen so that we can center the second relative layout which is 170x170 pixel square which will hold our buttons.

To center a View in the horizontal we use "android:layout_centerHorizontal", and to center in the vertical we use "android:layout_centerVertial". With RelativeLayouts the order in which you list the items does not matter, you place them using these alignment techniques.

So, we create 4 ImageViews here. The first is the pause button which is centered horizontally and aligned at the top vertically. The bottom button is the stop button, which is also centered horizontally, but vertically positioned at the bottom. And of course skip forward and back are both centered vertically, but aligned to the right and left.

Animation

Animation xml files are placed in a folder (you will have to create), "res/anim". Animation XML files are very easy to use to translate, scale, rotate, adjust alpha, and create a tint. There are currently some limitations with the animation functionality unfortunately. The 2 big issues right now is that the AnimationListener functionality does not work. This means that there is no way to tell when an animation is finished running. Also, there is a property called "fillAfter" which does not work. This property, when set, will not redraw the object when an animation is finished. For example if you set fillAfter to true, and then rotate an object, when the object is done rotating it should stay at the new angle. Unfortunately, it will always redraw the original object when the animation is finished.

For this example we will do just a very simple scale animation, so that when you hit a control the icon gets large and then scales smaller. Please feel free to mess around with this, remember you can scale, translate, rotate, adjust alpha, and tint objects. If you come up with something really cool let us know in the forum!

Here is our simple scale animation, which we can create in "res/anim" and call selected.xml:

 


      <?xml version="1.0" encoding="utf-8"?>

      <set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false" android:fillAfter="true">

       

          <scale

              android:fromXScale="1.5"

              android:fromYScale="1.5"

              android:toXScale="1"

              android:toYScale="1"

              android:duration="600"

              android:pivotX="50%"

              android:pivotY="50%"

              android:fillAfter="true"

              />

         

      </set>

So lets talk about what we are saying here. We are saying that when this animation starts we want to have the image scaled to 1.5 in the X and Y, and when the animation is done we want it to scale back to 1 in the X and Y. The duration states that this animation will take 600 ms. The pivotX and pivotY describe the points at which to scale around. Ie, you could have the left edge stay where it is and have it expand to the right, but this case we just specifiy to scale from the center.

This is a simple animation that works for the controls that we have designed, but lets say after the scale you wanted to do another animation, for example fade the object away. Although that does not make sense for these controls, I thought it would be good information to include. So to create another action that will execute after the scale you can create another tag, alpha in this example, and set the "startOffset" equal to the ammount of time that it took for the first animation to complete (or more or less if you want to have it start sooner or delay). Here is an example of an alpha animation that could be inserted after the scale animation above:

 


      <alpha android:fillAfter="true"

          android:startOffset="600"

          android:fromAlpha="1.0"

          android:toAlpha="0.0"

          android:duration="400" />

I recommend taking a look at the different types of animations in the API demos to see more.

Transparency using Themes and Colors

To make this activity transparent we will use a custom theme. All that we need to do is create a custom theme and set the background color to a color that we must define. This is very easy because colors are 8 digit hexidecimal numbers which include the alpha value. The last 6 digits are the RGB values, much like an HTML color. The first 2 digits is the alpha value, and that is what we must set to get a translucent background.

To create a theme you create a file called "styles.xml" in the "res/values" folder. It is not required to be named styles.xml, but that is the best practice. Here is the styles.xml file we will use:

 


      <?xml version="1.0" encoding="utf-8"?>

      <resources>

       

          <style name="Theme" parent="android:Theme.Dark"></style>

       

          <style name="Theme.Transparent">

              <item name="android:windowBackground">@drawable/transparent_background</item>

              <item name="android:windowNoTitle">true</item>

          </style>

      </resources>

As you can see the theme we created is a child to the standard dark theme, this means that it will inherit its properties. All we need to do is set the background color and hide the window's title. The color referred to here as "@drawable/transparent_background" is defined in colors.xml in the "res/values" folder:

 


      <?xml version="1.0" encoding="utf-8"?>

      <resources>

           <drawable name="transparent_background">#a0000000</drawable>

      </resources>

Notice that there is no color defined in the last 6 digits, we just define the alpha value to be A0 out of FF (160 out of 255 for those that don't automatically convert hex in their heads).

Now that we have this custom theme setup we have to somehow tell our ControlsMenu Activity (which we'll create on the next page) to use this custom theme. That is done in the AndroidManifest.xml file, here is the entry we will add for this new Activity:

 


      <activity class=".ControlsMenu" android:label="@string/app_name"

                  android:theme="@style/Theme.Transparent" />

No we have created all the XML we need, so lets put it to use in the next page and create the ControlsMenu Activity...

The Activity

This is a very simple Activity. It's not very smart, it's not aware of what is being played (yet). It's only role is to display 4 images, and when a user hits a key corresponding to one of them it needs to send a command to the service using the Interface from part 2 and trigger an animation to start.

Because this is a simple Activity we'll show the whole thing and discuss after:

 


      public class ControlsMenu extends Activity {

       

          private ImageView pauseImage;

          private ImageView skipbImage;

          private ImageView skipfImage;

          private ImageView stopImage;

          private MDSInterface mpInterface;

       

          @Override

          public void onCreate(Bundle icicle) {

              super.onCreate(icicle);

       

              setContentView(R.layout.controls);

       

              pauseImage = (ImageView) findViewById(R.id.pause);

              skipbImage = (ImageView) findViewById(R.id.skipb);

              skipfImage = (ImageView) findViewById(R.id.skipf);

              stopImage = (ImageView) findViewById(R.id.stop);

       

              this.bindService(new Intent(this, MDService.class), null, mConnection,

                      Context.BIND_AUTO_CREATE);

          }

       

          @Override

          public boolean onKeyUp(int keyCode, KeyEvent event) {

              try {

                  switch (keyCode) {

                  case KeyEvent.KEYCODE_DPAD_UP:

                      handleAnimation(pauseImage);

                      mpInterface.pause();

                      break;

                  case KeyEvent.KEYCODE_DPAD_LEFT:

                      handleAnimation(skipbImage);

                      mpInterface.skipBack();

                      break;

                  case KeyEvent.KEYCODE_DPAD_RIGHT:

                      handleAnimation(skipfImage);

                      mpInterface.skipForward();

                      break;

                  case KeyEvent.KEYCODE_DPAD_DOWN:

                      handleAnimation(stopImage);

                      mpInterface.stop();

                      break;

                  }

       

              } catch (DeadObjectException e) {

                  Log.e(getString(R.string.app_name), e.getMessage());

              }

       

              return super.onKeyUp(keyCode, event);

          }

       

          public void handleAnimation(View v) {

              v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.selected));

          }

       

          private ServiceConnection mConnection = new ServiceConnection() {

              public void onServiceConnected(ComponentName className, IBinder service) {

                  mpInterface = MDSInterface.Stub.asInterface((IBinder) service);

              }

       

              public void onServiceDisconnected(ComponentName className) {

                  mpInterface = null;

              }

          };

      }

First in the onCreate() function we initialize all of our private variables for the ImageView objects. Then we attempt to bind to our service we created. Once this binds it will call onServiceConnected down on line 74 which will initialize our interface to the service.

The user input is handled in the onKeyUp(int, KeyEvent) function on line 41. We just create a switch with the KeyCode that was pressed. If a relavent key is pressed we initiate an animation with the handleAnimation(View) function (line 69) and then send the appropriate command to the service.

You can see on line 70 that it's very easy to start playing an animation on a view. You just call it's startAnimation(Animation) function passing in an Animation as it's only argument. We use AnimationUtils to get the animation from it's resource id (R.anim.selected).

Now, all that is left is to launch this new Activity when a user selects a song, here is the new onListItemClick() from the MusicDroid ListView:

 


      @Override

      protected void onListItemClick(ListView l, View v, int position, long id) {

          try {

              Intent intent = new Intent(this, ControlsMenu.class);

              startSubActivity(intent,0);

              mpInterface.playFile(position);

          } catch(DeadObjectException e) {

              Log.e(getString(R.string.app_name), e.getMessage());

          }

      }

So playing with these new controls you can see that they are not ideal, because the controls have no idea what's going on...in part 4 we will attempt to change that. We will also add ID3 support so that we can also display the Artist/Song information, and a progress indicator on the song playing....so check back!

2 comments: