Improve stopwatch tab layout for multiwindow

Bug: 26840878
Change-Id: Ic0e15f3394c74c9998001797e24af91406321996
diff --git a/Android.mk b/Android.mk
index 906dab8..884085a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -6,6 +6,7 @@
 
 ifeq ($(TARGET_BUILD_APPS),)
 LOCAL_RESOURCE_DIR += frameworks/support/design/res
+LOCAL_RESOURCE_DIR += frameworks/support/v14/percent/res
 LOCAL_RESOURCE_DIR += frameworks/support/v14/preference/res
 LOCAL_RESOURCE_DIR += frameworks/support/v7/appcompat/res
 LOCAL_RESOURCE_DIR += frameworks/support/v7/gridlayout/res
@@ -13,6 +14,7 @@
 LOCAL_RESOURCE_DIR += frameworks/support/v7/recyclerview/res
 else
 LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/design/res
+LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/v14/percent/res
 LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/v14/preference/res
 LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/v7/appcompat/res
 LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/v7/gridlayout/res
@@ -31,6 +33,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES := android-opt-datetimepicker
 LOCAL_STATIC_JAVA_LIBRARIES += android-support-design
 LOCAL_STATIC_JAVA_LIBRARIES += android-support-v13
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v14-percent
 LOCAL_STATIC_JAVA_LIBRARIES += android-support-v14-preference
 LOCAL_STATIC_JAVA_LIBRARIES += android-support-v7-appcompat
 LOCAL_STATIC_JAVA_LIBRARIES += android-support-v7-gridlayout
@@ -39,6 +42,7 @@
 
 LOCAL_AAPT_FLAGS := --auto-add-overlay
 LOCAL_AAPT_FLAGS += --extra-packages android.support.design
+LOCAL_AAPT_FLAGS += --extra-packages android.support.v14.percent
 LOCAL_AAPT_FLAGS += --extra-packages android.support.v14.preference
 LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.appcompat
 LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.gridlayout
diff --git a/res/layout-land/stopwatch_fragment.xml b/res/layout-land/stopwatch_fragment.xml
index 779aba4..ca87895 100644
--- a/res/layout-land/stopwatch_fragment.xml
+++ b/res/layout-land/stopwatch_fragment.xml
@@ -13,47 +13,51 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
+
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     android:baselineAligned="false"
-    android:orientation="horizontal"
-    android:paddingBottom="@dimen/fab_height">
+    android:orientation="horizontal">
 
-    <!-- This FrameLayout reserves half the screen for the stopwatch. -->
-    <FrameLayout
+    <!-- Left gutter. -->
+    <Space
         android:layout_width="0dp"
         android:layout_height="match_parent"
-        android:layout_weight="1">
+        android:layout_weight="@integer/gutter_width_percent" />
 
-        <!-- This FrameLayout draws the stopwatch centered within its half of the screen. -->
+    <!-- Guttered content. -->
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="@integer/guttered_content_width_percent"
+        android:orientation="horizontal">
+
         <FrameLayout
-            android:layout_width="@dimen/circle_size"
-            android:layout_height="@dimen/circle_size"
-            android:layout_gravity="center"
-            android:gravity="center"
-            android:padding="16dp">
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:paddingBottom="@dimen/fab_height">
 
-            <com.android.deskclock.timer.CountingTimerView
-                android:id="@+id/stopwatch_time_text"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent" />
+            <include layout="@layout/stopwatch_view" />
 
-            <com.android.deskclock.stopwatch.StopwatchCircleView
-                android:id="@+id/stopwatch_time"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:background="@null" />
         </FrameLayout>
 
-    </FrameLayout>
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/laps_list"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:clipToPadding="false"
+            android:paddingBottom="@dimen/fab_height" />
 
-    <android.support.v7.widget.RecyclerView
-        android:id="@+id/laps_list"
+    </LinearLayout>
+
+    <!-- Right gutter. -->
+    <Space
         android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:layout_weight="1" />
+        android:layout_height="match_parent"
+        android:layout_weight="@integer/gutter_width_percent" />
 
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout-sw320dp/stopwatch_view.xml b/res/layout-sw320dp/stopwatch_view.xml
new file mode 100644
index 0000000..5a75818
--- /dev/null
+++ b/res/layout-sw320dp/stopwatch_view.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Sufficient space exists to include the bounding stopwatch circle. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <com.android.deskclock.timer.CountingTimerView
+        android:id="@+id/stopwatch_time_text"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <com.android.deskclock.stopwatch.StopwatchCircleView
+        android:id="@+id/stopwatch_time"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@null" />
+
+</merge>
\ No newline at end of file
diff --git a/res/layout/stopwatch_fragment.xml b/res/layout/stopwatch_fragment.xml
index 5ce57af..0be82f1 100644
--- a/res/layout/stopwatch_fragment.xml
+++ b/res/layout/stopwatch_fragment.xml
@@ -13,38 +13,39 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
+
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:baselineAligned="false"
     android:gravity="center"
-    android:orientation="vertical"
-    android:paddingBottom="@dimen/fab_height">
+    android:baselineAligned="false"
+    android:orientation="vertical">
 
-    <FrameLayout
-        android:layout_width="@dimen/circle_size"
-        android:layout_height="@dimen/circle_size"
-        android:layout_gravity="center"
-        android:padding="16dp">
+    <android.support.percent.PercentFrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center">
 
-        <com.android.deskclock.timer.CountingTimerView
-            android:id="@+id/stopwatch_time_text"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
+        <FrameLayout
+            android:layout_gravity="center"
+            app:layout_aspectRatio="100%"
+            app:layout_widthPercent="60%">
 
-        <com.android.deskclock.stopwatch.StopwatchCircleView
-            android:id="@+id/stopwatch_time"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:background="@null" />
-    </FrameLayout>
+            <include layout="@layout/stopwatch_view" />
+
+        </FrameLayout>
+
+    </android.support.percent.PercentFrameLayout>
 
     <android.support.v7.widget.RecyclerView
         android:id="@+id/laps_list"
         android:layout_width="match_parent"
         android:layout_height="0dp"
+        android:layout_weight="1"
         android:layout_gravity="center"
-        android:layout_weight="1" />
+        android:clipToPadding="false"
+        android:paddingBottom="@dimen/fab_height" />
 
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout/stopwatch_view.xml b/res/layout/stopwatch_view.xml
new file mode 100644
index 0000000..ecc4322
--- /dev/null
+++ b/res/layout/stopwatch_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Insufficient space exists to include the bounding stopwatch circle. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <com.android.deskclock.timer.CountingTimerView
+        android:id="@+id/stopwatch_time_text"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</merge>
\ No newline at end of file
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
index ff7fef4..bbec0e7 100644
--- a/res/values-land/dimens.xml
+++ b/res/values-land/dimens.xml
@@ -42,9 +42,6 @@
     <dimen name="medium_font_size">48sp</dimen>
 
     <dimen name="circle_size">190dip</dimen>
-    <dimen name="stopwatch_circle_margin_bottom">80dip</dimen>
-
-    <dimen name="sw_padding_end">8dp</dimen>
 
     <dimen name="alarm_alert_clock_padding_left">32dp</dimen>
 
diff --git a/res/values-sw600dp-land/dimens.xml b/res/values-sw600dp-land/dimens.xml
index d31c950..06650e6 100644
--- a/res/values-sw600dp-land/dimens.xml
+++ b/res/values-sw600dp-land/dimens.xml
@@ -36,9 +36,6 @@
     <dimen name="analog_clock_margin">48dp</dimen>
 
     <dimen name="circle_size">360dip</dimen>
-    <dimen name="stopwatch_circle_margin_bottom">112dip</dimen>
-
-    <dimen name="sw_padding_end">48dp</dimen>
 
     <!-- The maximum size of the font for the time in widgets. -->
     <dimen name="widget_max_clock_font_size">100dp</dimen>
diff --git a/src/com/android/alarmclock/WidgetUtils.java b/src/com/android/alarmclock/WidgetUtils.java
index e3099cc..be7bf33 100644
--- a/src/com/android/alarmclock/WidgetUtils.java
+++ b/src/com/android/alarmclock/WidgetUtils.java
@@ -18,11 +18,11 @@
 
 import android.appwidget.AppWidgetManager;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.os.Bundle;
 
 import com.android.deskclock.R;
+import com.android.deskclock.Utils;
 
 public final class WidgetUtils {
 
@@ -44,7 +44,7 @@
                 // No data , do no scaling
                 return 1f;
             }
-            Resources res = context.getResources();
+            final Resources res = context.getResources();
             float density = res.getDisplayMetrics().density;
             float ratio = (density * minWidth) / res.getDimension(R.dimen.min_digital_widget_width);
             ratio = Math.min(ratio, getHeightScaleRatio(context, options, id));
@@ -55,7 +55,7 @@
             }
 
             ratio = Math.min(ratio, 1.6f);
-            if (res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+            if (Utils.isPortrait(context)) {
                 ratio = Math.max(ratio, .71f);
             }
             else {
@@ -82,10 +82,10 @@
                 // No data , do no scaling
                 return 1f;
             }
-            Resources res = context.getResources();
+            final Resources res = context.getResources();
             float density = res.getDisplayMetrics().density;
             float ratio = density * minHeight / res.getDimension(R.dimen.min_digital_widget_height);
-            if (res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+            if (Utils.isPortrait(context)) {
                 return ratio * 1.75f;
             }
             return ratio;
diff --git a/src/com/android/deskclock/Utils.java b/src/com/android/deskclock/Utils.java
index 381db21..148a2bd 100644
--- a/src/com/android/deskclock/Utils.java
+++ b/src/com/android/deskclock/Utils.java
@@ -77,6 +77,8 @@
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY;
 import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.graphics.Bitmap.Config.ARGB_8888;
 
 public class Utils {
@@ -653,4 +655,20 @@
 
         return PreferenceManager.getDefaultSharedPreferences(storageContext);
     }
+
+    /**
+     * @param context from which to query the current device configuration
+     * @return {@code true} if the device is currently in portrait or reverse portrait orientation
+     */
+    public static boolean isPortrait(Context context) {
+        return context.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
+    }
+
+    /**
+     * @param context from which to query the current device configuration
+     * @return {@code true} if the device is currently in landscape or reverse landscape orientation
+     */
+    public static boolean isLandscape(Context context) {
+        return context.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE;
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchFragment.java b/src/com/android/deskclock/stopwatch/StopwatchFragment.java
index 420bab3..333ce42 100644
--- a/src/com/android/deskclock/stopwatch/StopwatchFragment.java
+++ b/src/com/android/deskclock/stopwatch/StopwatchFragment.java
@@ -20,6 +20,7 @@
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
@@ -106,10 +107,19 @@
         mLapsList = (RecyclerView) v.findViewById(R.id.laps_list);
         ((SimpleItemAnimator) mLapsList.getItemAnimator()).setSupportsChangeAnimations(false);
         mLapsList.setLayoutManager(mLapsLayoutManager);
+
+        // In landscape layouts, the laps list can reach the top of the screen and thus can cause
+        // a drop shadow to appear. The same is not true for portrait landscapes.
+        if (Utils.isLandscape(getActivity())) {
+            final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher();
+            mLapsList.addOnLayoutChangeListener(scrollPositionWatcher);
+            mLapsList.addOnScrollListener(scrollPositionWatcher);
+        }
         mLapsList.setAdapter(mLapsAdapter);
 
         // Timer text serves as a virtual start/stop button.
         mTimeText = (CountingTimerView) v.findViewById(R.id.stopwatch_time_text);
+        mTimeText.setShowBoundingCircle(mTime != null);
         mTimeText.setVirtualButtonEnabled(true);
         mTimeText.registerVirtualButtonAction(new ToggleStopwatchRunnable());
 
@@ -119,8 +129,8 @@
     }
 
     @Override
-    public void onResume() {
-        super.onResume();
+    public void onStart() {
+        super.onStart();
 
         // Conservatively assume the data in the adapter has changed while the fragment was paused.
         mLapCount = mLapsAdapter.getItemCount();
@@ -134,8 +144,8 @@
     }
 
     @Override
-    public void onPause() {
-        super.onPause();
+    public void onStop() {
+        super.onStop();
 
         // Stop all updates while the fragment is not visible.
         stopUpdatingTime();
@@ -350,8 +360,10 @@
             // to ensure they aren't seen as the first lap is drawn.
             mLapsList.removeAllViewsInLayout();
 
-            // Start animating the reference lap.
-            mTime.update();
+            if (mTime != null) {
+                // Start animating the reference lap.
+                mTime.update();
+            }
 
             // Recording the first lap transitions the UI to display the laps list.
             showOrHideLaps(false);
@@ -367,6 +379,10 @@
      */
     private void showOrHideLaps(boolean clearLaps) {
         final ViewGroup sceneRoot = (ViewGroup) getView();
+        if (sceneRoot == null) {
+            return;
+        }
+
         TransitionManager.beginDelayedTransition(sceneRoot);
 
         if (clearLaps) {
@@ -375,6 +391,17 @@
 
         final boolean lapsVisible = mLapsAdapter.getItemCount() > 0;
         mLapsList.setVisibility(lapsVisible ? VISIBLE : GONE);
+
+        if (Utils.isPortrait(getActivity())) {
+            // When the lap list is visible, it includes the bottom padding. When it is absent the
+            // appropriate bottom padding must be applied to the container.
+            final Resources res = getResources();
+            final int bottom = lapsVisible ? 0 : res.getDimensionPixelSize(R.dimen.fab_height);
+            final int top = sceneRoot.getPaddingTop();
+            final int left = sceneRoot.getPaddingLeft();
+            final int right = sceneRoot.getPaddingRight();
+            sceneRoot.setPadding(left, top, right, bottom);
+        }
     }
 
     private void adjustWakeLock() {
@@ -415,14 +442,14 @@
     private void startUpdatingTime() {
         // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
         stopUpdatingTime();
-        mTime.post(mTimeUpdateRunnable);
+        mTimeText.post(mTimeUpdateRunnable);
     }
 
     /**
      * Remove the runnable that updates times within the UI.
      */
     private void stopUpdatingTime() {
-        mTime.removeCallbacks(mTimeUpdateRunnable);
+        mTimeText.removeCallbacks(mTimeUpdateRunnable);
     }
 
     /**
@@ -454,7 +481,9 @@
         // Draw the latest stopwatch and current lap times.
         updateTime();
 
-        mTime.update();
+        if (mTime != null) {
+            mTime.update();
+        }
 
         // Start updates if the stopwatch is running.
         final Stopwatch stopwatch = getStopwatch();
@@ -488,7 +517,7 @@
                 final long endTime = SystemClock.elapsedRealtime();
                 final long delay = Math.max(0, startTime + REDRAW_PERIOD - endTime);
 
-                mTime.postDelayed(this, delay);
+                mTimeText.postDelayed(this, delay);
             }
         }
     }
@@ -531,4 +560,22 @@
         public void lapAdded(Lap lap) {
         }
     }
-}
+
+    /**
+     * Updates the vertical scroll state of this tab in the {@link UiDataModel} as the user scrolls
+     * the recyclerview or when the size/position of elements within the recyclerview changes.
+     */
+    private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
+            implements View.OnLayoutChangeListener {
+        @Override
+        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mLapsList));
+        }
+
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                int oldLeft, int oldTop, int oldRight, int oldBottom) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mLapsList));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/CountingTimerView.java b/src/com/android/deskclock/timer/CountingTimerView.java
index b87c644..e276543 100644
--- a/src/com/android/deskclock/timer/CountingTimerView.java
+++ b/src/com/android/deskclock/timer/CountingTimerView.java
@@ -85,6 +85,10 @@
     private boolean mVirtualButtonEnabled = false;
     private boolean mVirtualButtonPressedOn = false;
 
+    // Whether or not a bounding circle exists into which the text must be made to fit.
+    // If no such circle exists, the entire width of this component is available for text display.
+    private boolean mShowBoundingCircle;
+
     Runnable mBlinkThread = new Runnable() {
         private boolean mVisible = true;
         @Override
@@ -310,6 +314,11 @@
         mPaintMed.setColor(textColor);
     }
 
+    public void setShowBoundingCircle(boolean showBoundingCircle) {
+        mShowBoundingCircle = showBoundingCircle;
+        requestLayout();
+    }
+
     /**
      * Update the time to display. Separates that time into the hours, minutes, seconds and
      * hundredths. If update is true, the view is invalidated so that it will draw again.
@@ -420,10 +429,18 @@
      */
     private void setTotalTextWidth() {
         calcTotalTextWidth();
-        // To determine the maximum width, we find the minimum of the height and width (since the
-        // circle we are trying to fit the text into has its radius sized to the smaller of the
-        // two.
-        int width = Math.min(getWidth(), getHeight());
+
+        int width;
+        if (mShowBoundingCircle) {
+            // A bounding circle exists, so the available width in which to fit the timer text is
+            // the smaller of the width or height, which is also equal to the circle's diameter.
+            width = Math.min(getWidth(), getHeight());
+        } else {
+            // A bounding circle does not exist, so pretend that the entire width of this component
+            // is the diameter of a theoretical bounding circle.
+            width = getWidth();
+        }
+
         if (width != 0) {
             // Shrink 'width' to account for circle stroke and other painted objects.
             // Note on the "4 *": (1) To reduce divisions, using the diameter instead of the radius.