Wednesday, October 17, 2018

How to set layout attributes on an included layout

If you're using the tag to include parts of a layout into another layout you might also want to be able to add layout attributes to them. For example, in a RelativeLayout you might want to say that whatever you include will be below some other view. Or in a ConstraintLayout you might want to set the constraint rules on that included layout.

Turns out that even if the layout you include has layout_width and layout_height in order for android to honor the layout params that you are giving your include you need to set layout_width and layout_height within the tag.


Tuesday, February 27, 2018

How to create a selector drawable and change its states on the fly in code

If you've ever built your own buttons you will likely use a selector Drawable to describe all the different states that the button will go through when it's pressed or disabled etc...
To do that the most common way to do it is by using a selector XML resource file.
Here's an example:

<selector xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto">
  <item android:state_enabled="true"
        android:drawable="@drawable/normal_button"/>
  <item android:state_pressed="true"
        android:drawable="@drawable/pressed_button"/>
  <item app:state_red="true"
        android:drawable="@color/red"/>
  <item android:state_enabled="false"
        android:drawable="@color/white"/>
</selector>

Now, what if you want to change some of these conditionally or simply you want to do this in code?
Here's what you would write to get to the same selector behavior but using code:
    StateListDrawable states = new StateListDrawable();
    states.addState(
      new int[]{android.R.attr.state_enabled},
      getResources().getDrawable(R.drawable.blue_button));
    states.addState(
      new int[]{android.R.attr.state_pressed},
      getResources().getDrawable(R.drawable.grey_button));
    states.addState(
      new int[]{R.attr.state_red},
      getResources().getDrawable(R.color.red));
    states.addState(
      new int[]{-android.R.attr.state_enabled},
      getResources().getDrawable(R.color.white));
    myButton.setBackgroundDrawable(states);

As you can see all you need is to create a StateListDrawable and add all the states that your selector has. You can even add your own custom states (for example here I'm using my own R.attr.state_red)
The trick is to know that if you want to set the drawable for true you use the attribute as it is and if you want the false case you use its negative value. (in this example I set the disabled state using -android.R.attr.state_enabled)
Once all your states are defined you can just use that StateListDrawable as a background for your Button or ImageView or whatever...

Now you can change anything you want on the fly and switch or test behavior as you need.

PS: to declare your own stylable attributes (like my state_red) you create an attrs.xml file in your res/values/ folder and define them like this: (all you need here for this example is the line with state_red but I added more examples for the other possible types)

<resources>
  <declare-styleable name="MyButton">
    <attr name="primaryColor" format="reference|color" />
    <attr name="myButtonStyle" format="string" />
    <attr name="state_red" format="boolean" />
    <attr name="state_test" format="boolean" />

    <attr name="myflags">
      <flag name="one" value="1" />
      <flag name="two" value="2" />
      <flag name="four" value="4" />
      <flag name="eight" value="8" />
    </attr>

    <attr name="myenum">
      <enum name="one" value="1" />
      <enum name="two" value="2" />
      <enum name="three" value="3" />
    </attr>

  </declare-styleable>
</resources>


BONUS: If you want to set custom states and change them at runtime and in code here's what you do:
You will have to define your own class that extends Button or ImageView or whatever it is that your modifying.
In that child class, you will want to override the onCreateDrawableState method so that it will set different states depending on whatever use case you have. Here I'm making the state green or red depending on the value of a field variable called mMyBoolean:

  @Override  protected int[] onCreateDrawableState(int extraSpace) {
    final int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
    if (mMyBoolean) {
      mergeDrawableStates(drawableState, {R.attr.state_green});
    } else {
      mergeDrawableStates(drawableState, {R.attr.state_red});
    }
    return drawableState;
  }
}

Then all you have to do is set that boolean to whatever value it needs to be for your use case and then call refreshDrawableState(); on your Button or ImageView or whatever your View is.


Friday, June 9, 2017

How to create your own overflow menus anywhere in your UI


You're probably building a list or a grid and you'd like your items to have a little overflow menu.
Well, here's what you do. You inflate a PopupMenu.
Here are the details:


You can find a sample code here.

Tuesday, March 14, 2017

TL:DR:
If you ever plan on using Frame Animations (animation-list) to animate a list item in a recycler view make sure you stop the animation in ALL cases or it will be very easy to leak it.

DETAILS:
For one of my projects I had to add a loading list item in a recycler view so the user would see something pretty while they wait for the list to populate.

So I went ahead and created an animation-list,
<?xml version="1.0" encoding="utf-8">
android="http://schemas.android.com/apk/res/android" android:oneshot="false">
    <android:drawable="@drawable/tile_loading_01" android:duration="@integer/content_loading_anim" >
    <android:drawable="@drawable/tile_loading_02" android:duration="@integer/content_loading_anim" >
    <android:drawable="@drawable/tile_loading_03" android:duration="@integer/content_loading_anim" >
[...]

a new view holder for this list item
class ContentLoadingViewHolder extends RecyclerView.ViewHolder {
    private AnimationDrawable loopAnimation = null;
  
    ContentLoadingViewHolder(View itemView) {
        super(itemView);
        ImageView image = (ImageView) itemView.findViewById(R.id.illustration);
        if (image != null) {
            loopAnimation = (AnimationDrawable) image.getDrawable();
        }
    }

    void startAnimation() {
        Log.d("Starting animation - loopAnimation: " + loopAnimation
            + " isRunning: " + (loopAnimation != null ? loopAnimation.isRunning() : "null"));
        if (loopAnimation != null && !loopAnimation.isRunning()) {
            loopAnimation.start();
        }
    }

    void stopAnimation() {
        Log.d("Stopping animation - loopAnimation: " + loopAnimation
            + " isRunning: " + (loopAnimation != null ? loopAnimation.isRunning() : "null"));
        if (loopAnimation != null && loopAnimation.isRunning()) {
            loopAnimation.stop();        }
        }
    }

 in my adapter I added the call to start the animation in onbindview
@Override
@MainThread
public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, int unreliablePosition) {
    if (viewHolder instanceof ContentLoadingViewHolder) {
        ((ContentLoadingViewHolder)viewHolder).startAnimation();
    }
}

and stop it in onViewRecycled
@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
    clearAnimationIfRunning(holder);
    super.onViewRecycled(holder);
}

At this point you might think. Ok I'm good this should work. And for the most part it does but it will possibly leak your animation if you don't also stop it in all these other cases:
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
    clearAllAnimations();
    super.onDetachedFromRecyclerView(recyclerView);
}

@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
    clearAnimationIfRunning(holder);
    super.onViewRecycled(holder);
}

@Override
public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
    clearAnimationIfRunning(holder);
    return super.onFailedToRecycleView(holder);
}

void clearAllAnimations() {
    Log.d("Clearing all animations in the suggestion adapter.");
    // clear all animation in recycler view
    int count = recyclerView.getChildCount();
    for (int i = 0; i < count; ++i) {
        try {
            RecyclerView.ViewHolder holder = recyclerView.findViewHolderForAdapterPosition(i);
            clearAnimationIfRunning(holder);
        } catch (Throwable t) {
            Log.e("Unable to clear animations");
        }
    }

    // clear all animation if left pending in recycler view pool
    RecyclerView.RecycledViewPool pool = recyclerView.getRecycledViewPool();
    if (pool != null) {
        while (true) {
            RecyclerView.ViewHolder holder = pool.getRecycledView(LOADING.ordinal());
            if (holder != null) {
                clearAnimationIfRunning(holder);
            } else {
                break;
            }
        }
    }
}

private void clearAnimationIfRunning(RecyclerView.ViewHolder holder) {
    if (holder != null && holder instanceof ContentLoadingViewHolder) {
        ((ContentLoadingViewHolder)holder).stopAnimation();
    }
}

See the recycler might have failed to recycle the view in which case you need to catch it in "onFailedToRecycleView" and the user might tap the recent apps button and swipe the app closed and for that case you will catch it in onDetachedFromRecyclerView.

Lastly, if you're using this in a fragment, you also want to stop animations when the fragment gets detached:
@Override
public void onDetach() {
    if (adapter != null) {
        adapter.clearAllAnimations();
    }
    super.onDetach();
}

Hope this helps keep your app leak free.

Wednesday, May 11, 2016

How to make your ViewPager give a sneak peak at the next page

Simply add a scroll animation like so:

final long DRAG_HINT_ANIMATION_DURATION_IN = 500;
final long DRAG_HINT_ANIMATION_DURATION_OUT = 250;

ObjectAnimator scrollIn = ObjectAnimator.ofInt(viewPager, "scrollX", 200);
scrollIn.setDuration(DRAG_HINT_ANIMATION_DURATION_IN);
scrollIn.setInterpolator(new AccelerateDecelerateInterpolator());
ObjectAnimator scrollOut = ObjectAnimator.ofInt(viewPager, "scrollX", 0);
scrollOut.setDuration(DRAG_HINT_ANIMATION_DURATION_OUT);
scrollOut.setInterpolator(new AccelerateDecelerateInterpolator());

AnimatorSet set = new AnimatorSet();
set.playSequentially(scrollIn, scrollOut);
set.start();

This will scroll your viewpager by 200 pixels in 500ms. and then scroll it back in 250ms.
I'm currently using an easing interpolator (AccelerateDecelerateInterpolator)
It accelerates to the normal speed and then decelerates before reaching the end of the animation.
Of course any of these parameters are yours to modify.
Enjoy !