View: fix setTransientState

Following four steps with RecyclerView - CardView - ImageView:
a. fade in imageview in onBind:
calling imageView.setTransientState(true)
b. RecyclerView animate the item: calling
cardView.setTransientState(true)
c. fade-in finishes, calling imageView.setTransientState(false)
d. RecyclerView animation finishes: calling
cardView.setTransientState(false)

After these four steps, RecyclerView unexpectedly has transient
state.

The problem is in step b, when calling cardView.setTransientState()
it incorrectly calls parent.childHasTransientStateChanged(this, true)
which causes RecyclerView's mChildCountWithTransientState increased
to 2. And it's decreased to 1 in step d and stay as 1 forever.

The child should only call childHasTransientStateChanged() when
actual hasTransientState() changed.

Bug: 64235615
Test: ViewTransientStateTest
Change-Id: I99ed35cc9c49e54d36590d8f1d206501fd3288f2
This commit is contained in:
Dake Gu
2017-08-01 13:42:42 -07:00
parent 0d7ff5328e
commit 43ad57e548
5 changed files with 187 additions and 2 deletions

View File

@@ -9711,6 +9711,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* @param hasTransientState true if this view has transient state
*/
public void setHasTransientState(boolean hasTransientState) {
final boolean oldHasTransientState = hasTransientState();
mTransientStateCount = hasTransientState ? mTransientStateCount + 1 :
mTransientStateCount - 1;
if (mTransientStateCount < 0) {
@@ -9722,9 +9723,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
// update flag if we've just incremented up from 0 or decremented down to 0
mPrivateFlags2 = (mPrivateFlags2 & ~PFLAG2_HAS_TRANSIENT_STATE) |
(hasTransientState ? PFLAG2_HAS_TRANSIENT_STATE : 0);
if (mParent != null) {
final boolean newHasTransientState = hasTransientState();
if (mParent != null && newHasTransientState != oldHasTransientState) {
try {
mParent.childHasTransientStateChanged(this, hasTransientState);
mParent.childHasTransientStateChanged(this, newHasTransientState);
} catch (AbstractMethodError e) {
Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
" does not fully implement ViewParent", e);

View File

@@ -978,6 +978,13 @@
</intent-filter>
</activity>
<activity android:name="android.view.ViewTransientState" android:label="View Transient State">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
</intent-filter>
</activity>
<activity android:name="android.view.RemoteViewsActivity" android:label="RemoteViewsActicity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2017, 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.
*/
-->
<!-- Demonstrates view transient state, See corresponding Java code. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/p1"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/p2"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/p3"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</TextView>
</FrameLayout>
</FrameLayout>
</FrameLayout>

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2007 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.
*/
package android.view;
import android.app.Activity;
import android.os.Bundle;
import com.android.frameworks.coretests.R;
/**
* Exercise set View's transient state
*/
public class ViewTransientState extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.view_transient_state);
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2017 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.
*/
package android.view;
import android.app.Activity;
import android.test.ActivityInstrumentationTestCase;
import android.test.UiThreadTest;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.frameworks.coretests.R;
import static org.junit.Assert.assertFalse;
/**
* Exercise set View's transient state
*/
public class ViewTransientStateTest extends ActivityInstrumentationTestCase<ViewTransientState> {
View mP1;
View mP2;
View mP3;
public ViewTransientStateTest() {
super("com.android.frameworks.coretests", ViewTransientState.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
final Activity a = getActivity();
mP1 = a.findViewById(R.id.p1);
mP2 = a.findViewById(R.id.p2);
mP3 = a.findViewById(R.id.p3);
}
@UiThreadTest
@MediumTest
public void testSetTransientState1() throws Exception {
mP3.setHasTransientState(true);
mP2.setHasTransientState(true);
mP3.setHasTransientState(false);
mP2.setHasTransientState(false);
assertFalse(mP3.hasTransientState());
assertFalse(mP2.hasTransientState());
assertFalse(mP1.hasTransientState());
}
@UiThreadTest
@MediumTest
public void testSetTransientState2() throws Exception {
mP3.setHasTransientState(true);
mP2.setHasTransientState(true);
mP2.setHasTransientState(false);
mP3.setHasTransientState(false);
assertFalse(mP3.hasTransientState());
assertFalse(mP2.hasTransientState());
assertFalse(mP1.hasTransientState());
}
@UiThreadTest
@MediumTest
public void testSetTransientState3() throws Exception {
mP2.setHasTransientState(true);
mP3.setHasTransientState(true);
mP3.setHasTransientState(false);
mP2.setHasTransientState(false);
assertFalse(mP3.hasTransientState());
assertFalse(mP2.hasTransientState());
assertFalse(mP1.hasTransientState());
}
@UiThreadTest
@MediumTest
public void testSetTransientState4() throws Exception {
mP2.setHasTransientState(true);
mP3.setHasTransientState(true);
mP2.setHasTransientState(false);
mP3.setHasTransientState(false);
assertFalse(mP3.hasTransientState());
assertFalse(mP2.hasTransientState());
assertFalse(mP1.hasTransientState());
}
}