Show FPS in UI state

BUG=skia:
GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2043793002

Review-Url: https://codereview.chromium.org/2043793002
diff --git a/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/StateAdapter.java b/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/StateAdapter.java
index 4c9dcd4..d546c7b 100644
--- a/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/StateAdapter.java
+++ b/platform_tools/android/apps/viewer/src/main/java/org/skia/viewer/StateAdapter.java
@@ -6,6 +6,7 @@
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
 import android.widget.Spinner;
 import android.widget.TextView;
 
@@ -15,12 +16,23 @@
 
 import java.util.ArrayList;
 
+/*
+    The navigation drawer requires ListView, so we implemented this BaseAdapter for that ListView.
+    However, the ListView does not provide good support for updating just a single child view.
+    For example, a frequently changed child view such as FPS state will reset the spinner of
+    all other child views; although I didn't change other child views and directly return
+    the convertView in BaseAdapter.getView(int position, View convertView, ViewGroup parent).
+
+    Therefore, our adapter only returns one LinearLayout for the ListView.
+    Within that LinearLayout, we maintain views ourselves so we can efficiently update its children.
+ */
 public class StateAdapter extends BaseAdapter implements AdapterView.OnItemSelectedListener {
     static final String NAME = "name";
     static final String VALUE = "value";
     static final String OPTIONS = "options";
 
     ViewerActivity mViewerActivity;
+    LinearLayout mLayout;
     JSONArray mStateJson;
 
     public StateAdapter(ViewerActivity viewerActivity) {
@@ -36,7 +48,11 @@
     public void setState(String stateJson) {
         try {
             mStateJson = new JSONArray(stateJson);
-            notifyDataSetChanged();
+            if (mLayout != null) {
+                updateDrawer();
+            } else {
+                notifyDataSetChanged();
+            }
         } catch (JSONException e) {
             e.printStackTrace();
         }
@@ -44,17 +60,12 @@
 
     @Override
     public int getCount() {
-        return mStateJson.length();
+        return 1;
     }
 
     @Override
     public Object getItem(int position) {
-        try {
-            return mStateJson.getJSONObject(position);
-        } catch (JSONException e) {
-            e.printStackTrace();
-            return null;
-        }
+        return null;
     }
 
     @Override
@@ -64,39 +75,66 @@
 
     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
-        if (convertView == null) {
-            convertView = LayoutInflater.from(mViewerActivity).inflate(R.layout.state_item, null);
+        if (mLayout == null) {
+            mLayout = new LinearLayout(mViewerActivity);
+            mLayout.setOrientation(LinearLayout.VERTICAL);
+            updateDrawer();
         }
-        TextView nameText = (TextView) convertView.findViewById(R.id.nameText);
-        TextView valueText = (TextView) convertView.findViewById(R.id.valueText);
-        Spinner optionSpinner = (Spinner) convertView.findViewById(R.id.optionSpinner);
-        JSONObject stateObject = (JSONObject) getItem(position);
-        try {
-            nameText.setText(stateObject.getString(NAME));
-            String value = stateObject.getString(VALUE);
-            JSONArray options = stateObject.getJSONArray(OPTIONS);
-            if (options.length() == 0) {
-                valueText.setText(value);
-                valueText.setVisibility(View.VISIBLE);
-                optionSpinner.setVisibility(View.GONE);
+        return mLayout;
+    }
 
-            } else {
-                ArrayList<String> optionList = new ArrayList<>();
-                String[] optionStrings = new String[options.length()];
-                for(int i=0; i<options.length(); i++) {
-                    optionList.add(options.getString(i));
+    private View inflateItemView(JSONObject item) throws JSONException {
+        View itemView = LayoutInflater.from(mViewerActivity).inflate(R.layout.state_item, null);
+        TextView nameText = (TextView) itemView.findViewById(R.id.nameText);
+        TextView valueText = (TextView) itemView.findViewById(R.id.valueText);
+        Spinner optionSpinner = (Spinner) itemView.findViewById(R.id.optionSpinner);
+        nameText.setText(item.getString(NAME));
+        String value = item.getString(VALUE);
+        JSONArray options = item.getJSONArray(OPTIONS);
+        if (options.length() == 0) {
+            valueText.setText(value);
+            valueText.setVisibility(View.VISIBLE);
+            optionSpinner.setVisibility(View.GONE);
+
+        } else {
+            ArrayList<String> optionList = new ArrayList<>();
+            String[] optionStrings = new String[options.length()];
+            for (int j = 0; j < options.length(); j++) {
+                optionList.add(options.getString(j));
+            }
+            optionSpinner.setAdapter(new ArrayAdapter<String>(mViewerActivity,
+                    android.R.layout.simple_spinner_dropdown_item, optionList));
+            optionSpinner.setSelection(optionList.indexOf(value));
+            optionSpinner.setOnItemSelectedListener(this);
+            optionSpinner.setVisibility(View.VISIBLE);
+            valueText.setVisibility(View.GONE);
+        }
+        itemView.setTag(item.toString()); // To save unnecessary view update
+        itemView.setTag(R.integer.value_tag_key, value); // To save unnecessary state change event
+        return itemView;
+    }
+
+    private void updateDrawer() {
+        try {
+            if (mStateJson.length() < mLayout.getChildCount()) {
+                mLayout.removeViews(
+                        mStateJson.length(), mLayout.getChildCount() - mStateJson.length());
+            }
+            for (int i = 0; i < mStateJson.length(); i++) {
+                JSONObject stateObject = mStateJson.getJSONObject(i);
+                if (mLayout.getChildCount() > i) {
+                    View childView = mLayout.getChildAt(i);
+                    if (stateObject.toString().equals(childView.getTag())) {
+                        continue; // No update, reuse the old view and skip the remaining step
+                    } else {
+                        mLayout.removeViewAt(i);
+                    }
                 }
-                optionSpinner.setAdapter(new ArrayAdapter<String>(mViewerActivity,
-                        android.R.layout.simple_spinner_dropdown_item, optionList));
-                optionSpinner.setSelection(optionList.indexOf(value));
-                optionSpinner.setOnItemSelectedListener(this);
-                optionSpinner.setVisibility(View.VISIBLE);
-                valueText.setVisibility(View.GONE);
+                mLayout.addView(inflateItemView(stateObject), i);
             }
         } catch (JSONException e) {
             e.printStackTrace();
         }
-        return convertView;
     }
 
     @Override
@@ -104,7 +142,10 @@
         View stateItem = (View) parent.getParent();
         String stateName = ((TextView) stateItem.findViewById(R.id.nameText)).getText().toString();
         String stateValue = ((TextView) view).getText().toString();
-        mViewerActivity.onStateChanged(stateName, stateValue);
+        if (!stateValue.equals(stateItem.getTag(R.integer.value_tag_key))) {
+            stateItem.setTag(null); // Reset the tag to let updateDrawer update this item view.
+            mViewerActivity.onStateChanged(stateName, stateValue);
+        }
     }
 
     @Override
diff --git a/platform_tools/android/apps/viewer/src/main/res/values/integers.xml b/platform_tools/android/apps/viewer/src/main/res/values/integers.xml
new file mode 100644
index 0000000..d3da8e7
--- /dev/null
+++ b/platform_tools/android/apps/viewer/src/main/res/values/integers.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <integer name="value_tag_key">1</integer>
+</resources>
\ No newline at end of file
diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp
index c11b82b..de4c4cc 100644
--- a/tools/viewer/Viewer.cpp
+++ b/tools/viewer/Viewer.cpp
@@ -67,7 +67,7 @@
 const char* kBackendStateName = "Backend";
 const char* kSoftkeyStateName = "Softkey";
 const char* kSoftkeyHint = "Please select a softkey";
-
+const char* kFpsStateName = "FPS";
 
 Viewer::Viewer(int argc, char** argv, void* platformData)
     : fCurrentMeasurement(0)
@@ -170,6 +170,8 @@
 }
 
 void Viewer::initSlides() {
+    fAllSlideNames = Json::Value(Json::arrayValue);
+
     const skiagm::GMRegistry* gms(skiagm::GMRegistry::Head());
     while (gms) {
         SkAutoTDelete<skiagm::GM> gm(gms->factory()(nullptr));
@@ -410,6 +412,7 @@
     fAnimTimer.updateTime();
     if (fSlides[fCurrentSlide]->animate(fAnimTimer) || fDisplayStats) {
         fWindow->inval();
+        updateUIState(); // Update the FPS
     }
 }
 
@@ -418,11 +421,12 @@
     Json::Value slideState(Json::objectValue);
     slideState[kName] = kSlideStateName;
     slideState[kValue] = fSlides[fCurrentSlide]->getName().c_str();
-    Json::Value allSlideNames(Json::arrayValue);
-    for(auto slide : fSlides) {
-        allSlideNames.append(Json::Value(slide->getName().c_str()));
+    if (fAllSlideNames.size() == 0) {
+        for(auto slide : fSlides) {
+            fAllSlideNames.append(Json::Value(slide->getName().c_str()));
+        }
     }
-    slideState[kOptions] = allSlideNames;
+    slideState[kOptions] = fAllSlideNames;
 
     // Backend state
     Json::Value backendState(Json::objectValue);
@@ -443,10 +447,20 @@
         softkeyState[kOptions].append(Json::Value(softkey.c_str()));
     }
 
+    // FPS state
+    Json::Value fpsState(Json::objectValue);
+    fpsState[kName] = kFpsStateName;
+    double measurement = fMeasurements[
+            (fCurrentMeasurement + (kMeasurementCount-1)) % kMeasurementCount
+    ];
+    fpsState[kValue] = SkStringPrintf("%8.3lf ms", measurement).c_str();
+    fpsState[kOptions] = Json::Value(Json::arrayValue);
+
     Json::Value state(Json::arrayValue);
     state.append(slideState);
     state.append(backendState);
     state.append(softkeyState);
+    state.append(fpsState);
 
     fWindow->setUIState(state);
 }
diff --git a/tools/viewer/Viewer.h b/tools/viewer/Viewer.h
index 4b260df..3a4164d 100644
--- a/tools/viewer/Viewer.h
+++ b/tools/viewer/Viewer.h
@@ -66,6 +66,8 @@
     // identity unless the window initially scales the content to fit the screen.
     SkMatrix               fDefaultMatrix;
     SkMatrix               fDefaultMatrixInv;
+
+    Json::Value            fAllSlideNames; // cache all slide names for fast updateUIState
 };