1.5M ratings
277k ratings

See, that’s what the app is perfect for.

Sounds perfect Wahhhh, I don’t wanna

Dragging, Scaling, Rotating on April Fools Day

The April Fools 2016 project was executed and finalized on a very tight deadline. The mobile platforms (Android, iOS) had only 2 weeks to ship a finalized, polished implementation from scratch. Originally, we planned on doing a full-blown custom election, where users would, themselves, be able to run as a candidates in the election, create their own campaign and be featured on a top Candidates page. However, we had to scope down the project as much as we could. Development had to occur while scoping the project and mocks were incomplete.

One of the biggest contributors to the success of the AF project were the new creator tools we provided. The campaign posters and campaign endorsements.

While creating the campaign poster creator on Android, there were 4 main actions to implement for users:
1) Adding items to the poster
2) Dragging items
3) Scaling items
4) Rotating items
* “items” refers to text and stickers

I decided to structure the implementation as follows:
image of activity -> layoutView -> canvas, with imageViews and textviews being added to the canvas, and the color picker being a separate ViewGroup.

image


Adding Text and Stickers

A custom CanvasLayout subclassing FrameLayout was the main drawing board. I added buttons in activity layout with click with the CanvasLayout through CanvasLayout.changeBackgroundTapped(), CanvasLayout.stickerTapped() or CanvasLayout.addTextTapped(). When the user taps the add-text button, CanvasLayout dynamically adds a TextView to the layout.

public void addTextItem() {
  TextView textView = new TextView();
  textView.color = ...; 
  textView.properties = ...;
  textView.setFocusable(true);
}

And similar for stickers, except adding ImageViews with selected drawables to the CanvasLayout view.

image

Dealing with Touches

Unfortunately, there’s no simple way to scale or rotate views on Android. I created a custom AFTextView subclassing TextView and a custom AFStickerView subclassing ImageView. These views intercept touch events and figure out what the user is trying to do. A simple overview of what the logic in onTouch() looked like:

@Override
public onTouch(MotionEvent ev) {
  if (ev == TOUCH_DOWN) {
    // save the x & y position of the user's touch down!
  } elseif (ev == TOUCH_UP) {
    // reset the variables, the user just lifted their finger and doesn't want to do anything
  } elseif (ev == TOUCH_MOVE) {
    // we're either dragging, scaling or rotating, figure out which one!
  }
}

(For information on each event type, refer to MotionEvent reference docs)

As you can probably tell, the most difficult part is figuring out what to do on ACTION_MOVE. I started off with scaling and dragging. The biggest difference between the two is: you need two fingers for scaling, you need one finger for dragging. So let’s keep track of each finger with an ID, mPointer1, mPointer2 and initialize them to -1. Then I checked whether we have one or two fingers down when I received a ACTION_MOVE event. Our onTouch() logic now is as follows:

@Override
public onTouch(MotionEvent ev) {
  if (ev == ACTION_DOWN) {
    mPointer1 = ev.getPointerId();
	// ... save the x & y position of first finger's touch down point
  } else if (ev == ACTION_UP) {
    // reset the variables, the user just lifted their finger and doesn't want to do anything
    mPointer1 = -1;
    mPointer2 = -1;
  } else if (ev == ACTION_MOVE) {
    if (mPointer2 == -1) {
      // only one finger down, we are dragging
    } else {
      // we're either scaling or rotating!
    }
  } else if (ev == ACTION_POINTER_DOWN) {
    mPointer2 = ev.getPointerId();
    // ... save x & y of second finger's touch down point
  }
}

We have a new event we are intercepting, ACTION_POINTER_DOWN. This event occurs when a second finger touches down on the view. We now know whether we are dragging or doing something else.

image

Unfortunately, there is another problem. I want to be able to scale in the X direction (horizontally) and the Y direction (vertically) without caring about the aspect ratio.

It becomes impossible to figure out what the user is trying to do now. I decided the to intercept the touch events on the main CanvasLayout. On the CanvasLayout, if a rotation action is detected, a rotation is applied to the selected view. This brought us to the final onTouch() logic on the AFTextView and CanvasLayout:

AFTextView.java

@Override
public onTouch(MotionEvent ev) {
  if (ev == ACTION_DOWN) {
    mPointer1 = ev.getPointerId();
    ...
  } else if (ev == ACTION_UP) {
    mPointer1 = -1;
    mPointer2 = -1;
    ...
  } else if (ev == ACTION_MOVE) {
    if (mPointer2 == -1) {
      // only one finger down, we are dragging!
    } else {
      // two fingers down, we're  scaling!
    }
  } else if (ev == ACTION_POINTER_DOWN) {
    mPointer2 = ev.getPointerId();
    ...
  }
}

CanvasLayout.java

@Override
public onTouch(MotionEvent ev) {
  if (ev == ACTION_DOWN) {
    mPointer1 = ev.getPointerId();
    ...
  } else if (ev == ACTION_POINTER_DOWN) {
    mPointer2 = ev.getPointerId();
    ...
  } else if (ev == ACTION_MOVE) {
    // user is moving their first finger in the rotation gesture! 
    // calculate the angle between the second finger and this finger before & after this move
  } else if (ev == ACTION_UP) {
    // rotation is finished
  }
  return true;
}
image

Great, now we have all the logic in place to know when we need to scale, drag, and rotate our views. The details of how to manipulate the view accordingly warrants another post all together. Rotation in particular had me brushing up on my linear algebra to calculate the angle of change using 4 coordinates (2 coordinates for the position of the fingers before the rotation, 2 coordinates for the position of the fingers after rotation):

image

Another interesting (read: annoying) thing to deal with was the fact that what would be considered rotating +X degrees in the regular cartesian plane (counter-clockwise) is considered rotating -X degrees in Android views rotation. Sigh…

Anyway, after ironing out some bugs and use some touchSlops to make touches more realistic, the interaction with elements was looking great.

At the end of April Fools day, there had been tens of thousands of campaign posters created on Mobile. I think it’s safe to say users had a good time adding, dragging, scaling and rotating views for their favorite lizards.

image

- @vanillaburritos , Android Engineer @ Tumblr

decision2016 aprilfools2016 android effectiveandroid ontouch wretchedtooth mop timefordeborah rickforall