Friday, October 25, 2013

Programming things to remember 2013 Oct

Application context and resources

Regardless of how you get access to the resource it will be what relates to the current configuration. So even if you get resource via application context it will provide the correct one after eg. rotation. This is true even if you retain the Resources object (provided by either Context). The configuration resolution happens when you getXXX(). (If you retain the string itself, of course it won't change)
private Resources mResources;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        if (getLastNonConfigurationInstance() == null) {
            mResource = getResources();
        } else {
            mResource = (Resources) getLastNonConfigurationInstance(); // deprecated
        }
        final TextView viewById = (TextView)this.findViewById(R.id.text1);
        viewById.setText(mResource.getString(R.string.foo));
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mResource;
    }

Posting delayed message to Handler of main Thread won't leak the Activity it was created from

The Handler doesn't have a strong reference to the Activity. So if a Handler is created from the main thread (from an Activity eg.), and you don't set explicitly a reference, the Activity can freely be GC-d. Of course the Handler itself will be referenced, so that may be leaked.

public class MyActivity extends Activity {
    private Handler mHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d("|MQ", "onCreate()");

        mHandler = new MyHandler(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d("|MQ", "onResume()" + " MyActivity.this: " + MyActivity.this);
        mHandler.sendEmptyMessageDelayed(1, 30 * 1000);
    }
}


private class MyHandler extends Handler {
    private final WeakReference mMyActivity;

    public MyHandler(MyActivity myActivity) {
        mMyActivity = new WeakReference(myActivity);
    }

    @Override
    public void handleMessage(Message msg) {
        if (mMyActivity.get() != null) {
            Log.d("|MQ", "handleMessage()" + " MyActivity.this: " + mMyActivity.get());
        } else {
            Log.d("|MQ", "handleMessage()" + " no MyActivity");
        }
    }
}

13:40:43.217 D/|MQ: onCreate()
13:40:43.267 D/|MQ: onResume() MyActivity.this: com.example.broadcastReceive.MyActivity@428d7f38
rotate device
13:40:44.638 D/|MQ: onCreate()
13:40:44.669 D/|MQ: onResume() MyActivity.this: com.example.broadcastReceive.MyActivity@429150c8
rotate device
13:40:46.190 D/|MQ: onCreate()
13:40:46.240 D/|MQ: onResume() MyActivity.this: com.example.broadcastReceive.MyActivity@428b5f08
rotate device
13:40:47.782 D/|MQ: onCreate()
13:40:47.832 D/|MQ: onResume() MyActivity.this: com.example.broadcastReceive.MyActivity@42900c30
13:41:13.279 D/|MQ: handleMessage() no MyActivity
13:41:14.671 D/|MQ: handleMessage() no MyActivity
13:41:16.252 D/|MQ: handleMessage() MyActivity.this: com.example.broadcastReceive.MyActivity@428b5f08
13:41:17.844 D/|MQ: handleMessage() MyActivity.this: com.example.broadcastReceive.MyActivity@42900c30

Anonymous handler leaks the activity

Anonymous class has an implicit reference to the class it was created from. So if such a Handler is created in the onCreate() it will have a reference to the Activity. Then if you postDelayed a Message to the Handler (or a Runnable, which will be wrapped by a Message at the end of the day), the Message will have a reference to the Handler. The main Looper has a reference to the Message, and the Application has a reference to the Looper. And so the Activity is leaked (it won't get GC-d, after onDestroy() till the Message is handled).
Example.
Additional info on the different kind of classes.

Posting delayed Messages to Handler doesn't count the time spent in deep sleep

It uses the SystemClock.uptimeMillis() as a base. More info.


Monday, October 7, 2013

Programming things to remember 2013 Sept

DownloadManager

- Attach a BroadcastReceiver to listen for DownloadManager.ACTION_DOWNLOAD_COMPLETE.
- Finish the (only) Activity. At this point the app process will still run
- Disable wifi
Result: the app process will be killed, no broadcast received when the download is paused, nor when the framework continues and finishes the download.

Broadcasts

Broadcast will be received even if the app is in the background, if BroadcastReceiver was registered. It is just a good practice to unregister it if not needed (because it may consume resource in vain). So generally you should unregister onPause() if you registered it onResume(). Test code.

If you register the broadcast receiver with the activity's context, it will receive broadcasts as long as the activity is alive. However, you can register the receiver with the applicatino context, and it will still receive broadcasts even if the activity isn't alive. cited from here

Public field (member variable) Matcher

I wrote one. Code here

NineOldAndroid animation

When you use NineOldAndroid for property animation, it will still Value-animate under the hood. Meaning if a View has button and you rotate it by 90 degrees around the y axis (or animating the visibility to gone). The button will be still clickable.

Saturday, April 27, 2013

Parametrized unit test for JUnit and Android (DataSet)

Developing for Android I found testing difficult. Especially when it comes to Unit Testing. Lets be honest Android doesn't provide options to do proper unit test (no, if it begins with "create an instrumentation test", it is not unit test). Third party frameworks (eg. Robolectric) can be used to mock Android classes. They require you to use their test runner, or extend that. This can't be combined with the Parametrized runner of JUnit (which could be used to run a bunch of input parameters against the same test method). Thankfully JUnit has an other powerful extension, called Rules. I've created a package to provide a Parametrized (or DataSet) like annotation.

DataSet package

  • can be used with any TestRunner (eg. with Robolectric Runner)
  • can have multiple datasets in the same test class (and different test methods can use either of them)
  • can set expected exceptions (and they can be different for each testVector)
  • can have run time generated dataset
Note: at least JUnit 4 is needed to use this, since Rules were introduced in that.

If you use it, I would be delighted, if you could let me know. It is always good feeling when someone else finds my work useful :-).

Friday, March 15, 2013

TextView action on disappearing soft keyboard event

The task is to reformat a TextView when the user hides the soft keyboard (ime). It should correct it as a proper money amount: two decimal digits, decimal separator. Of course there is no way on Android to query if the keyboard is going to hide. But there are a few ways, the keyboard can be hidden by the user: backpress when it is visible, focus outside of the TextView, change container TabFragment to other Fragment. This is my solution for this:
/**
 * Simple subclass of an EditText that allows to capture and dispatch BackPress, when the EditText has the focus.
 * This event is later used to finalise formatted text in the amount field (append trailing ".00" if necessary).
 * I could not find other way to detect keyboard is about to hide.
 * Created by: mikinw
 */
@SuppressWarnings("UnusedDeclaration")
public class HwKeySensibleEditText extends EditText {

    public HwKeySensibleEditText(Context context) {
        super(context);
    }

    public HwKeySensibleEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public HwKeySensibleEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
        return super.dispatchKeyEvent(event);
    }

    @Override
    public Parcelable onSaveInstanceState() {
        // This forces a focus changed event to allow {@link AmountReformatter} to do
        // validation when user hides keyboard
        onFocusChanged(false, FOCUS_BACKWARD, null);
        return super.onSaveInstanceState();
    }
}

This class is responsible to reformat the text inside the TextView. It handles the forced focus changed event also. I didn't delete the references to the outer Constants class, but you can find out, what those values should be.
/**
 * Controller class for formatting amount field.
 * Handles all automatic correction that is needed for the amount field.
 * In the constructor it gets a {@link HwKeySensibleEditText} - which is a slightly modified EditText.
 * Auto correction has two steps:
 * - as to user types (add pound sign, don't allow multiple zeros at the beginning, etc. please check
 *   correctOnTheFlyText() function for this. And related unit tests)
 * - on focus lost (add ".00" as necessary. please check correctFinalAmount() function and related unit tests)
 *
 * Also 2 listener can be attached also: OnValueChangedListener and OnEnterLeaveListener.
 * Created by: mikinw
 */
public class AmountReformatter implements TextWatcher, View.OnKeyListener, View.OnFocusChangeListener {

    private HwKeySensibleEditText mAmountTextView;
    private OnValueChangedListener mValueChangedListener;
    private OnEnterLeaveListener mEnterLeaveListener;
    private static final int MAX_INTEGER_DIGITS = 15;

    public interface OnValueChangedListener {
        void onValueChanged (String value);
    }

    public interface OnEnterLeaveListener {
        void onEnter();
    }

    private AmountReformatter(HwKeySensibleEditText amountTextView,
                              OnValueChangedListener valueChangedListener,
                              OnEnterLeaveListener enterLeaveListener) {
        if (amountTextView == null) {
            throw new IllegalArgumentException("amountTextView can't be null");
        }
        mAmountTextView = amountTextView;
        mValueChangedListener = valueChangedListener;
        mEnterLeaveListener = enterLeaveListener;
    }

    private boolean mDeleting = false;
    @Override
    public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
        mDeleting = after < count && mAmountTextView.hasFocus();
    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
    }

    @Override
    public void afterTextChanged(Editable editable) {
        if(mAmountTextView.hasFocus()){
            //update onthefly only if tv has focus, cause correctFinalAmount will be overwrited
            mAmountTextView.removeTextChangedListener(this);
            final String curValue = editable.toString();
            String corrected = null;
            if(mDeleting && curValue.length() == 0){
                //don't allow user to delete pound char
                corrected = Constants.POUND_STRING;
            }else{
                corrected = correctOnTheFlyText(curValue);
            }
            // prevent infinite recursive call
            if ( !corrected.equals(editable.toString())) {
                editable.replace(0, editable.length(), corrected);
            }
            mAmountTextView.addTextChangedListener(this);
        }

        if (mValueChangedListener != null) {
            mValueChangedListener.onValueChanged(mAmountTextView.getText().toString());
        }
    }


    @Override
    public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
        if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.getAction() == KeyEvent.ACTION_UP) {
            mAmountTextView.clearFocus();
        } else if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.getAction() == KeyEvent.ACTION_UP) {
            final InputMethodManager imm =
                    (InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
            mAmountTextView.clearFocus();
            return false;
        }
        return false;
    }

    @Override
    public void onFocusChange(View view, boolean focusGained) {
        final String txtAmount = mAmountTextView.getText().toString();

        if (focusGained) {
            if (TextUtils.isEmpty(txtAmount)) {
                mAmountTextView.setText(Constants.POUND_STRING);
            }else{
                //needs to remove thousands separator
                mAmountTextView.setText(correctOnTheFlyText(txtAmount));
            }
            //move cursor to the end
            mAmountTextView.setSelection(mAmountTextView.getText().length());

            mAmountTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
            if (mEnterLeaveListener != null) {
                mEnterLeaveListener.onEnter();
            }
        } else {
            //restore texthint if there is only pound char
            mAmountTextView.setText(Constants.POUND_STRING.equals(txtAmount)
                                            ? ""
                                            : correctFinalAmount(txtAmount));
        }
    }

    /**
     * Appends ".00" as needed or truncates to have exactly 2 decimals. Does not check other validity of the original
     * string (eg. will accept alphaliterals). It it's value is zero, then returns empty string.
     *
     * @param original amount string that needs to be modified according to the above rules
     * @return modified amount text containing decimal separator and two digits after that
     */
    @SuppressWarnings("MagicCharacter")
    static String correctFinalAmount(String original) {
        // if no . add . to end
        // fill the end to have 2 decimals
        if (TextUtils.isEmpty(original)) {
            return original;
        }

        final String withoutPoundSign = original.charAt(0) == Constants.POUND_CHAR ? original.substring(1) : original;
        BigDecimal decimal;
        try {
            decimal = new BigDecimal(withoutPoundSign);
            if (decimal.compareTo(new BigDecimal(0)) == 0) {
                return "";
            }
        } catch (NumberFormatException nfe) {
            return "";
        }

        decimal = decimal.setScale(2, RoundingMode.DOWN);
        final DecimalFormat df = new DecimalFormat();
        df.setDecimalFormatSymbols(Constants.FORCED_FORMAT_SYMBOLS);
        df.setGroupingSize(3);
        df.setGroupingUsed(true);
        df.setMaximumFractionDigits(2);
        df.setMinimumFractionDigits(2);
        df.setPositivePrefix(Constants.POUND_STRING);
        return df.format(decimal);

    }



    /**
     * Filters out not allowed characters (eg. letters, symbols). Also truncates the leading zeros, but does allow to
     * have "0.". Also adds pound symbol to the beginning. Truncates ending numbers after the first "." character to
     * have at most 2 of them.
     * @param original String that has to be formatted, corrected
     * @return formatted string according to the rules above
     */
    static String correctOnTheFlyText(String original) {
        if (TextUtils.isEmpty(original)) {
            return original;
        }
        final StringBuilder preDot = new StringBuilder(original.length() + 1);
        final StringBuilder postDot = new StringBuilder(original.length() + 1);
        boolean leadingZero = false;
        boolean dot = false;

        int i = 0;
        final int to = original.length();
        for (; i < to; i++) {
            final char ch = original.charAt(i);
            if (ch == '0') {
                leadingZero = true;
            } else if (ch > '0' && ch <= '9') {
                break;
            } else if (ch == Constants.FORCED_FORMAT_SYMBOLS.getDecimalSeparator()) {
                dot = true;
                leadingZero = true;
                break;
            }
        }

        for (; i < to; i++) {
            final char ch = original.charAt(i);
            if (ch >= '0' && ch <= '9') {
                if (preDot.length() < MAX_INTEGER_DIGITS) {
                    preDot.append(ch);
                }
            }else if(ch == Constants.FORCED_FORMAT_SYMBOLS.getGroupingSeparator()){
                //thousands separator ignor, will be added later
            } else if (ch == Constants.FORCED_FORMAT_SYMBOLS.getDecimalSeparator()) {
                dot = true;
                break;
            }
        }

        for (int decimalCount = 0; i < to && decimalCount < 2; i++) {
            final char c = original.charAt(i);
            if (c >= '0' && c <= '9') {
                postDot.append(c);
                decimalCount++;
            }
        }

        if (leadingZero && preDot.length() == 0) {
            preDot.insert(0, '0');
        }
        if (dot) {
            preDot.append(Constants.FORCED_FORMAT_SYMBOLS.getDecimalSeparator());
        }

        //add thousands separator
        preDot.append(postDot);

        preDot.insert(0, Constants.POUND_CHAR);

        return preDot.toString();

    }

    /** Static initiliser */
    public static void setListenersFor(HwKeySensibleEditText amountTextView,
                                       OnValueChangedListener valueChangedListener,
                                       OnEnterLeaveListener enterListener) {
        final AmountReformatter reformatter = new AmountReformatter(amountTextView, valueChangedListener, enterListener);
        amountTextView.setOnFocusChangeListener(reformatter);
        amountTextView.addTextChangedListener(reformatter);
        amountTextView.setOnKeyListener(reformatter);
    }
}

Activity lifecycle events

It took me quite a bit of trouble to understand Activity lifecycle. I decided to make a practical reference of how the lifecycle callbacks are called. I cover some typical use cases (I think it is quite self explanatory, but if you have question or suggestion how can I make it more readable, please contact me):
  • open activity - press home
  • open activity - press back
  • open activity - rotate activity
  • open activity - rotate activity (with custom handler)
  • open activity - open other activity - press back
  • open activity - open other activity - rotate - press back
  • open activity - open other activity for result - press back
Here is the link to the spreadsheet. And some results of other experiments:
  • onConfigurationChange() is not called when ime (soft keyboard) pops up or hides
  • onConfigurationChange() is called during rotate, if manifest is set so. does min api have to be below 13? if this is called, no lifecycle event will be called (?), however onCreateView is called for each view. - no. it is called even with min api level set to 13
  • when to use onRetainNonConfigurationInstance()? - when you want to preserve activity when configuration changes (keep a music decoder running)
  • will onPause be called, when an alert dialog appears? notificaion bar is dragged down? - no. no. it will be called though when a dialog (full screen activity, but in that case onStop will be called also) of an other application is started eg. by taping on it in notifiation bar. And onSaveInstanceState() will be called also.
  • onRestoreInstanceState() is only called, when onSaveInstanceState() returns non empty bundle? - no, it gets called all times
  • Will onDestroy() be called if the app is removed from the recent task list? - yes. (mostly :-) we have to get used to this framework feature is purposedly unreliable)
  • onSaveInstanceState saves a bundle. when this bundle is returned? does it override intent bundle? what is the difference if android:stateNotNeeded is set in manifest? - rather not do anything with android:stateNotNeeded flag, because it is yet another special solution for yet another marginal problem. But! saveInstanceState saves a bundle indeed. This bundle is given to onCreate() as an argument. This is null, if the activity is newly started (or backpress and start again). It is not null otherwise (mostly when it is being rebuilt after a configuration change (eg. rotation). Remember that, onCreate is not called when the activity only goes to background normally. But, if the framework calls onDestroy() eg. because of a memory shortage, the onCreate() will be called and this argument will be not null). You can try to get non existing values from it, you will get the default value (0 in case of int, false in case of boolean, etc). Intent extras are different. You can set extras when starting an activity. You can call intent.putExtra() overloads. There is a putExtras(Bundle) call also, but this bundle has nothing to do with saveInstanceState argument. These extras can be retrieved from the new activity with getIntent().getXXXExtra(String, defaultValue) (where XXX can be Int, Boolean, etc.). This cal will always return something, even if non existing key is requested, because a default value has to be provided.

Wednesday, March 13, 2013

List animation with drawingCache

A bit of animation for the rest of the items when a listitem is removed from a ListView. I've seen an interesting solution for this. The basic idea is to get a bitmap copy of the existing layout and animate the bitmap only. And when the animation finishes, hide the bitmap. With this we don't have to inflate (and measure and layout again and again).
One addition might be the ViewGroup.setClipChildren(false) call. This may prevent a visual glitch: the item at the bottom of the screen may be clipped so the animation may animate a half item. I didn't try it out though.
I made a small change in the code linked above, but I copy here the code to have syntax highlight.
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    public void onItemClick(AdapterView<?> parent, View rowView, int positon,long id) {
        listView.setDrawingCacheEnabled(true); // turn this on to be able to get the bitmapcopy
        Bitmap bitmap = listView.getDrawingCache(); // get the bitmapcopy of the view
        int bottom = rowView.getBottom();
        Bitmap myBitmap1 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), Math.min(bottom, bitmap.getHeight())); // get the upper half
        Bitmap myBitmap2 = Bitmap.createBitmap(bitmap,
                                               0,
                                               bottom,
                                               bitmap.getWidth(),
                                               bitmap.getHeight() - myBitmap1.getHeight()); // get the lower half (that will slide up)
        listView.setDrawingCacheEnabled(false); // turn off, not to mess up anything
        imgView1.setBackgroundDrawable(new BitmapDrawable(getResources(), myBitmap1)); // these to imageView are added to the layout (eg. in the layout file)
        imgView2.setBackgroundDrawable(new BitmapDrawable(getResources(), myBitmap2));
        imgView1.setVisibility(View.VISIBLE);
        imgView2.setVisibility(View.VISIBLE);
        FrameLayout.LayoutParams lp2 = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        FrameLayout.LayoutParams lp1 = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        lp2.setMargins(0, rowView.getBottom(), 0, 0);
        lp1.height = bottom;
        imgView1.setLayoutParams(lp1);
        imgView2.setLayoutParams(lp2);
        TranslateAnimation transanim=new TranslateAnimation(0, 0, 0, -rowView.getHeight());
        transanim.setDuration(1000);
        transanim.setAnimationListener(new Animation.AnimationListener() {
            public void onAnimationStart(Animation animation) {}
            public void onAnimationRepeat(Animation animation) {}
            public void onAnimationEnd(Animation animation) {
                imgView1.setVisibility(View.GONE);
                imgView2.setVisibility(View.GONE);
            }
        });
        mAdapter.remove(mAdapter.getItem(positon));
        imgView2.startAnimation(transanim);
   }});

Tuesday, March 12, 2013

Animated removal of list items

Task to do: we have a ListView in an Android layout. Whenever an item is removed, the list item flies out from its original position to the top of the ListView.

Android has predefined animations for adding and removing items from a ViewGroup. Even more, you can define animation for the other elements (that are not being removed), when some sibling element is being removed. See: http://developer.android.com/training/animation/layout.html . However a ListView is a ViewGroup, this animation never happens, because the adapter of the ListView is immediately updates the view after a removal. You can set animation for the listItems and start them eg. during addition, but not during removal.

There are a couple of ways to make it work:
  • Seaching google, many suggest to utilise onListItemClick listener, and start animation than (eg. http://stackoverflow.com/questions/3928193/how-to-animate-addition-or-removal-of-android-listview-rows). And at the same time schedule a runnable with postDelayed(). I don't believe, relying on times schedules is a good idea generally, even though we might know the exact value of the delay (with Animation.getDuration() in this case). It still can be unprecise.
  • A bit improvement is to use setAnimationListener to remove the item exactly at the end of the animation. Problem in my case is, the event of the removal of the item comes from the model (and view should not modify its data, that should be a copy of the model, or else synch issues may raise). So the onListItemClick is not involved in this process at all.
  • Implement a custom ListView. (but if for each problem I have to implement a custom class, what the Android framework is good for?)
  • My approach is to initiate an animation from the Adapter (that receives update messages from the model). Unfortunately it will have a small visual flaw: the other list items will jump to their new space without animation.
    1. The adapter inflates a new view (getView() is visible from there). Actually it is weird the adapter doesn't have reference to the ui element, it controls, so we can't get the view itself by default (however we could set this reference manually).
    2. places it to the same position where the removable item was,
    3. starts a custom animation
    4. and at the same time it can remove the item.
A fairly good post about interpolators is here, to show how each interpolator works.

The adapter starts the animation when an item is removed.
class MyAdapter extends ArrayAdapter<WhateverListItem> {
// ... removed some code for clarity

    /** Animation used when an item is removed from the adapter. Could be set by the constructor */
    private RemoveAnimation mRemoveAnimation;

    /**
     * Sets the remove animation. This could be done in the constructor, but in my project, this method was more appropriate
     */
    public void setRemoveAnimation(RemoveAnimation removeAnimation) {
        mRemoveAnimation = removeAnimation;
    }

    @Override
    public void remove(SpendingRewardItem object) {
        final View view = mVisibleItems.get(object.mOfferId);
        if (view != null) { // we only want to play the animation for visible items. This mVisibleItems set is maintained by us
            final View removedView = getView(getPosition(object), null, null); // create a copy of the removed item
            if (mRemoveAnimation != null) mRemoveAnimation.animate(view, removedView); // play the animation
            mVisibleItems.remove(object.mOfferId);
        }
        super.remove(object);
    }

    /**
     * Tracks which item is visible currently. Used to test if a removed item should be animated.
     */
    private WeakHashMap<String, View> mVisibleItems = new WeakHashMap<String, View>();

    /**
     * ListAdapter override.
     * @param position item position in the parents' inner list
     * @param convertView reused view if any
     * @param parent parent of the view
     * @return inflated view
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final View view = super.getView(position, convertView, parent);

        // ... customize view, holder pattern, etc.

        mVisibleItems.put(getItem(position).mOfferId, view);
        return view;
    }

}

The animation itself. I use the end of animation to initiate an other one (described in a previous post), I leave related lines here commented out. There are some secret additions. The animated item can go out of the original container. It plays multiple animations on the same item. It is important, that this translates only vertically. Horizontally the item will stay where it was (in my case it will stay in the middle).
public class RemoveAnimation {

//    private static final int GLOW_ANIM_DURATION = 500;
    /** This will be the root ViewGroup  for the animated item. With this, the view can "fly out" of the original ListView */
    private RelativeLayout mAnimationRoot;
    /** Used to acquire interpolators */
    private Context mContext;
//    private final View mTargetTab;
//    private TabGlow mGlow;

    /** In this implementation, animationRoot has to be RelativeLayout, because animate() uses its typed LayoutParams */
    public SpendingRewardsRemoveAnimation(RelativeLayout animationRoot, View targetTab) {
        mAnimationRoot = animationRoot;
        mContext = animationRoot.getContext();
//        mTargetTab = targetTab;
//        mGlow = new TabGlow();
    }

    public void animate(View originalView, View copyView) {
        if (originalView != null && copyView != null) {

            final View animatingView;
            animatingView = copyView;
            // place the copyView to where the originalView was.
            final RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(originalView.getWidth(), originalView.getHeight());
            layoutParams.leftMargin = originalView.getLeft();
            layoutParams.topMargin = originalView.getTop() + getDistanceFromRoot(originalView);
            animatingView.setLayoutParams(layoutParams);

// you need to set the background colour of the list items, or else they will have transparent background. Which is weird usually.
//            animatingView.setBackgroundColor(Color.argb(255, 14, 52, 128));
            mAnimationRoot.addView(animatingView);
            mAnimationRoot.invalidate(); // ask the framework to redraw this view


            // create scale animations. You can play with these parameters. Check reference
            final ScaleAnimation scaleUp = new ScaleAnimation(1.0f, 1.1f, 1.0f, 1.1f, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF, 0f);
            final ScaleAnimation scaleDown = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0f);
            scaleUp.setDuration(50);
            scaleUp.setInterpolator(mContext, android.R.anim.decelerate_interpolator);
            scaleDown.setDuration(250);
            scaleDown.setInterpolator(mContext, android.R.anim.accelerate_interpolator);
            scaleDown.setStartOffset(50); // second animation starts, when firsts finishes

            // create translation animation
            final TranslateAnimation trans = new TranslateAnimation(
                    Animation.RELATIVE_TO_SELF,
                    0,
                    Animation.RELATIVE_TO_SELF,
                    0,
                    Animation.ABSOLUTE,
                    0,
                    Animation.ABSOLUTE,
                    -1.0f * layoutParams.topMargin // move the item from the current position upwards, to 0 (finally topMargin + (-1 * topMargin) will be equal to 0
            );
            trans.setDuration(250);
            trans.setStartOffset(50);
            trans.setInterpolator(mContext, android.R.anim.accelerate_interpolator);

            // add new animations to the set
            final AnimationSet animationSet = new AnimationSet(false);
            animationSet.addAnimation(scaleUp); // view first pops up
            animationSet.addAnimation(scaleDown); // a bit later shrinks to nothing
            animationSet.addAnimation(trans); // and flies to the top of 

            animationSet.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                    animatingView.setVisibility(View.VISIBLE);
                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    animatingView.setVisibility(View.GONE);
                    mAnimationRoot.post(new Runnable() {
                        @Override
                        public void run() {
                            mAnimationRoot.removeView(animatingView); // we don't want to keep the animated View forever

                        }
                    });


//                    mGlow.glow(mTargetTab, GLOW_ANIM_DURATION);
                }

                @Override
                public void onAnimationRepeat(Animation animation) {
                }
            });
            animatingView.startAnimation(animationSet);

        }
    }

    /** This calculates the absolute position of the view. Which can differ from getTop() when the any parent container also has a vertical displacement. */
    private int getDistanceFromRoot(View originalView) {
        View current = originalView;
        ViewGroup parent = null;
        int ret = 0;

        do {
            if (current.getParent() instanceof ViewGroup) {
                parent = (ViewGroup)current.getParent();
                ret += parent.getTop();
                current = parent;
            }
        } while (parent != null && parent != mAnimationRoot);
        return ret;
    }
}

Wiring up is easy:
class MyFragment extends Fragment {

// removed some code for brevity

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View inflated = inflater.inflate(R.layout.spending_rewards_hub, null);

        final ListView listView = (ListView) inflated.findViewById(R.id.listView);
        mAdapter = new MyAdapter(/* whatever you initialise with */);
        mAdapter.setRemoveAnimation(new SpendingRewardsRemoveAnimation((RelativeLayout)inflated, activeTab));
        listView.setAdapter(mAdapter);

        // ... more UI initialisation as needed
    }
}


Friday, March 8, 2013

Glow effect

The task to accomplish is to make a bit of colour animation on a button (or practically any view), when a defined event happens (lats say, when a "flying" list item flies to it). The animation should be a growing-then-declining glow effect.
So the background should be animated from the current state (which is #ff333333)
to change its color to #ff3355dd within 200ms
than change its color to #ff333333 again within 200ms.

Using Android Honeycomb this can easily done with property animation:
private static final int GLOW_ANIM_DURATION = 400;
    private void tabGlowAnimation(View targetTab) {
        final ObjectAnimator objAnim =
                ObjectAnimator.ofObject(targetTab,
                                        "backgroundColor", // we want to modify the backgroundColor
                                        new ArgbEvaluator(), // this can be used to interpolate between two color values
                                        targetTab.getContext().getResources().getColor(R.color.tab_background), // start color defined in resources as #ff333333
                                        targetTab.getContext().getResources().getColor(R.color.tab_glow) // end color defined in resources as #ff3355dd
                );
        objAnim.setDuration(GLOW_ANIM_DURATION / 2);
        objAnim.setRepeatMode(ValueAnimator.REVERSE); // start reverse animation after the "growing" phase
        objAnim.setRepeatCount(1);
        objAnim.start();
    }

This is very convenient. However it still has 2 problems:
  • It doesn't work on preHC devices
  • the start color is predefined during coding time (which is not a problem in this case). It is not easy to determine the color of the background, because the background is a drawable and not a color at this point. And/or you needed to implement a custom Evaluator

Unfortunately on earlier devices it is - lets say - challenging to implement a same effect. My solution is an intermittent view layer between the foreground and background of the button. This view has a custom drawable, which alpha value is animated as required. Actually with this we can have any fancy glow (circular, tiger shaped, etc.). I used an idea found in TransitionDrawable.java.

I've seen this post: http://nathanael.hevenet.com/android-dev-fading-background-animation/, but since I tried to use it on a tabview, it messed up the layout.

public class GlowEffect implements Runnable {
    private static final int MAX_ALPHA = 255;
    /**
     * time interval to reschedule a redraw.
     */
    private static final int MIN_TICK_MILLIS = 30;
    /**
     * The alpha of the drawable of this view will be adjusted for the glow effect
     */
    private View mGlowLayer;

    /**
     * the time, when the animation started. (Or should have started if we restart the glow animation)
     */
    private long mStartTimeMillis;
    /**
     * duration of the whole animation = glow increase + glow decrease
     */
    private int mDuration;
    /**
     * inner representation of the required alpha value. Used when the animation restarts.
     * We have to store it, because Drawable.getAlpha() doesn't exist.
     */
    private double mAlpha;
    /**
     * The time between each animation steps.
     */
    private int mTickMillis;

    public GlowEffect() {
    }

    public void glow(View glowLayer, int durationMillis) {
        mGlowLayer = glowLayer;
        // correct duration time if the animation has been restarted and the previous is still in progress
        // we continue the animation from that point
        final long suspectedTimeFromStart = Math.round(mAlpha * mDuration / 2);
        mDuration = durationMillis;

        mStartTimeMillis = SystemClock.uptimeMillis() - suspectedTimeFromStart;
        mGlowLayer.setVisibility(View.VISIBLE);
        mGlowLayer.getBackground().setAlpha(0);
        mTickMillis = Math.max(MIN_TICK_MILLIS, durationMillis / MAX_ALPHA);
        mGlowLayer.postDelayed(this, mTickMillis);
    }

    @Override
    public void run() {
        boolean done = true;
        final Drawable who = mGlowLayer.getBackground();

        float normalized = (float) (SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
        normalized = Math.min(normalized, 1.0f);
        done = normalized >= 1.0f;

        // linear increase for half of the duration, than linear decrease
        //noinspection MagicNumber
        mAlpha = (0.5 - Math.abs(normalized - 0.5f)) * 2.0f;
        int alpha = (int)Math.round(mAlpha * MAX_ALPHA);

        alpha = Math.max(0, alpha);
        alpha = Math.min(MAX_ALPHA, alpha);

        who.setAlpha(alpha);
        who.invalidateSelf();

        if (!done) {
            mGlowLayer.postDelayed(this, mTickMillis);
        } else {
            mGlowLayer.setVisibility(View.GONE);
        }
    }

}

Whenever a glow is needed, the glow() should be called.


Wednesday, January 23, 2013

Issues to raise during code review

My workmate on one day wanted to change this signature
public void init(char a1, char a2, char a3){}
to this
public void init(char[] a){}
Should I raise it as an issue during the code review or not? Not the question itself is important (which is this should not be raised), but the why. 
- during a code review the exact solution should not be criticized.
- the change does not go against the coding style
- the change does not introduce (too much) complexity
- the change does not harm the readability (too much)
- however I'm on the side to express things explicitly (having 3 char that explicitly shows, how many parameter is needed. And I don't have to care about the validation of the parameter), so I would never introduce such change.

This post doesn't make too much sense, because I've forgotten the main reason to write it. Anyway, I keep it. At least, I have my first post.