How to create a Rotary Dialer application for Android ?

Classically, Android users use stock dial to make calls. There are also many alternatives available on Google Play Store for stock dial. But, some users like add a touch of retro to their smartphone and seek for an old school rotary dialer.

If old guys are nostalgic about old rotary dialers, young guys sometimes doesn’t know what it is. So what is a rotary dialer ?

Old Phone with Rotary Dialer

A rotary dialer was used on old phones to dial digits and call numbers.

Create an Android dialer with that retro touch is an interesting subject and more a good way to learn how to create a specific view on Android.

 

1. Images

First, we need to create some images to represent a rotary dialer. We need the following images :

  • A background image of the rotary dialer with numbers and letters. This image must be static during rotary dialer animation.
  • A top image with the tick. This image must be also static during animation.top
  • Last image will be used to be rotated during the rotary dialer animation.

dialer

 

These images must be in the drawable directory of resources in your project.

 

2. Draw the Rotary Dialer

To draw the rotary dialer, we need to create a specific view named RotaryDialerView that must extend the class View :


public class RotaryDialerView extends View {

private final Drawable rotorDrawable;

public RotaryDialerView(Context context) {
  this(context, null);
}

public RotaryDialerView(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
}

public RotaryDialerView(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  rotorDrawable = context.getResources().getDrawable(R.drawable.dialer);
}

@Override
protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);
  canvas.save();

  rotorDrawable.setBounds(0, 0, rotorDrawable.getIntrinsicWidth(),
  rotorDrawable.getIntrinsicHeight());

  rotorDrawable.draw(canvas);

  canvas.restore();
 }
}

We load the dialer resource that is the only drawable with a dynamic behaviour. Here, we just display it.

The static images are displayed in the layout of the activity :


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

  <RelativeLayout
   android:layout_width="fill_parent"
   android:layout_height="fill_parent" >

    <View
     android:layout_width="320dip"
     android:layout_height="320dip"
     android:layout_centerInParent="true"
     android:background="@drawable/bg" />

    <com.ssaurel.rotarydialer.RotaryDialerView
     android:id="@+id/rotary_dialer"
     android:layout_width="320dip"
     android:layout_height="320dip"
     android:layout_centerInParent="true" />

    <View
     android:layout_width="320dip"
     android:layout_height="320dip"
     android:layout_centerInParent="true"
     android:background="@drawable/top" />
  </RelativeLayout>

</LinearLayout>

We can know test the display and see the result :

Rotary Dialer screenshot 1

 

3. Animate the Rotary Dialer

With rotary dialer displayed, we have to animate the dialer when user makes a rotate touch on the screen. How to proceed ?

We’re going to listen touch events on RotaryDialerView. Then, we must make a litte bit of trigonometry to determinate the rotation angle. Goal is to use that angle to draw the dialer with a specific rotation of that angle.

During MotionEvent.ACTION_MOVE, we set the new angle of rotation and choose to redraw the view. Note that you must add a rotation of that angle when you draw the dialer. When MotionEvent.ACTION_UP is fired, we can get the actual position of the dialer compared to the tick and so get the number dialed. We fire that event with a specific method.

Code of RotaryDialerView is updated like this :


public class RotaryDialerView extends View {

  private float rotorAngle;
  private final Drawable rotorDrawable;
  private final int r1 = 50;
  private final int r2 = 160;
  private double lastFi;

  public RotaryDialerView(Context context) {
    this(context, null);
  }

  public RotaryDialerView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public RotaryDialerView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    rotorDrawable = context.getResources().getDrawable(R.drawable.dialer);
  }

  private void fireDialListenerEvent(int number) {
    // TODO fire dial event
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int availableWidth = getRight() - getLeft();
    int availableHeight = getBottom() - getTop();

    int x = availableWidth / 2;
    int y = availableHeight / 2;

    canvas.save();

    rotorDrawable.setBounds(0, 0, rotorDrawable.getIntrinsicWidth(),
    rotorDrawable.getIntrinsicHeight());

    if (rotorAngle != 0) {
      canvas.rotate(rotorAngle, x, y);
    }

    rotorDrawable.draw(canvas);

    canvas.restore();
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    final float x0 = getWidth() / 2;
    final float y0 = getHeight() / 2;
    float x1 = event.getX();
    float y1 = event.getY();
    float x = x0 - x1;
    float y = y0 - y1;
    double r = Math.sqrt(x * x + y * y);
    double sinfi = y / r;
    double fi = Math.toDegrees(Math.asin(sinfi));

    if (x1 > x0 && y0 > y1) {
      fi = 180 - fi;
    } else if (x1 > x0 && y1 > y0) {
      fi = 180 - fi;
    } else if (x0 > x1 && y1 > y0) {
      fi += 360;
    }

    switch (event.getAction()) {
    case MotionEvent.ACTION_MOVE:
      if (r > r1 && r < r2) {
        rotorAngle += Math.abs(fi - lastFi) + 0.25f;
        rotorAngle %= 360;
        lastFi = fi;
        invalidate();
        return true;
      }

    case MotionEvent.ACTION_DOWN:
      rotorAngle = 0;
      lastFi = fi;
      return true;

    case MotionEvent.ACTION_UP:
      final float angle = rotorAngle % 360;
      int number = Math.round(angle - 20) / 30;

      if (number > 0) {
        if (number == 10) {
          number = 0;
        }

        fireDialListenerEvent(number);
      }

      rotorAngle = 0;

      post(new Runnable() {
        public void run() {
          float fromDegrees = angle;
          Animation anim = new RotateAnimation(fromDegrees, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
          anim.setInterpolator(AnimationUtils.loadInterpolator(getContext(), android.R.anim.decelerate_interpolator));
          anim.setDuration((long) (angle * 5L));
          startAnimation(anim);
        }
      });

      return true;
    default:
      break;
  }

  return super.onTouchEvent(event);
 }
}

 

Here, you must note the animation launched when user ends his touch on the screen. Goal is to replace the dialer to its start position with a rotate animation. The animation is defined with the standard Android android.R.anim.decelerate_interpolator .

 

4. Add a number area

Last step is to add a number area to display digits dialed. We update the layout like this :


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

  <RelativeLayout
   android:layout_width="fill_parent"
   android:layout_height="wrap_content" >

     <LinearLayout
      android:layout_width="fill_parent"
      android:layout_height="66dp"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:layout_marginBottom="10dp"
      android:layout_marginLeft="10dp"
      android:layout_marginRight="10dp"
      android:layout_marginTop="10dp"
      android:orientation="horizontal" >

     <EditText
      android:id="@+id/digits"
      android:layout_width="0dip"
      android:layout_height="66dp"
      android:layout_weight="1"
      android:background="@drawable/btn_dial_textfield"
      android:focusableInTouchMode="false"
      android:freezesText="true"
      android:inputType="phone"
      android:maxLines="1"
      android:paddingLeft="45dip"
      android:scrollHorizontally="true"
      android:textColor="@color/dialer_button_text"
      android:textSize="28sp" />

     <ImageButton
      android:id="@+id/backspace"
      style="@android:style/Widget.Button.Inset"
      android:layout_width="wrap_content"
      android:layout_height="66dp"
      android:background="@drawable/btn_dial_delete"
      android:gravity="center"
      android:src="@drawable/ic_delete_phone_number" />
    </LinearLayout>

    <ImageView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_alignParentLeft="true"
     android:layout_alignParentTop="true"
     android:layout_marginLeft="18dp"
     android:layout_marginTop="25dp"
     android:src="@drawable/ic_dial_number" />
  </RelativeLayout>

  <RelativeLayout
   android:layout_width="fill_parent"
   android:layout_height="fill_parent" >

   <View
    android:layout_width="320dip"
    android:layout_height="320dip"
    android:layout_centerInParent="true"
    android:background="@drawable/bg" />

   <com.ssaurel.rotarydialer.RotaryDialerView
    android:id="@+id/rotary_dialer"
    android:layout_width="320dip"
    android:layout_height="320dip"
    android:layout_centerInParent="true" />

   <View
    android:layout_width="320dip"
    android:layout_height="320dip"
    android:layout_centerInParent="true"
    android:background="@drawable/top" />
  </RelativeLayout>

</LinearLayout>

 

Our Rotary Dialer application now looks like this :

Rotary Dialer screenshot 2

 

5. Fire dial event

We have a number are and so we must fill it with dialed number. To make that, we must implement the fireDialListenerEvent seen previously in the class RotaryDialerView.

We define a DialListener interface with onDial(int number) method. Class interested by digits dialed should implement that class and then register to the RotaryDialerView. We add the following code to RotaryDialerView :


public interface DialListener {
  void onDial(int number);
}

private final List<DialListener> dialListeners = new ArrayList<DialListener>();

// ...

public void addDialListener(DialListener listener) {
  dialListeners.add(listener);
}

public void removeDialListener(DialListener listener) {
  dialListeners.remove(listener);
}

private void fireDialListenerEvent(int number) {
  for (DialListener listener : dialListeners) {
    listener.onDial(number);
  }
}

6. Display number dialed

To display number dialed, we must register with our main activity as a DialListener to RotaryDialerView and display digit dialed :


public class RotaryDialer extends Activity {
  private EditText digits;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    RotaryDialerView rotaryDialerView = (RotaryDialerView) findViewById(R.id.rotary_dialer);
    digits = (EditText) findViewById(R.id.digits);
    rotaryDialerView.addDialListener(new RotaryDialerView.DialListener() {
      public void onDial(int number) {
        digits.append(String.valueOf(number));
      }
    });

    ImageButton backspace = (ImageButton) findViewById(R.id.backspace);
    backspace.setOnClickListener(new View.OnClickListener() {
      public void onClick(View v) {
        if (digits.getText().toString().length() > 0) {
          digits.getText().delete(digits.getText().length() - 1,
          digits.getText().length());
        }
      }
    });

    backspace.setOnLongClickListener(new View.OnLongClickListener() {
      public boolean onLongClick(View v) {
        if (digits.getText().toString().length() > 0) {
          digits.getText().clear();
          return true;
        }

        return false;
      }
    });

    digits.setOnClickListener(new View.OnClickListener() {
      public void onClick(View v) {
        makeCall();
      }
    });

}

  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_CALL) {
      makeCall();
      return true;
    }

    return super.onKeyDown(keyCode, event);
  }

  private void makeCall() {
    if (digits.getText().length() > 0) {
      String toDial = "tel:" + digits.getText().toString();
      startActivity(new Intent(Intent.ACTION_CALL, Uri.parse(toDial)));
    }
  }

}

Here, we have also added the possibility to send Intent.ACTION_CALL to launch a call with the dialed number.

7. Conclusion

To conclude, you can see our Rotary Dialer application in action in that demo video :

 

You have noticed that it’s an extended version of our Rotary Dialer application created during this tutorial. A lot of features have been added but the core feature is exactly the same. You can try the application here from Google Play Store : https://play.google.com/store/apps/details?id=com.ssaurel.rotarydialer

Now, it’s your turn to play. Don’t hesitate to create your own and add new exciting features :).

8. Extra

A live coding video to create Rotary Dialer application is also available here on Youtube :

 

 

Comments

  1. By s.saurel

  2. By moham

  3. By Lesli

  4. By user732

Leave a Reply