I'm making an intro section to my Android app using a ViewPager
containing a series of fragments. The fragment code is:
package testing.testmusic;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import butterknife.Bind;
import butterknife.ButterKnife;
public class IntroPageFragment extends Fragment {
/**
* Used during logging to identify this class.
*/
private static final String TAG = "[IntroPageFragment]";
/**
* Used to prevent premature attempts to update the UI.
*/
private boolean viewsBoundToInstanceVariables = false;
/**
* Holds and displays a {@code Bitmap} at the top of the screen. This {@code View} is drawn in
* front of {@code backImageHolder} but behind {@code contentHolder} by default.
*/
@Bind(R.id.intro_fragment_imageFront) protected ImageView frontImageHolder;
/**
* Holds and displays a {@code Bitmap} at the top of the screen. This {@code View} is drawn
* behind both {@code backImageHolder} and {@code contentHolder} by default.
*/
@Bind(R.id.intro_fragment_imageBack) protected ImageView backImageHolder;
/**
* Displays custom content at the centre of the screen. This {@code View} is drawn in front of
* both {@code backImageHolder} and {@code contentHolder} by default.
*/
@Bind(R.id.intro_fragment_content) protected FrameLayout contentHolder;
/**
* The image to display in {@code frontImageHolder}.
*/
private Bitmap frontImage = null;
/**
* The image to display in {@code backImageHolder}.
*/
private Bitmap backImage = null;
/**
* The content to display in {@code contentHolder}.
*/
private View content = null;
/**
* Constructs a new IntroPageFragment instance. Avoid calling this method directly, instead
* call {@link #newInstance()}.
*/
public IntroPageFragment() {}
/**
* @return a new IntroPageFragment instance.
*/
public static IntroPageFragment newInstance() {
return new IntroPageFragment();
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
final View fragmentRoot = inflater.inflate(R.layout.fragment_intro_page, container, false);
ButterKnife.bind(this, fragmentRoot);
viewsBoundToInstanceVariables = true;
updateViewsToReflectData();
return fragmentRoot;
}
/**
* Displays the provided image at the top of the screen. The image will be drawn in front of
* any image supplied to {@link #setBackImage(Bitmap)}, but behind any {@code Views} supplied
* to {@link #setContent(View)} (by default).
*
* @param frontImage the image to display
* @return this {@code IntroPageFragment} to allow method chaining
*/
public IntroPageFragment setFrontImage(final Bitmap frontImage) {
this.frontImage = frontImage;
updateViewsToReflectData();
return this;
}
/**
* @return the ImageView which holds the image supplied to {@link #setFrontImage(Bitmap)}
*/
public ImageView getFrontImageHolder() {
return frontImageHolder;
}
/**
* Displays the provided image at the top of the screen. The image will be drawn in behind of
* any image supplied to {@link #setBackImage(Bitmap)}, and behind any {@code Views} supplied
* to {@link #setContent(View)} (by default).
*
* @param backImage the image to display
* @return this {@code IntroPageFragment} to allow method chaining
*/
public IntroPageFragment setBackImage(final Bitmap backImage) {
this.backImage = backImage;
updateViewsToReflectData();
return this;
}
/**
* @return the ImageView which holds the image supplied to {@link #setBackImage(Bitmap)}
*/
public ImageView getBackImageHolder() {
return backImageHolder;
}
/**
* Adds the provided content to a FrameLayout at the centre of the screen. The content will be
* drawn in front of any images supplied to {@link #setFrontImage(Bitmap)} and {@link
* #setBackImage (Bitmap)} (by default).
*
* @param content the content to display on this page
* @return this {@code IntroPageFragment} to allow method chaining
*/
public IntroPageFragment setContent(final View content) {
this.content = content;
updateViewsToReflectData();
return this;
}
/**
* @return the FrameLayout which displays the content supplied to {@link #setContent(View)}
*/
public FrameLayout getContentHolder() {
return contentHolder;
}
/**
* Updates all views to use the images and content supplied to {@link #setFrontImage(Bitmap)},
* {@link #setBackImage(Bitmap)} and {@link #setContent(View)}.
*/
public void updateViewsToReflectData() {
if (viewsBoundToInstanceVariables) {
frontImageHolder.setImageBitmap(frontImage);
backImageHolder.setImageBitmap(backImage);
contentHolder.removeAllViews();
if (content != null) {
contentHolder.addView(content);
contentHolder.invalidate();
}
}
}
}
The fragment layout is:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager
android:id="@+id/intro_activity_viewpager"
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:fitsSystemWindows="false"
tools:context="testing.testmusic.IntroActivity">
</android.support.v4.view.ViewPager>
The app launches and displays fragments as expected, however when scrolling through pages, the third scroll (either forward or back, doesn't make a difference) causes the following error:
FATAL EXCEPTION: main
Process: testing.testmusic, PID: 12404
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:4309)
at android.view.ViewGroup.addView(ViewGroup.java:4145)
at android.view.ViewGroup.addView(ViewGroup.java:4086)
at android.view.ViewGroup.addView(ViewGroup.java:4059)
at testing.testmusic.IntroPageFragment.updateViewsToReflectData(IntroPageFragment.java:158)
at testing.testmusic.IntroPageFragment.onCreateView(IntroPageFragment.java:80)
at android.support.v4.app.Fragment.performCreateView(Fragment.java:1962)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1067)
at android.support.v4.app.FragmentManagerImpl.attachFragment(FragmentManager.java:1426)
at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:728)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1613)
at android.support.v4.app.FragmentManagerImpl.executePendingTransactions(FragmentManager.java:570)
at android.support.v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:141)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1106)
at android.support.v4.view.ViewPager.populate(ViewPager.java:952)
at android.support.v4.view.ViewPager$3.run(ViewPager.java:251)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858)
at android.view.Choreographer.doCallbacks(Choreographer.java:670)
at android.view.Choreographer.doFrame(Choreographer.java:603)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
The trace leads to method updateViewsToReflectData
in IntroPageFragment
, specifically at contentHolder.addView(content)
. I don't understand why this error is occurring since contentHolder.removeAllViews()
is always called directly prior. Why is this error occurring?
In case it helps, here is my activity code:
package testing.testmusic;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import butterknife.Bind;
import butterknife.ButterKnife;
public class IntroActivity extends AppCompatActivity {
/**
* Used during logging to identify this class.
*/
private static final String TAG = "[IntroActivity]";
/**
* Displays an interactive multi-page intro screen to the user.
*/
@Bind(R.id.intro_activity_viewpager) protected ViewPager viewPager;
/**
* Adapts the elements of {@code fragments} to views in {@code viewPager}.
*/
private IntroPageAdapter adapter = new IntroPageAdapter(getSupportFragmentManager());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_introduction);
ButterKnife.bind(this);
viewPager.setAdapter(adapter);
createPages();
}
/**
* Populates {@code fragments} with the pages to display in {@code viewPager}.
*/
private void createPages() {
Bitmap rainbow = BitmapHelper
.decodeSampledBitmapFromResource(getResources(), R.raw.rainbow, 100, 100);
Bitmap frontDots = BitmapHelper
.decodeSampledBitmapFromResource(getResources(), R.raw.front, 1000, 1000);
Bitmap backDots = BitmapHelper
.decodeSampledBitmapFromResource(getResources(), R.raw.back, 1000, 1000);
for (int i = 0; i < 10; i++) {
IntroPageFragment page = IntroPageFragment.newInstance();
page.setFrontImage(frontDots);
page.setBackImage(backDots);
ImageView content = new ImageView(this);
content.setImageBitmap(rainbow);
page.setContent(content);
adapter.addPage(page);
}
}
}
and here is my adapter code:
package testing.testmusic;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import java.util.ArrayList;
import java.util.List;
public class IntroPageAdapter extends FragmentPagerAdapter {
List<IntroPageFragment> fragments;
public IntroPageAdapter(FragmentManager fm) {
super(fm);
this.fragments = new ArrayList<>();
}
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
public void addPage(IntroPageFragment page) {
if (page != null) {
fragments.add(page);
notifyDataSetChanged();
}
}
public void removePage(IntroPageFragment page) {
fragments.remove(page);
notifyDataSetChanged();
}
}