1

In my app, there is currently a section that holds a fragment that queries the database for a list of objects and then displays them (using a ListView).
When a user clicks one of the objects in the list, the section grows bigger in height to incorporate a different fragment that .replace()s the ListView fragment and moves up.

This new fragment displays information related to the object the user clicked.
One of the views in this fragment is a ViewPager that contains only 2 fragments (at most):

  1. A timer fragment.
  2. A calendar fragment.

The timer fragment is a fragment that simply uses a CountDownTimer and changes a TextView's text onTick(). This fragment may or may not be shown, depending on the object.

The calendar fragment however, contains an heavier view - a custom calendar view.
The calendar is supposed to show the days a year before and after the current date (now).

The calendar fragment (or view, actually, since there is no logic at all in the fragment) causes the animation that fires when a user clicks on an object to stutter and it looks bad. This only happens in the first time an object is clicked. In the second time it's smoother (I guess it's thanks to the ViewPager saving the fragment's state, but I'm not sure...).

I was wondering whether there is any specific bottleneck/problem in the code that would cause it to behave like that. My best guess is that there are just too many views being loaded together.

The first solution I thought of, is to basically move the animation inside the fragment and fire it only after the fragment is fully loaded and ready to be displayed. That, however, would mean that I will be controlling the parent (the container of the fragment) from within the fragment itself... not sure whether this is a good design or not. I could create a listener for that, and place its call in the end of onCreateView(), though.

Another possible solution, and this is just a theory of mine, but creating all of the views on a separate thread, and only then adding them to the UI in the main thread could maybe slightly speed up the process. Although, I'm not really sure how better that would be (in terms of performance), if at all, and how much of a good practice this is.

How can I optimize my CalendarView (or maybe my whole "Object View Fragment") in order to allow the animation to work properly?

CalendarView consists of:

  • A vertical LinearLayout that contains 2 sub-views:
    1. A topbar which is a TableLayout with a single with the names of the 7 days.
    2. A GridView which is filled with TextViews of day numbers.

Some code:

MainActivity - replaces the current fragment

@Override
public void OnGoalSelected(Parcelable goal) {
    Log.i(TAG, "GoalSelected");
    isMinimized = false;

    HabitGoalViewFragment newFragment = new HabitGoalViewFragment();
    Bundle args = new Bundle();
    args.putParcelable(GOAL_POSITION_KEY, goal);
    newFragment.setArguments(args);

    getSupportFragmentManager().addOnBackStackChangedListener(this);
    getSupportFragmentManager().beginTransaction()
            .setCustomAnimations(R.anim.fadein, R.anim.fadeout, R.anim.fadein, R.anim.fadeout)
            .replace(R.id.cardFragmentContainer, newFragment)
            .addToBackStack(null)
            .commit();

    mActionBar.setCustomView(R.layout.ab_goal_view);

    mLL.getLayoutParams().height += screenHeight;
    ObjectAnimator objAnim = ObjectAnimator.ofFloat(mLL, "translationY", AmountToMove).setDuration(500);
    objAnim.setInterpolator(new DecelerateInterpolator());
    objAnim.start();
}

HabitGoalView

public static Class<?>[] mFragmentArray = {
        HabitCalendarFragment.class,
        HabitDurationTimerFragment.class
};

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_goal_view, container, false);

    mActionBar = getActivity().getActionBar();
    mActionBar.getCustomView().findViewById(R.id.goal_view_back_button_parent).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            getActivity().getSupportFragmentManager().popBackStackImmediate();
        }
    });

    mHabitGoal = getArguments().getParcelable(MainActivity.GOAL_POSITION_KEY);

    ............

    mViewPager = (ViewPager) view.findViewById(R.id.pager);
    mViewPager.setAdapter(new ViewPagerAdapter(getActivity().getSupportFragmentManager(), getActivity()));

    return view;
}

private class ViewPagerAdapter extends FragmentStatePagerAdapter {

    private Context mContext;

    public ViewPagerAdapter(FragmentManager fm, Context context) {
        super(fm);
        mContext = context;
    }

    @Override
    public Fragment getItem(int position) {
        return Fragment.instantiate(mContext, mFragmentArray[position].getName());
    }

    @Override
    public int getCount() {
        return mFragmentArray.length;
    }
}

CalendarView (This is the View itself, not the fragment!)

public class CalendarView extends LinearLayout {

private final String TAG = this.getClass().getName();
private Context mContext;
private TableLayout mTopBarTableLayout;
private TableRow mTableRow;

public CalendarView(Context context) {
    super(context);
    mContext = context;

    this.setOrientation(VERTICAL);

    mTopBarTableLayout = new TableLayout(mContext);

    mTopBarLinearLayout.setStretchAllColumns(true);
    mTableRow = new TableRow(mContext);
    int[] mDaysList = {R.string.days_sunday, R.string.days_monday, R.string.days_tuesday,
            R.string.days_wednesday, R.string.days_thursday, R.string.days_friday, R.string.days_saturday}; //
    AutoScrollingTextView mDayTextView;
    int padding;
    for (int i = 0; i < 7; i++) {

        mDayTextView= new AutoScrollingTextView(mContext);
        padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
        mDayTextView.setPadding(padding, padding, padding, padding);
        mDayTextView.setTextSize(16);
        mDayTextView.setGravity(Gravity.CENTER);

        mDayTextView.setText(getResources().getString(mDaysList[j]).substring(0, 3).toUpperCase(Locale.US));

        mDayTextView.setWidth(0);
        mDayTextView.setSingleLine(true);

        mDayTextView.setHorizontallyScrolling(true);
        mDayTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);

        mTableRow.addView(mDayTextView, i);
    }

    mTopBarLinearLayout.addView(mTableRow, 0);

    this.addView(mTopBarLinearLayout, 0);
    this.addView(new CalendarGridView(mContext), 1);
}

private class CalendarGridView extends GridView {

    Context mContext;
    DateTime CurrentMonthDateTime, NextYearDT, LastYearDT;
    int offset;

    public CalendarGridView(Context context) {
        super(context);
        mContext = context;
        init();
    }

    public CalendarGridView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }

    public CalendarGridView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mContext = context;
        init();
    }

    public void init() {
        this.setNumColumns(7); // 7 columns - 1 for each day
        this.setStretchMode(STRETCH_COLUMN_WIDTH);

        this.setAdapter(new CalendarAdapter());

        CurrentMonthDateTime = DateTime.now();
        LastYearDT = DateTime.now().minusYears(1).withDayOfMonth(1);

        offset = LastYearDT.getDayOfWeek();
        if (offset == 7)
            offset = 0;

        int n = Days.daysBetween(LastYearDT.withTimeAtStartOfDay(), CurrentMonthDateTime.withTimeAtStartOfDay()).getDays() + offset;
        Log.i(TAG, "Days Offset = " + n);
        this.setSelection(n);
    }

    private class CalendarAdapter extends BaseAdapter {

        private int offset, n;
        private DateTime mDateTime, mDateToPrint;

        public CalendarAdapter() {
            mDateTime = DateTime.now().minusYears(1).withDayOfMonth(1);
            NextYearDT = DateTime.now().plusYears(1).withDayOfMonth(1);

            n = Days.daysBetween(mDateTime.withTimeAtStartOfDay(),
                    NextYearDT.withTimeAtStartOfDay()).getDays();

            // round up to the nearest number divisible by 7
            n += (7 - n%7);

            offset = mDateTime.getDayOfWeek(); // 1 - mon, 2 - tue ... 7 - sun

            // set first day to Sunday 
            if (offset == 7)
                offset = 0;

            mDateTime = mDateTime.minusDays(offset);
        }

        @Override
        public int getCount() {
            return n;
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            SquareTextView view = (SquareTextView) convertView;
            if (convertView == null) {
                view = new SquareTextView(mContext);
                view.setTextSize(18);
                view.setGravity(Gravity.CENTER);
                view.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // TODO Auto-generated method stub
                        Log.i(TAG, ((SquareTextView) v).getText().toString());
                    }
                });
            } else {
                view.setBackgroundResource(0); // TODO set as drawable and then remove it
                view.setTag(null);
            }

            mDateToPrint = mDateTime.plusDays(position);

            if (mDateToPrint.getDayOfMonth() == 1)
                view.setTag(mDateToPrint.monthOfYear().getAsShortText(Locale.ENGLISH));

            view.setText(mDateToPrint.getDayOfMonth() + "");

            if (mDateToPrint.withTimeAtStartOfDay().isEqual(CurrentMonthDateTime.withTimeAtStartOfDay())) {
                //view.setBackgroundResource(R.color.background_gray);
                view.setBackgroundColor(getResources().getColor(R.color.green));
                view.setTag("today");
            }

            return view;
        }

    }
}
}
Asaf
  • 2,005
  • 7
  • 37
  • 59
  • 1
    The problem is, you're creating who knows how many views when only a few are needed. Implement a viewholder pattern to make it more efficient. – SemaphoreMetaphor Jul 29 '14 at 18:39
  • 1
    @bbaker, I am recycling views, though. Also, each view in the calendar is a `TextView`, so there is no need for a view holder, since there are no child views whatsoever I guess. Thanks for commenting! – Asaf Jul 29 '14 at 19:30
  • @xTCx Can you try something? => Fire the `ObjectAnimator` with a short delay - say `500ms`. Declare the `ObjectAnimator` as `final`. Create a `Handler` and place the call `objAnim.start()` inside a `Runnable`. Post the `Runnable` to `mHandler.postDelayed(mRunnable, 500L);`. Is that any better? The fragment(or view) may not be getting enough time to initialize before the call to animate. If delayed posting _does_ work, you can use this on the first transaction and then let the ViewPager handle subsequent retrievals. – Vikram Aug 06 '14 at 07:30

1 Answers1

2

So I finally found a solution! At the end the source to my problem was elsewhere, but just in case someone would have the same kind of problem/stuttering, I will share the best way to solve it based on my knowledge - using listeners. It was also recommended in the Android development tutorials.

Using a custom listener interface you can let the containing activity know that the fragment has finished all of it's loading. That way, you can make the animation run only after the fragment has been loaded, which would result in a smooth animation.

Example interface:

public interface OnFragmentLoadedListener {
    public abstract void OnFragmentLoaded(Object A);
}

You then implement the interface in the activity that contains the fragment:

public class MainActivity extends Activity implements MyFragment.OnFragmentLoadedListener {
    ................

    public void OnFragmentLoaded(Object A) {
        //Do something...
    }

Then you need to set the listener inside the fragment. You can either create a method like setOnFragmentListener(OnFragmentListener mListener) or, as I prefer, get the reference to the listener in the onAttach() method of the Fragment class:

public void onAttach(Activity activity) {
    super.onAttach(activity);

    try {
        this.mListener = (OnFragmentLoadedListener) activity;
    } catch (final ClassCastException e) {
        throw new ClassCastException(activity.toString() + " must implement OnFragmentLoadedListener");
    }
}

Finally, you need to let the listener know when the fragment is ready to be shown.
In my case, that would be after ANOTHER fragment would load, so I put my listener call in the interface implementation of the containing (and contained) fragment.

if(mListener != null)
    mListener.OnFragmentLoaded(A);

My source of my problem was really... JODA TIME

The source to my problem was actually my usage of the joda-time library.
I used the original joda-time for java, which was a mistake.
Everything worked fine and all but after trying hard to solve my problem I wondered whether it was possible that joda-time was actually the problem since the rest of my calendar code was very much like the calendar widget provided with android.

Surprisingly, I came across this question/answer, which proved my theory right.
At first, I was actually quite disappointed, because I thought I'd have to go back to using androids' default Calendar class, and then I remembered there was a version of joda for android, and thought there was nothing to lose by trying it out.

Anyways, turns out this android version really did wonders. It worked like magic and sped up my whole app! You can find this great library here: joda-time-android

I don't really know why, which is why I invite anyone who knows to explain it in a new answer, which I'll gladly mark as accepted.

Community
  • 1
  • 1
Asaf
  • 2,005
  • 7
  • 37
  • 59