Android Spinner Reveal Animation Material Design-ThrowExceptions

Exception or error:

I just need something pretty simple.
Can’t find any guide/resource online that will help me to achieve the following behaviour:

So there are 3 things:

  1. A Ripple that appears to be bounded to the view
  2. Scale up animation of the popup (Can’t find a way to customize it)
  3. The text is carried over from the Spinner field to the actual popup

Any help is appreciated .

EDIT

The ripple is actually a straight forward thing that can be found in the docs. The scale up animation of the popup dropdown is what interesting me the most. If I only could get a reference to that popup , I could animate it however I want… Ideas anybody ?

How to solve:

I’ve been working on a solution to this for the past >48hours (I have some extra time don’t blame me) and I can tell you these for sure:

  1. Using the AppCompat theme and the normal Spinner gives you the ripples for free (yay!). If you have a custom Spinner make sure to extend AppCompatSpinner instead and you get some good theming. Quoting the AppCompatSpinner javadoc:

A Spinner which supports compatible features on older version of the platform, including:

  • Allows dynamic tint of it background via the background tint methods in ViewCompat.
  • Allows setting of the background tint using backgroundTint and backgroundTintMode.
  • Allows setting of the popups theme using popupTheme.

This will automatically be used when you use Spinner in your layouts. You should only need to manually use this class when writing custom views.

  1. There is no way to retrieve the PopupWindow that displays by extending ANY class. There are a lot of nested classes that contribute to the feature (spoiler: They are mostly private).

  2. You can customize the enter and exit transitions of the PopupWindowstatically in your theme (woohoo!). I’m currently using AppCompat v23 and doing some investigation i found:

    • The style that customizes ListPopupWindow LOLLIPOP and above

      <style name="Widget.Material.ListPopupWindow">
          <item name="dropDownSelector">?attr/listChoiceBackgroundIndicator</item>
          <item name="popupBackground">@drawable/popup_background_material</item>
          <item name="popupElevation">@dimen/floating_window_z</item>
          <item name="popupAnimationStyle">@empty</item>
          <item name="popupEnterTransition">@transition/popup_window_enter</item>
          <item name="popupExitTransition">@transition/popup_window_exit</item>
          <item name="dropDownVerticalOffset">0dip</item>
          <item name="dropDownHorizontalOffset">0dip</item>
          <item name="dropDownWidth">wrap_content</item>
      </style>
      
    • The style that customizes it pre-LOLLIPOP:

      <style name="Base.Widget.AppCompat.ListPopupWindow" parent="">
          <item name="android:dropDownSelector">?attr/listChoiceBackgroundIndicator</item>
          <item name="android:popupBackground">@drawable/abc_popup_background_mtrl_mult</item>
          <item name="android:dropDownVerticalOffset">0dip</item>
          <item name="android:dropDownHorizontalOffset">0dip</item>
          <item name="android:dropDownWidth">wrap_content</item>
      </style>
      
    • And this is what you set in your Theme:

      // From the constructor listPopupWindowStyle is the 
      // theme attribute you're looking for.
      public ListPopupWindow(Context context, AttributeSet attrs) {
          this(context, attrs, R.attr.listPopupWindowStyle);
      }
      

That said, I’m still in the process of getting your specifics but I’m solving it by cloning 2 classes: AppCompatSpinner and ListPopupWindow and adding a public getPopup() getter to the cloned ListPopupWindow code so its accessible in my cloned AppCompatSpinner. This opens up a couple of opportunities. An example is my centerPopup() method which should shift the position of the laid out popup based on the centers of the spinnerItem (the selected item that shows in the spinner view itself) and the spinnerDropdownItem (individual views that show as list items in the drop down). The centerPopup() method is then called in DropdownPopup.show() right after the call to super.show()

private void centerPopup(boolean updateListSelection) {
    boolean aboveAnchor = getPopup().isAboveAnchor();

    int[] spinnerLocation = new int[2];
    int[] popupLocation = new int[2];

    ListView listView = getListView();

    /*
        Popup is anchored at spinner TOP LEFT when it is set to overlap else at BOTTOM LEFT

        Also seems the windowlayoutparams generated for popup is wrapcontent for width and height
        As a result popup locationOnScreen returns [0, 0] which is relative to the window.
        We can take it as relative to spinner location and add spinnerLocation X value to give
        left edge of popup
     */

    MaterialSpinner.this.getLocationInWindow(spinnerLocation);
    int spMidX = spinnerLocation[0] + (MaterialSpinner.this.getWidth() / 2);
    int spMidY = spinnerLocation[1] + (MaterialSpinner.this.getHeight() / 2);

    Rect spinnerBgdPadding = new Rect();
    MaterialSpinner.this.getBackground().getPadding(spinnerBgdPadding);

    // ----- BUG - returns erroneous height
    // Ideally should measure one of the drop down list children and give a height
    // exactly as it wouldwhen laid out eventually.
    // Works only for lists with homogenously tall content
    View child = listView.getAdapter().getView(0, null, listView);

    child.setLayoutParams(new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));

    child.measure(
            MeasureSpec.makeMeasureSpec(listView.getWidth() - listView.getPaddingLeft() - listView.getPaddingRight(), MeasureSpec.AT_MOST),
            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
    );
    int listItemHeight = child.getMeasuredHeight();
    // ----- /BUG

    getPopup().getContentView().getLocationInWindow(popupLocation);
    int popMidX = spinnerLocation[0] + (getPopup().getWidth()) / 2;
    int popMidY = spinnerLocation[1] + (listItemHeight / 2);

    int hoff = getHorizontalOffset();
    int voff = getVerticalOffset();

    int xoff = spMidX - popMidX - hoff - spinnerBgdPadding.left;
    int yoff = spMidY - popMidY - voff;

    getPopup().update(spinnerLocation[0] + xoff, spinnerLocation[1] + yoff, -1, -1, true);

    if (updateListSelection)
        listView.setSelectionFromTop(MaterialSpinner.this.getSelectedItemPosition(), 0)
    ;

    // getPopup().update(MaterialSpinner.this, xoff, yoff, -1, -1);
    // int numVisItems = listView.getLastVisiblePosition() - getFirstVisiblePosition();

}

If the correct listItemHeight is returned the popup window should not only appear over the the spinner but the text centers should align too. (Might be off by some pixels due to background and padding related issues but they can be solved).

Once centered, the next step would be to scroll the dropDownList selectedPosition View to the center of the popupWindow and update the yoff Y-offset accordingly.

The popupEnterTransition attribute limits to only LOLLIPOP but if that isnt a problem, you can insert your scale up transition there and that should get you the effect you want. I haven’t tried it however but I also think a better animation would be a reveal effect from the spinner center (it looks like that from your specs video).

But all these are good…in theory x_x lol

###

1) A ripple can be achieved in two ways: one is to use a CardView as a parent element of each single list item and another way is to create a selector and set it as a background for your single list item.

2) You can try to use element transitions to animate your view accordingly

3)You should pass your text as extra data from your shopping info list item to the inner dropdown list OR you’ll need to simply save and maintain selected position

You might also want to use third party libraries for this.
Check http://android-arsenal.com to find one!

Leave a Reply

Your email address will not be published. Required fields are marked *