Chap 11 Challenge - Adding First and Last Buttons(+bonus)


#1

Hello,

I think I solved the challenge correctly but was curious on if this was the best possible solution:

In the layout, I added a FrameLayout as the root and added the 2 buttons and view group below.

activity_crime_pager.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:text="First"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/first_button"
        android:layout_gravity="bottom"
        android:layout_margin="16dp"/>
    <Button
        android:text="Last"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/last_button"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp"/>
    <android.support.v4.view.ViewPager
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/crime_view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp">
    </android.support.v4.view.ViewPager>
</FrameLayout>

In CrimePagerActivity.java, I used the “setCurrentItem” method of ViewPager to find the first and last items and set the button logic to perform this. I also implemented “onPageScrolled” for mViewPager to make the respective buttons invisible if it was the first or last crime:

private Button mFirstButton;
private Button mLastButton;

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

    UUID crimeId = (UUID) getIntent().getSerializableExtra(EXTRA_CRIME_ID);

    mViewPager = (ViewPager) findViewById(R.id.crime_view_pager);
    mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener () {

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            if (mViewPager.getCurrentItem() == 0) {
                mFirstButton.setVisibility(View.INVISIBLE);
            } else {
                mFirstButton.setVisibility(View.VISIBLE);
            }

            if(position == mViewPager.getAdapter().getCount()) {
                mLastButton.setVisibility(View.INVISIBLE);
            }

            if (mViewPager.getCurrentItem() == (mCrimes.size() - 1)) {
                mLastButton.setVisibility(View.INVISIBLE);
            } else {
                mLastButton.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public void onPageSelected(int position) {
        }

        @Override
        public void onPageScrollStateChanged(int state) {
        }

    });

    mFirstButton = (Button) findViewById(R.id.first_button);
    mFirstButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mViewPager.setCurrentItem(0);
        }
    });

    mLastButton = (Button) findViewById(R.id.last_button);
    mLastButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mViewPager.setCurrentItem(mViewPager.getAdapter().getCount());
       }
    });

My main question is, was this the optimal approach?

Also, is there any difference to using “mViewPager.getAdapter().getCount()” or “mCrimes.size() - 1”. I used both here and they seemed to work equally.


#2

There is a bug in your code. If you go to crime #98 and then swipe right to go to crime #99 the Last button will still be there. This is because mViewPager.getAdapter().getCount() returns the full count of the dataset in the adapter which will be 1 more than the last position.

To fix it just change the line:
mViewPager.getAdapter().getCount()
to
mViewPager.getAdapter().getCount() - 1


#3

You also can create the first and last buttons like menu items.
https://developer.android.com/training/animation/screen-slide.html


#4

Instead of using the ViewPager.OnPageChangeListener interface, you can create a private class which extends ViewPager.SimpleOnPageChangeListener. This way you can override any of the original interface methods, without leaving the other ones unimplemented.

Both solutions work, but showing unimplemented methods is quite ugly in my opinion :slight_smile: Just a small thing, but maybe it helps someone cleaning up their code.

PS. You asked: “Also, is there any difference to using “mViewPager.getAdapter().getCount()” or “mCrimes.size() - 1”. I used both here and they seemed to work equally.” There’s no difference between the two, since the mViewPager count is set by the size of mCrimes.


#5

if you use FrameLayout then the text of Title is overlapped with buttons. I am using ConstraintLayout.

I use this logic to control the locking of buttons:

       public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            mButtonHome.setEnabled((position+positionOffset > 0));

            mButtonEnd.setEnabled((position+positionOffset < (mCrimes.size()-1)));
        }

#6

Hey. I don’t know if this helps, but there’s also the ViewPager.SimpleOnPageChangeListener class.
it’s already implemented the interface methods and you can just overwrite the ones you need to. hth


#7

Why do use void onPageScrolled(int, float , int) ?

May be simple:

viewPager.addOnPageChangeListener(new SimpleOnPageChangeListener() {
  @Override public void onPageSelected(int position) {
    moveToFirstButton.setEnabled(position > 0);
    moveToLastButton.setEnabled(position < crimes.size() - 1);
  }
});

#8

For the layout, I used an outer LinearLayout (vertical orientation) containing the ViewPager and then a ConstraintLayout for the buttons at the bottom left and right corners of the screen:

<?xml version="1.0" encoding="utf-8"?>
<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:orientation="vertical">

    <android.support.v4.view.ViewPager
        android:id="@+id/crime_view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="top"
        android:layout_weight="1">

    </android.support.v4.view.ViewPager>

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_weight="0">

        <Button
            android:id="@+id/jump_to_first_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:text="@string/jump_to_first_button_text"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/jump_to_last_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginTop="8dp"
            android:text="@string/jump_to_last_button_text"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </android.support.constraint.ConstraintLayout>

</LinearLayout>

For the logic, I did not need to implement any new methods, just wire up the buttons and add an instance variable to keep track of the last index. In CrimePagerActivity, I added

private Button mJumpToFirstButton;
private Button mJumpToLastButton;
private int lastIndex;

In OnCreate I added:

// Hook up previous and next buttons

lastIndex = mCrimes.size() - 1;

mJumpToFirstButton = findViewById(R.id.jump_to_first_button);
mJumpToLastButton = findViewById(R.id.jump_to_last_button);

mJumpToFirstButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        mViewPager.setCurrentItem(0, true);
        toggleButtons();
    }
});

mJumpToLastButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        mViewPager.setCurrentItem(lastIndex, true);
        toggleButtons();
    }
});

And finally, my new toggleButtons method:

private void toggleButtons() {
        if (mViewPager.getCurrentItem() == 0) {
            mJumpToFirstButton.setEnabled(false);
        }
        else {
            mJumpToFirstButton.setEnabled(true);
        }

        if (mViewPager.getCurrentItem() == lastIndex) {
            mJumpToLastButton.setEnabled(false);
        }
        else {
            mJumpToLastButton.setEnabled(true);
        }
    }

You just need the toggleButtons() call when either button is clicked, after changing out the content, and then in the adapter’s getItem() method (when swiping left or right).


#9

But if you move to the beginning of the Crimes using swipes, that won’t work


#10

The only thing that doesn’t work with my solution is that when you swipe all the way to the beginning or end, it doesn’t disable the button to jump to the beginning or end. However, when you press those buttons they do get disabled, not that it really matters.