Showing posts with label Animators. Show all posts
Showing posts with label Animators. Show all posts

Wednesday, September 16, 2015

MyNotes: ImageSwitcher

 

As I mentioned in my last post I added ImageSwitchers to the NoteEditOptionsBar. ImageSwitchers are a type of ViewAnimator.  Where a ViewFlipper animates swapping views, the ImageSwitcher does the same thing between images. 

This allows me to smoothly change the image displayed for some of icons based on the state of the note.  I have two icons that this applies to: save and cancel/delete.   For the save icon, I only want it available when the note has been edited and contains text.  In all other cases I want it gone.

I also want the delete/cancel icon to differ based on note state.  If the note is persisted, then the delete icon shows.  Otherwise, I want the cancel icon to show. 

Both of the problems can be solved in a lot of different ways.  For instance, I could define two independent views for the cancel and delete.  The views would share the same space claim on the UI, but only show one at a time.   I could then apply a ViewFlipper to transition between the views.  That was actually how I first solved the problem, but it seemed like overkill.  I has a nested layout didn’t feel necessary and I didn’t like having two icons that performed the same click behavior, but only differed by the image displayed.

I could also implement it as a single ImageView and then changed the source image as needed.   I wasn’t happy with this because there was an abrupt transition between the images.  I could add animation logic around, but again it felt like overkill and seemed fragile.

Ultimately, that was I why I decided to move to the ImageSwitcher.  It givesme to have the best of both worlds.  I (theoretically) have a single ImageView that smoothly animates between images. 

The theoretically is because the ImageSwitcher uses a factory to create Views as needed.  I think it actually create multiple Views under the covers, but I from my code’s perspective I can treat it as a single View and leave the messy details to Google.

Step 1: Define the ImageSwitcher in XML

   1:  
   2:     <ImageSwitcher
   3:         android:id="@+id/component_note_edit_options_bar_purge"
   4:         style="@style/EditToolbarOption"
   5:         android:layout_width="wrap_content"
   6:         android:layout_height="wrap_content"
   7:         android:layout_alignParentLeft="true"
   8:         android:inAnimation="@anim/fade_in"
   9:         android:outAnimation="@anim/fade_out" />

In the XML, define the ImageSwitcher like any other View.  It has two special attributes inAnimation and outAnimation to define any special animations around switching the images.   In this case I want to fade out the old image and fade in the new image.


Step 2: Create a View Factory in Code



   1:  
   2:     /**
   3:      * The view factory to support the {@link ImageSwitcher}s. This will create a basic {@link
   4:      * ImageView} when making the view.
   5:      */
   6:     final ViewSwitcher.ViewFactory mViewFactory = new ViewSwitcher.ViewFactory() {
   7:  
   8:         public View makeView() {
   9:             // Create a new ImageView set it's properties
  10:             ImageView imageView = new ImageView(getContext());
  11:             imageView.setLayoutParams(new ImageSwitcher.LayoutParams(LayoutParams.MATCH_PARENT,
  12:                                                                      LayoutParams.MATCH_PARENT));
  13:             return imageView;
  14:         }
  15:     };

This is a mandatory bit for the ImageSwitcher.  Use the makeView method to create the views you want to be handled by the ImageSwitcher.   In my case,  I want simple, no frills ImageViews so I create one and return it.


Step 3: Assign the factory



   1: void initialize(Context context) {
   2:     View rootView = LayoutInflater.from(context)
   3:                                   .inflate(R.layout.component_note_edit_options_bar, this,
   4:                                            true);
   5:     ButterKnife.inject(this, rootView);
   6:     mPurgeImageSwitcher.setFactory(mViewFactory);
   7:     mSaveImageSwitcher.setFactory(mViewFactory);
   8:  
   9: }

Assign the factory created in step 2 to the ImageSwitchers.


Step 4: Swap the images



   1: /**
   2:   * Change the Purge button imagery based on the note. If the note is currently persisted, then
   3:   * show the delete image. Otherwise, show the cancel image.
   4:   *
   5:   * @param note
   6:   *         the note to use for determining the purge button UI.
   7:   */
   8:  private void updatePurgeButtonImagery(@NonNull Note note) {
   9:      if (note.getId() == Note.NOT_STORED) {
  10:          mPurgeImageSwitcher.setImageResource(R.drawable.cancel_background);
  11:      }
  12:      else {
  13:          mPurgeImageSwitcher.setImageResource(R.drawable.delete_note_background);
  14:      }
  15:  }

To change the image shown in the ImageSwitcher call setImageDrawable, setImageResource, or setImageUri.





Commits


The commits with these changes can be found on github in https://github.com/fsk-software/mynotes/commit/0558ef8dfec2ecebe3199cd4b8ecc5266f9c22cf and https://github.com/fsk-software/mynotes/commit/e2312a77dfd9247641ab894cb678953dd986216b.

Wednesday, July 8, 2015

MyNotes: Color Shifting

In my last two entries I covered the Shared Element Transition and the Translation Animations.  In this entry I am going to cover the last of the main edit note animations: The color shift.


This animation happens when the user changes the color of the note.  It is a subtle animation, but adds a nice polish to the UI.
This is a very simple animation that slowly changes from color A to color B over a time interval.  This animation can be found in the BackgroundAnimator class inside the common library.
BackgroundAnimator
/**
 * A helper class to help with background drawable animators
 */
public class BackgroundAnimatorHelper {

    /**
     * Animate a transition from one color to the next.
     *
     * @param view
     *         The view to animate.
     * @param fromColor
     *         the initial color for the animation.
     * @param toColor
     *         the final color for the animation.
     * @param duration
     *         The time in milliseconds for the animation.
     * @param animatorListener
     *         The listener for the animation.
     */
    public static void crossBlendColors(@NonNull View view, int fromColor, int toColor,
                                        int duration, Animator.AnimatorListener animatorListener) {

        view.animate().cancel();

        ObjectAnimator animator = ObjectAnimator
                .ofObject(view, "backgroundColor", new ArgbEvaluator(), fromColor, toColor);
        if (animatorListener != null) {
            animator.addListener(animatorListener);
        }
        animator.setDuration(duration);
        animator.start();
    }


    /**
     * Animate a transition from one color resource to the next.
     *
     * @param view
     *         The view to animate.
     * @param fromColorResource
     *         the initial color resource for the animation.
     * @param toColorResource
     *         the final color resource for the animation.
     * @param duration
     *         The time in milliseconds for the animation.
     * @param animatorListener
     *         The listener for the animation.
     */
    public static void crossBlendColorResource(@NonNull View view, int fromColorResource,
                                               int toColorResource, int duration,
                                               Animator.AnimatorListener animatorListener) {

        Resources resources = view.getResources();
        int fromRgb = resources.getColor(fromColorResource);
        int toRgb = resources.getColor(toColorResource);
        crossBlendColors(view, fromRgb, toRgb, duration, animatorListener);
    }


There are two methods here, but the meat of the animation is here:

    public static void crossBlendColors(@NonNull View view, int fromColor, int toColor,
                                        int duration, Animator.AnimatorListener animatorListener) {

        view.animate().cancel();

        ObjectAnimator animator = ObjectAnimator
                .ofObject(view, "backgroundColor", new ArgbEvaluator(), fromColor, toColor);
        if (animatorListener != null) {
            animator.addListener(animatorListener);
        }
        animator.setDuration(duration);
        animator.start();
    }

All this method does is create an ObjectAnimator with the ArgbEvaluator. The ArgEvaluator is a special interpolator that does the work of the color shift.  All the animator does is use the ArgbEvalauator to change the background color.

The other method, crossBlendColorResources, is a convenience method that figures out the argb value for  a color resource and then called the crossBlendColors method to perform the color shift.

Commits


These changes can be found at commits https://github.com/fsk-software/mynotes/commit/a0632503c5fabf74a31cca091d7975419d002328 and https://github.com/fsk-software/mynotes/commit/ec427c6a5684220836f823824d58a61cd2aea47b.

Wednesday, July 1, 2015

MyNotes: Edit Note Toolbar Slide


The EditNoteActivity toolbar comes in two flavors, the main toolbar and the color picker toolbar.  These are implemented as separate fragments, NoteEditMainToolbarFragment and the NoteEditColorPickerFragment respectively.

Here are the pictures:

Main Toolbar:
Screenshot_2015-06-17-22-42-51

Color Picker:
Screenshot_2015-06-17-22-42-47

For the phone layout this toolbar sets right under the notification bar.  I am still undecided about the edit note tablet layout, so I will cover that under another entry.
Anyway, for the phone I want the toolbar to slide down when entering and slide up when exiting.
In practice it looks like this:

FractionalFrameLayout

I am making these animations work by specifying a custom animation on the fragment transactions:
    private void showFragment(@NonNull Fragment fragment, @NonNull String tag) {
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        transaction.setCustomAnimations(R.animator.slide_down, R.animator.slide_up);
        transaction.replace(R.id.activity_single_note_header_container, fragment, tag);
        transaction.commit();
    }

In this case the enter animation in the R.animator.slide_down and the exit animation is R.animator.slide_up.

R.animator.slide_down:

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_longAnimTime"
    android:interpolator="@android:anim/anticipate_overshoot_interpolator"
    android:propertyName="yFraction"
    android:valueFrom="-1.0"
    android:valueTo="0.0"
    android:valueType="floatType"/>

The R.animator.slide_up is nearly identical, except the it swaps the valueFrom and the valueTo.  It also uses the overshoot interpolator to give it a little wobble at the end.

This animator translates it’s target view up or down based on a position proportional to its target view height.  So a valueFrom of –1 translates the target view –100% of its height up.  This will move the toolbar fully off the screen.  Translating to back to 0 moves it back to its original position.

Unfortunately, the “yFraction” property name does not exist in the default android views.  I had to write a special view in the common library, FractionalFrameLayout, that handles that. 

FractionalFrameLayout:

public class FractionalFrameLayout extends FrameLayout {


    /**
     * The last requested y-fraction.
     */
    private float mRequestedYFraction;


    /**
     * The last requested x-fraction.
     */
    private float mRequestedXFraction;
    ...

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getHeight() != 0) {
            setYFraction(mRequestedYFraction);
        }

        if (getWidth() != 0) {
            setXFraction(mRequestedXFraction);
        }
    }


    /**
     * Get the current horizontal percentage of the view that is visible.
     *
     * @return the current horizontal percentage of the view that is visible.
     */
    public float getXFraction() {
        int width = getWidth();
        return (width == 0) ? 0 : getX() / (float) width;
    }


    /**
     * Set the current horizontal percentage of the view that is visible.
     *
     * @return the current horizontal percentage of the view that is visible.
     */
    public void setXFraction(float xFraction) {
        mRequestedXFraction = xFraction;
        int width = getWidth();
        if (width != 0) {
            setX(xFraction * width);
        }
    }


    public float getYFraction() {
        int height = getHeight();
        return (height == 0) ? 0 : getY() / (float) height;
    }


    /**
     * Set the current vertical percentage of the view that is visible.
     *
     * @return the current vertical percentage of the view that is visible.
     */
    public void setYFraction(float yFraction) {
        mRequestedYFraction = yFraction;
        int height = getHeight();

        if (height != 0) {
            setY(yFraction * height);
        }
    }
}

The animator’s propertyValue=”yFraction” corresponds to the FractionalFrameLayout’s setyFraction(float) method.  That method changes the y position of the view based on the view height and the yFraction value. 

This class also overwrites the onMeasure to try handle a case where the view blinks briefly when drawn for the first time on the animation start, but that code isn’t working quite right right.  I have to go back to the drawing board on that areas.

This Layout is the parent layout for the toolbar fragments.  Here is the xml for the main toolbar to show it in action:

layouts/fragment_note_edit_main_toolbar_fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<com.fsk.common.presentation.components.FractionalFrameLayout
    style="@style/NoteEditToolbar"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/fragment_note_edit_color_picker_done"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/save_note_background"/>

        <View
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:layout_marginBottom="4dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_marginTop="4dp"
            android:alpha="0.5"
            android:background="@android:color/black"/>

        <include
            layout="@layout/include_color_picker_radio_group"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>
</com.fsk.common.presentation.components.FractionalFrameLayout>

Commits


These changes can be found at commits https://github.com/fsk-software/mynotes/commit/a0632503c5fabf74a31cca091d7975419d002328 and https://github.com/fsk-software/mynotes/commit/ec427c6a5684220836f823824d58a61cd2aea47b.