Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fragment in round Wear watch turning black in emulator/watch

Im getting a black screen on my Android Wear round watch (on square it works). Both layouts show up OK in the Android Studio layout design.

Project to test:

https://bitbucket.org/powder366/motoblack

Im using the following code:

public class MyFragment extends Fragment {
...
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View contentView = inflater.inflate(R.layout.my_fragment, container, false);
...

Layout my_fragment:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/my_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:keepScreenOn="true"
    tools:context=".WearFragment">

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text1"
        android:text="Hi1"
        android:layout_gravity="center_horizontal"
        style="@style/UnitsStyle"/>

    <TextView
        android:id="@+id/text2"
        android:text="Hi2"
        android:layout_gravity="center_horizontal"
        style="@style/BaseStyle"/>

    <TextView
        android:id="@+id/text3"
        android:text="Hi3"
        android:layout_gravity="center_horizontal"
        style="@style/UnitsStyle"/>

</LinearLayout>

</LinearLayout>

Calling it from:

public class MenuAdapter extends FragmentGridPagerAdapter
...
    public MenuAdapter(Context context, FragmentManager fm, Handler handler) {
    super(fm);
    mContext = context;
    mHandler = handler;
    myFragment = new MyFragment();
}
...
@Override
public Fragment getFragment(int rowNum, int colNum) {
    Log.d(TAG, String.format("getFragment(%d, %d)", rowNum, colNum));

    if(colNum == 0) {
        return myFragment;
    }

    if(colNum == 1) {
        final SecondFragment switchAction = SecondFragment.newInstance(mHandler);
        return switchAction;
    }

    return null;
}
...

Which is called from:

public class WearActivity extends Activity {
...

private GridViewPager gridViewPager;
private MenuAdapter menuAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
    Log.d(TAG, "Create(Wear)");
    super.onCreate(savedInstanceState);
    setContentView(R.layout.wear_activity);

    final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
    stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
        @Override
        public void onLayoutInflated(WatchViewStub stub) {
            menuAdapter = new MenuAdapter(WearActivity.this, getFragmentManager(), mHandler);
            gridViewPager = (GridViewPager) findViewById(R.id.pager);
            gridViewPager.setAdapter(menuAdapter);
        }
    });
}
...

Have anyone seen this or can give me a hint on how to solve it?

Update:

wear_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WatchViewStub
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/watch_view_stub"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:rectLayout="@layout/rect_activity"
  app:roundLayout="@layout/round_activity"
  tools:context=".WearActivity"
  tools:deviceIds="wear">
</android.support.wearable.view.WatchViewStub>

rect_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:layout_gravity="center"
  tools:deviceIds="wear_square">

<com.xyz.my.ScrollableGridViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true"/>

</FrameLayout>

round_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:layout_gravity="center"
  tools:deviceIds="wear_round">

<com.xyz.my.ScrollableGridViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true"/>

</FrameLayout>

Extra info:

public class ScrollableGridViewPager extends GridViewPager {
...

enter image description hereenter image description here

like image 810
powder366 Avatar asked Sep 12 '14 12:09

powder366


1 Answers

The problem is not so trivial, so I hope that I've described it pretty clearly.

Reason #1:

The real reason of this issue is that the onLayoutInflated(WatchViewStub) method is called twice on the round device.

It is invoked for the first time when the rect_activity.xml is inflated:

WearActivity$1.onLayoutInflated(WatchViewStub) line: 26 
WatchViewStub.inflate() line: 133   
WatchViewStub.onMeasure(int, int) line: 141 
WatchViewStub(View).measure(int, int) line: 16648   

But right after that the second call to this method is done with newly inflated round_activity.xml when onApplyWindowInsets(WindowInsets) is dispatched:

WearActivity$1.onLayoutInflated(WatchViewStub) line: 26 
WatchViewStub.inflate() line: 133   
WatchViewStub.onApplyWindowInsets(WindowInsets) line: 112   
WatchViewStub(View).dispatchApplyWindowInsets(WindowInsets) line: 6051  
WatchViewStub(ViewGroup).dispatchApplyWindowInsets(WindowInsets) line: 5435 
SwipeDismissLayout(ViewGroup).dispatchApplyWindowInsets(WindowInsets) line: 5439    
PhoneWindow$DecorView(ViewGroup).dispatchApplyWindowInsets(WindowInsets) line: 5439 
ViewRootImpl.dispatchApplyInsets(View) line: 1170   

I don't know whether this issue occurs only on emulator or on real device also, but I think this should be reported as a bug in wearable-support-lib (in WatchViewStub component to be precise). Such behavior is not documented anywhere.

Reason #2 (visual consequences of #1):

The issue above sounds like it should do pretty nothing... New layout is inflated (so the old one is removed), new MenuAdapter will be created (the old one will not be used any more so who cares), and this newly created MenuAdapter should be set as an adapter in new GridViewPager. Everything should "reset" so it should still works fine (apart that the performance issue due to the fact that layout is inflated two times instead of just once).

Not exactly... because here is the tricky part with GridViewPager.

I don't know the exact source codes of GridViewPager and FragmentGridPagerAdapter but it seems to be similar to ViewPager and FragmentPagerAdapter code. FragmentPagerAdapter adds newly created fragment to the FragmentManager with special tag that is composed with id of the ViewPager and position of given child fragment:

122    private static String More ...makeFragmentName(int viewId, int index) {
123        return "android:switcher:" + viewId + ":" + index;
124    }

Once a fragment is added to the FragmentManager (in current Activity) it can be referenced by this tag name in future. The real problem is caused by the fact that onLayoutInflated(WatchViewStub) method is called twice. The first fragment is created in first MenuAdapter and added to the container with id R.id.pager (from the rect_activity.xml). Then round_activity.xml is inflated and second call to onLayoutInflated(WatchViewStub) is invoked - new fragment is created in new MenuAdapter and added to newly inflated GridViewPager with the same id: R.id.pager. So the first fragment and second one want to have exactly the same tag in FragmentManager.

In standard FragmentPagerAdapter if fragment with given tag is already present in FragmentManager it is attached to it's previous parent again. You can see source codes here:

50     @Override
51     public Object More ...instantiateItem(View container, int position) {
52         if (mCurTransaction == null) {
53             mCurTransaction = mFragmentManager.beginTransaction();
54         }
55 
56         // Do we already have this fragment?
57         String name = makeFragmentName(container.getId(), position);
58         Fragment fragment = mFragmentManager.findFragmentByTag(name);
59         if (fragment != null) {
60             if (DEBUG) Log.v(TAG, "Attaching item #" + position + ": f=" + fragment);
61             mCurTransaction.attach(fragment);
62         } else {
63             fragment = getItem(position);
64             if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
65             mCurTransaction.add(container.getId(), fragment,
66                     makeFragmentName(container.getId(), position));
67         }
68         if (fragment != mCurrentPrimaryItem) {
69             fragment.setMenuVisibility(false);
70         }
71 
72         return fragment;
73     }

So probably the situation is similar and the first fragment is reused, which was added to the first GridViewPager with id R.id.pager - but this parent is not there any more so the fragment cannot be attached anywhere (and be displayed). This is the implementation thing that there can be only one ViewPager-like parent with given id in one Activity.

For one more experiment I've set different ids for both GridViewPagers. One with rect_activity.xml has id R.id.pager1 and another one in round_activity.xml has `R.id.pager2'.

final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
    stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
        @Override
        public void onLayoutInflated(WatchViewStub stub) {
            menuAdapter = new MenuAdapter(WearActivity.this, getFragmentManager());
            GridViewPager gridViewPager1 = (GridViewPager) findViewById(R.id.pager1);
            if(gridViewPager1!=null) {          // rect_activity
                gridViewPager1.setAdapter(menuAdapter);
            }

            GridViewPager gridViewPager2 = (GridViewPager) findViewById(R.id.pager2);
            if(gridViewPager2!=null) {          // round_activity
                gridViewPager2.setAdapter(menuAdapter);
            }
        }
    });

In first pass only gridViewPager1 is found and only its adapter is set. Similar in the second pass - only adapter for gridViewPager2 is set. Because GridViewPagers have different ids there is no collision in fragment tags, so the end result is as expected.
But this is only to prove the theory described above:)

enter image description here

Solution:

WatchViewStub is used to provide different layouts for rect and round screens. I can see that you use exactly the same layouts for both shapes. You can get rid of the WatchViewStub and just do like this:

public class WearActivity extends Activity {

    private GridViewPager gridViewPager;
    private MenuAdapter menuAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.rect_activity);

        menuAdapter = new MenuAdapter(WearActivity.this, getFragmentManager());
        gridViewPager = (GridViewPager) findViewById(R.id.pager);
        gridViewPager.setAdapter(menuAdapter);
    }
}

this will make your example work perfectly without the use of WatchViewStub.

like image 93
Maciej Ciemięga Avatar answered Sep 19 '22 16:09

Maciej Ciemięga