Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cb3b347c authored by James Lemieux's avatar James Lemieux
Browse files

Replace ListView of laps with RecyclerView

Animation performance has improved dramatically on KK as
a result of this change. \o/

bug: 24542370
Change-Id: I7a5df79b5f891e9427bd1cdb7f1ba68e87a0fe06
parent 799b8237
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@
            android:layout_height="@dimen/circle_size"
            android:layout_gravity="center"
            android:gravity="center"
            android:padding="8dp">
            android:padding="16dp">

            <com.android.deskclock.timer.CountingTimerView
                android:id="@+id/stopwatch_time_text"
@@ -49,7 +49,7 @@

    </FrameLayout>

    <ListView
    <android.support.v7.widget.RecyclerView
        android:id="@+id/laps_list"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
+2 −2
Original line number Diff line number Diff line
@@ -26,7 +26,7 @@
        android:layout_width="@dimen/circle_size"
        android:layout_height="@dimen/circle_size"
        android:layout_gravity="center"
        android:padding="8dp">
        android:padding="16dp">

        <com.android.deskclock.timer.CountingTimerView
            android:id="@+id/stopwatch_time_text"
@@ -40,7 +40,7 @@
            android:background="@null" />
    </FrameLayout>

    <ListView
    <android.support.v7.widget.RecyclerView
        android:id="@+id/laps_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
+71 −73
Original line number Diff line number Diff line
@@ -18,11 +18,11 @@ package com.android.deskclock.stopwatch;

import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.android.deskclock.R;
@@ -37,7 +37,7 @@ import java.util.List;
 * Displays a list of lap times in reverse order. That is, the newest lap is at the top, the oldest
 * lap is at the bottom.
 */
class LapsAdapter extends BaseAdapter {
class LapsAdapter extends RecyclerView.Adapter<LapsAdapter.LapItemHolder> {

    private final LayoutInflater mInflater;
    private final Context mContext;
@@ -51,6 +51,7 @@ class LapsAdapter extends BaseAdapter {
    public LapsAdapter(Context context) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        setHasStableIds(true);
    }

    /**
@@ -59,57 +60,26 @@ class LapsAdapter extends BaseAdapter {
     * @return 0 if no laps are yet recorded; lap count + 1 if any laps exist
     */
    @Override
    public int getCount() {
    public int getItemCount() {
        final int lapCount = getLaps().size();
        final int currentLapCount = lapCount == 0 ? 0 : 1;
        return currentLapCount + lapCount;
    }

    /**
     * @return {@code null} for the current lap, the Lap object for all other laps
     */
    @Override
    public Lap getItem(int position) {
        return position == 0 ? null : getLaps().get(position - 1);
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    @Override
    public long getItemId(int position) {
        // During transitions the adapter may be queried with invalid positions.
        if (position >= getCount()) {
            return -1;
        }

        final Lap lap = getItem(position);
        return lap == null ? getLaps().size() + 1 : lap.getLapNumber();
    public LapItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        final View v = mInflater.inflate(R.layout.lap_view, parent, false /* attachToRoot */);
        return new LapItemHolder(v);
    }

    @Override
    public View getView(int position, View view, ViewGroup parent) {
        if (getCount() == 0) {
            return null;
        }

        // Inflate a new view if necessary.
        if (view == null) {
            view = mInflater.inflate(R.layout.lap_view, parent, false);
            final TextView lapTime = (TextView) view.findViewById(R.id.lap_time);
            final TextView lapNumber = (TextView) view.findViewById(R.id.lap_number);
            final TextView accumulatedTime = (TextView) view.findViewById(R.id.lap_total);
            view.setTag(new LapItemHolder(lapNumber, lapTime, accumulatedTime));
        }

    public void onBindViewHolder(LapItemHolder viewHolder, int position) {
        final long lapTime;
        final int lapNumber;
        final long totalTime;

        // Lap will be null for the current lap.
        final Lap lap = getItem(position);
        final Lap lap = position == 0 ? null : getLaps().get(position - 1);
        if (lap != null) {
            // For a recorded lap, merely extract the values to format.
            lapTime = lap.getLapTime();
@@ -123,31 +93,40 @@ class LapsAdapter extends BaseAdapter {
        }

        // Bind data into the child views.
        final LapItemHolder holder = (LapItemHolder) view.getTag();
        holder.lapTime.setText(formatLapTime(lapTime));
        holder.accumulatedTime.setText(formatAccumulatedTime(totalTime));
        holder.lapNumber.setText(formatLapNumber(getLaps().size() + 1, lapNumber));

        return view;
        viewHolder.lapTime.setText(formatLapTime(lapTime, true));
        viewHolder.accumulatedTime.setText(formatAccumulatedTime(totalTime, true));
        viewHolder.lapNumber.setText(formatLapNumber(getLaps().size() + 1, lapNumber));
    }

    /**
     * @return {@code false} to prevent the laps from responding to touch
     */
    @Override
    public boolean isEnabled(int position) {
        return false;
    public long getItemId(int position) {
        final List<Lap> laps = getLaps();
        if (position == 0) {
            return laps.size() + 1;
        }

        return laps.get(position - 1).getLapNumber();
    }

    /**
     * @param currentLapView the view that displays the current lap information
     * @param lapTime time accumulated for the current lap
     * @param accumulatedTime time accumulated for the current lap and all prior laps
     * @param rv the RecyclerView that contains the {@code childView}
     * @param totalTime time accumulated for the current lap and all prior laps
     */
    void updateCurrentLap(View currentLapView, long lapTime, long accumulatedTime) {
        final LapItemHolder holder = (LapItemHolder) currentLapView.getTag();
        holder.lapTime.setText(formatLapTime(lapTime));
        holder.accumulatedTime.setText(formatAccumulatedTime(accumulatedTime));
    void updateCurrentLap(RecyclerView rv, long totalTime) {
        // If no laps exist there is nothing to do.
        if (getItemCount() == 0) {
            return;
        }

        final View currentLapView = rv.getChildAt(0);
        if (currentLapView != null) {
            // Compute the lap time using the total time.
            final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);

            final LapItemHolder holder = (LapItemHolder) rv.getChildViewHolder(currentLapView);
            holder.lapTime.setText(formatLapTime(lapTime, false));
            holder.accumulatedTime.setText(formatAccumulatedTime(totalTime, false));
        }
    }

    /**
@@ -157,7 +136,18 @@ class LapsAdapter extends BaseAdapter {
     */
    Lap addLap() {
        final Lap lap = DataModel.getDataModel().addLap();

        if (getItemCount() == 10) {
            // 10 total laps indicates all items switch from 1 to 2 digit lap numbers.
            notifyDataSetChanged();
        } else {
            // New current lap now exists.
            notifyItemInserted(0);

            // Prior current lap must be refreshed once with the true values in place.
            notifyItemChanged(1);
        }

        return lap;
    }

@@ -261,38 +251,44 @@ class LapsAdapter extends BaseAdapter {

    /**
     * @param lapTime the lap time to be formatted
     * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
     *                  set changes; they are not allowed to occur during bind
     * @return a formatted version of the lap time
     */
    private String formatLapTime(long lapTime) {
    private String formatLapTime(long lapTime, boolean isBinding) {
        // The longest lap dictates the way the given lapTime must be formatted.
        final long longestLapTime = Math.max(DataModel.getDataModel().getLongestLapTime(), lapTime);
        final String formattedLapTime = formatTime(longestLapTime, lapTime, " ");
        final String formattedTime = formatTime(longestLapTime, lapTime, " ");

        // If the newly formatted lap time has altered the format, refresh all laps.
        if (mLastFormattedLapTimeLength != formattedLapTime.length()) {
            mLastFormattedLapTimeLength = formattedLapTime.length();
        final int newLength = formattedTime.length();
        if (!isBinding && mLastFormattedLapTimeLength != newLength) {
            mLastFormattedLapTimeLength = newLength;
            notifyDataSetChanged();
        }

        return formattedLapTime;
        return formattedTime;
    }

    /**
     * @param accumulatedTime the accumulated time to be formatted
     * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
     *                  set changes; they are not allowed to occur during bind
     * @return a formatted version of the accumulated time
     */
    private String formatAccumulatedTime(long accumulatedTime) {
    private String formatAccumulatedTime(long accumulatedTime, boolean isBinding) {
        final long totalTime = getStopwatch().getTotalTime();
        final long longestAccumulatedTime = Math.max(totalTime, accumulatedTime);
        final String formattedAccumulatedTime = formatTime(longestAccumulatedTime, accumulatedTime, " ");
        final String formattedTime = formatTime(longestAccumulatedTime, accumulatedTime, " ");

        // If the newly formatted accumulated time has altered the format, refresh all laps.
        if (mLastFormattedAccumulatedTimeLength != formattedAccumulatedTime.length()) {
            mLastFormattedAccumulatedTimeLength = formattedAccumulatedTime.length();
        final int newLength = formattedTime.length();
        if (!isBinding && mLastFormattedAccumulatedTimeLength != newLength) {
            mLastFormattedAccumulatedTimeLength = newLength;
            notifyDataSetChanged();
        }

        return formattedAccumulatedTime;
        return formattedTime;
    }

    private Stopwatch getStopwatch() {
@@ -306,16 +302,18 @@ class LapsAdapter extends BaseAdapter {
    /**
     * Cache the child views of each lap item view.
     */
    private static final class LapItemHolder {
    static final class LapItemHolder extends RecyclerView.ViewHolder {

        private final TextView lapNumber;
        private final TextView lapTime;
        private final TextView accumulatedTime;

        public LapItemHolder(TextView lapNumber, TextView lapTime, TextView accumulatedTime) {
            this.lapNumber = lapNumber;
            this.lapTime = lapTime;
            this.accumulatedTime = accumulatedTime;
        public LapItemHolder(View itemView) {
            super(itemView);

            lapTime = (TextView) itemView.findViewById(R.id.lap_time);
            lapNumber = (TextView) itemView.findViewById(R.id.lap_number);
            accumulatedTime = (TextView) itemView.findViewById(R.id.lap_total);
        }
    }
}
 No newline at end of file
+18 −35
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.SystemClock;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.transition.AutoTransition;
import android.transition.Transition;
import android.transition.TransitionManager;
@@ -29,13 +31,11 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.widget.ListView;

import com.android.deskclock.DeskClock;
import com.android.deskclock.DeskClockFragment;
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
import com.android.deskclock.Utils;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.data.Lap;
import com.android.deskclock.data.Stopwatch;
@@ -69,11 +69,14 @@ public final class StopwatchFragment extends DeskClockFragment {
    /** The data source for {@link #mLapsList}. */
    private LapsAdapter mLapsAdapter;

    /** The layout manager for the {@link #mLapsAdapter}. */
    private LinearLayoutManager mLapsLayoutManager;

    /** Draws the reference lap while the stopwatch is running. */
    private StopwatchTimer mTime;

    /** Displays the recorded lap times. */
    private ListView mLapsList;
    private RecyclerView mLapsList;

    /** Displays the current stopwatch time. */
    private CountingTimerView mTimeText;
@@ -87,11 +90,13 @@ public final class StopwatchFragment extends DeskClockFragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
        mLapsAdapter = new LapsAdapter(getActivity());
        mLapsLayoutManager = new LinearLayoutManager(getActivity());

        final View v = inflater.inflate(R.layout.stopwatch_fragment, container, false);
        mTime = (StopwatchTimer) v.findViewById(R.id.stopwatch_time);
        mLapsList = (ListView) v.findViewById(R.id.laps_list);
        mLapsList.setDividerHeight(0);
        mLapsList = (RecyclerView) v.findViewById(R.id.laps_list);
        mLapsList.getItemAnimator().setSupportsChangeAnimations(false);
        mLapsList.setLayoutManager(mLapsLayoutManager);
        mLapsList.setAdapter(mLapsAdapter);

        // Timer text serves as a virtual start/stop button.
@@ -375,24 +380,10 @@ public final class StopwatchFragment extends DeskClockFragment {

            // Recording the first lap transitions the UI to display the laps list.
            showOrHideLaps(false);
        }

        } else {
            if (mLapsList.getFirstVisiblePosition() > 0) {
        // Ensure the newly added lap is visible on screen.
                mLapsList.smoothScrollToPosition(0);
            } else {
                // Avoid nasty bugs in the Transition framework prior to L MR1.
                // Without the transition, the new lap just appears on screen instantaneously.
                if (Utils.isLMR1OrLater()) {
                    // Ignore updates to the current lap while adding the new recorded lap.
                    final Transition transition = new AutoTransition();
                    transition.excludeChildren(R.id.lap_view, true);

                    final ViewGroup sceneRoot = (ViewGroup) getView();
                    TransitionManager.beginDelayedTransition(sceneRoot, transition);
                }
            }
        }
        mLapsList.scrollToPosition(0);
    }

    /**
@@ -431,7 +422,7 @@ public final class StopwatchFragment extends DeskClockFragment {
            mLapsAdapter.clearLaps();
        }

        final boolean lapsVisible = mLapsAdapter.getCount() > 0;
        final boolean lapsVisible = mLapsAdapter.getItemCount() > 0;
        mLapsList.setVisibility(lapsVisible ? VISIBLE : GONE);
    }

@@ -495,18 +486,10 @@ public final class StopwatchFragment extends DeskClockFragment {
        // Update the total time display.
        mTimeText.setTime(totalTime, true, true);

        // Update the current lap if one exists and is visible on the screen.
        final boolean lapsExist = mLapsAdapter.getCount() > 0;
        final boolean currentLapIsVisible = mLapsList.getFirstVisiblePosition() == 0;
        if (!mLapsListIsTransitioning && lapsExist && currentLapIsVisible) {
            final View currentLapView = mLapsList.getChildAt(0);
            if (currentLapView != null) {
                // Compute the lap time using the total time.
                final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);

        // Update the current lap.
                mLapsAdapter.updateCurrentLap(currentLapView, lapTime, totalTime);
            }
        final boolean currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0;
        if (!mLapsListIsTransitioning && currentLapIsVisible) {
            mLapsAdapter.updateCurrentLap(mLapsList, totalTime);
        }
    }