Skip to content
This repository was archived by the owner on Mar 3, 2023. It is now read-only.

Android Discover Users Screen

Miroslav Smukov edited this page Jul 17, 2016 · 8 revisions

Discover Users Screen

I envisioned my app as a social app that will connect nearby users that have similar interests and goals. To achieve this functionality I need to offer my users a way to discover such users in their vicinity, and send them a contact request so they can start communicating. For this I decided to implement a Discover Users screen that will show the list of nearby users. The user will be able to swipe through this list and send the contact requests to other users. Let's see how I implemented the UI for this screen.

Available Resources

Again, Android's maturity is proving to be a great advantage. I managed to find an official documentation that literally hold your hand and guides you through the process of creating an activity with a list of full screen fragments that you can swipe through - exactly what I had in mind.

You can find the original post here, but I'm also going to cover some key things that I had to do in order to make it work in my application.

Create User Details Layout

We need to create a layout that will represent an individual user details. Multiple instances of this layout will be inflated and hosted inside the ViewPager. This is a similar approach to creating a layout for individual rows inside a ListView.

I won't be pasting here this layout as it's a bit long and at the same time not special in any way. I already created similar layouts in the past in my other posts. Instead, you can follow this link to see the source code.

In addition to this layout I also created another simple layout that will be displayed when there is no more users nearby to send invites to. You can see this layout below:

Source Code

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textSize="18sp"
        android:textStyle="bold"
        android:text="No More Users Nearby"
        android:gravity="center"
        android:paddingBottom="100dp"/>

</LinearLayout>

Create the Fragments

Now, let's create the fragments that will use the layouts that we just defined above. Instances of these fragments will be created and added to ViewPager to represent an individual user to which a contact request can be sent. However, let's first define a simple layout for our No More Users Nearby message:

Source Code

public class NoMoreUsersFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(
                R.layout.no_more_users_discover_layout, container, false);

        return rootView;
    }
}

That was easy. No let's create a fragment class for individual contacts:

Source Code

public class DiscoverUserFragment extends Fragment{

    private Contact contact;

    TextView profileName;
    TextView employment;
    TextView education;
    TextView interests;
    TextView knowledgeable;
    TextView currentGoals;


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(
                R.layout.discover_contact_layout, container, false);

        Bundle bundle = getArguments();
        String contactJSON = bundle.getString("contact");
        this.contact = new Gson().fromJson(contactJSON, Contact.class);

        return rootView;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        profileName = (TextView) view.findViewById(R.id.profile_name);
        employment = (TextView) view.findViewById(R.id.txtEmployment);
        education = (TextView) view.findViewById(R.id.txtEducation);
        interests = (TextView) view.findViewById(R.id.txtInterests);
        knowledgeable = (TextView) view.findViewById(R.id.txtKnowledgeable);
        currentGoals = (TextView) view.findViewById(R.id.txtCurrentGoals);

        prepareUI(contact);
    }

    private void prepareUI(Contact contact){
        profileName.setText(contact.getFullName());
        employment.setText(contact.getEmployment());
        education.setText(contact.getEducation());
        interests.setText(contact.getInterests());
        knowledgeable.setText(contact.getKnowledgeableIn());
        currentGoals.setText(contact.getCurrentGoals());
    }

}

This one is a bit more complicated, but still very simple to understand. All it does is inflate the discover_contact_layout and populates its views with appropriate Contact data.

The Contact data is sent to fragment via setArguments(bundle) (we'll cover this call a little bit down the road). We are then able to get back this data by calling getArguments() method and obtaining the sent Bundle. Since contact data is sent in a form of a JSON string, we are using the Gson library. You can read here about how I added this library to my Android project.

Create a Slider Layout

Now let's move to the layout and activity that will host the ViewPager.

ViewPager

A ViewPager is a layout manager that allows the user to flip left and right through pages of data. ViewPager is most often used in conjunction with Fragment, which is a convenient way to supply and manage the lifecycle of each page. There are standard adapters implemented for using fragments with the ViewPager, which cover the most common use cases.

We'll start by creating a basic activity. To do this we first right-click on our root package folder, and then go to New > Activity > Basic Activity. We give a name to our activity DiscoverUsersSliderActivity and set the NavigationActivity as its Hierarchical Parent. After we click on Finish button, the activity class and layout will be created.

Let's see the code for the layout in activity_discover_users_slider.xml:

Source Code

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:fitsSystemWindows="true"
    >

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_accept"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/ic_done_black_24dp"
        android:tint="@color/colorBackground"/>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_dismiss"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_marginBottom="85dp"
        android:layout_marginEnd="@dimen/fab_margin"
        android:src="@drawable/ic_close_black_24dp"
        android:tint="@color/colorBackground"/>

    <android.support.v4.view.ViewPager
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="20dp"/>

</android.support.design.widget.CoordinatorLayout>

The root component for this layout will be a CoordinatorLayout because of its ability to show Floating Action Buttons (FABs). We'll add the two FABs - one for sending a contact request, and the other for dismissing the contact suggestion - one under the other. We are using the android:layout_marginBottom to position the second FAB above the first one.

However, the key part of this layout is the ViewPager that we added. This is where our list of fragments will be rendered, and this is what will handle the swipe events.

Create a Slider Adapter

Before we implement the Slider Activity, let's add one more crucial piece of the puzzle, the FragmentStatePagerAdapter that we'll call DiscoverUsersPagerAdapter:

Source Code

public class DiscoverUsersPagerAdapter  extends FragmentStatePagerAdapter {

    private final List<Contact> lstContacts;
    private int itemToDelete = -1;

    public DiscoverUsersPagerAdapter(FragmentManager fm, List<Contact> contacts) {
        super(fm);
        lstContacts = contacts;
    }

    @Override
    public Fragment getItem(int position) {
        if (lstContacts != null) {

            if(lstContacts.size() == 0 || position >= lstContacts.size()){
                return new NoMoreUsersFragment();
            }
            else {
                DiscoverUserFragment duf = new DiscoverUserFragment();

                Bundle bundle = new Bundle();
                bundle.putString("contact", new Gson().toJson(lstContacts.get(position)));
                duf.setArguments(bundle);

                return duf;
            }

        } else {
            return new NoMoreUsersFragment();
        }
    }

    @Override
    public int getCount() {
        //take into account the last "No More Users.." page
        if (lstContacts != null) {
            return lstContacts.size() + 1;
        } else {
            return 1;
        }
    }

    public void setItemToDelete(int position){
        this.itemToDelete = position;
    }

    public int getItemToDelete(){
        return this.itemToDelete;
    }

    public void add(Contact contact) {
        lstContacts.add(contact);
    }

    public void add(List<Contact> contacts) {
        lstContacts.addAll(contacts);
    }

    public void remove(int position){
        if(lstContacts.size() != 0 && position < lstContacts.size()) {
            lstContacts.remove(position);
        }
    }

}

The idea with this adapter is similar to ListAdapters that we covered before. This adapter will hold the data that will bind to ViewPager.

ViewPager will call the getItem(position) method to get the appropriate view to render. If there is no contacts in the adapter, the getItem(position) will return the NoMoreUsersFragment with the appropriate message. However, if there is a contact at the requested position, a DiscoverUserFragment will be instantiated, and we'll provide the contact data to the fragment by using the setArguments(bundle) method.

Also, we'll use the getItemToDelete() and setItemToDelete(..) methods for a little hack that we'll have to do use in our slider activity.

Create a Slider Activity

Finally, the last piece of the puzzle, the actual slider activity that we called DiscoverUsersSliderActivity. You can see the full source code for this class here, as I won't be pasting it here since its too large. I'll only past smaller, interesting bits that I'll try to explain as best as I can.

fabAccept = (FloatingActionButton) findViewById(R.id.fab_accept);
fabAccept.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if(pager.getCurrentItem() < pagerAdapter.getCount()-1){
            removeContact(pager.getCurrentItem());
            //TODO: update db
        }
    }
});
fabDismiss = (FloatingActionButton) findViewById(R.id.fab_dismiss);
fabDismiss.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if(pager.getCurrentItem() < pagerAdapter.getCount()-1){
            removeContact(pager.getCurrentItem());
            //TODO: update db
        }
    }
});

The above code gets the references to our two FABs and sets the onClickListener on them. The listeners are the same at the moment, but they'll change in time once we implement different actions that need to be handled depending if the user is dismissing the contact, or sending a contact request.

The two onClickListeners are calling the removeContact(..) method in order to remove the current contact (fragment) from the ViewPager.

public void removeContact(int position) {
 pagerAdapter.setItemToDelete(position);
 pager.setCurrentItem(position+1);
}

The removeContact(..) method above is setting the position of the item to be deleted in the pagerAdapter, and we'll soon see why. The pager.setCurrentItem(..) call will tell the ViewPager to slide automatically to the requested item, which will trigger some OnPageChange events that we are handling with the code below.

pager = (ViewPager) findViewById(R.id.pager);
pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if(position == pagerAdapter.getCount()-1){
            fabAccept.hide();
            fabDismiss.hide();
        }else{
            fabAccept.show();
            fabDismiss.show();
        }
    }

    @Override
    public void onPageSelected(int position) {

    }

    @Override
    public void onPageScrollStateChanged(int state) {
        switch(state)
        {
            case ViewPager.SCROLL_STATE_DRAGGING:

                break;

            case ViewPager.SCROLL_STATE_IDLE:
                //this happens when the smooth scroll ends
                //check if there is any pending item to be deleted
                int pendingDeletePosition = pagerAdapter.getItemToDelete();
                if(pendingDeletePosition != -1){
                    pagerAdapter.setItemToDelete(-1);

                    //We need to do this so that ViewPager would delete all its views.
                    pager.setAdapter(null);

                    //now remove the item from the adapter, reassign it, and scroll to next item
                    pagerAdapter.remove(pendingDeletePosition);
                    pager.setAdapter(pagerAdapter);
                    pager.setCurrentItem(pendingDeletePosition);
                }

                break;

            case ViewPager.SCROLL_STATE_SETTLING:
                break;
        }
    }
});

The above events will get triggered by the users slide inputs, but, more importantly, by the pager.setCurrentItem(..) call in our removeContact(..) method.

The onPageScrolled event is simply checking if we are watching at the No More Users Nearby message, in which case we want to hide the two FABs.

The onPageScrollStateChanged handles a couple of different page scroll states. The state that we are interested in is the ViewPager.SCROLL_STATE_IDLE that happens when the smooth scroll ends. Here we are checking if there's an item set to be deleted in our pagerAdapter. This item will be set by the removeContact(..) method. If there is an item to be deleted, we need to go through a few steps to handle this removal appropriately.

First, we need to set the adapter on our ViewPager to null, so that ViewPager would delete all its views. Then we need to actually remove the contact from our pagerAdapter so it doesn't appear again. Next, we assign again the same pagerAdapter instance back to our ViewPager, and finally, we are scrolling back to the next item that should be displayed.

It took me a while to figure out these steps and, a disclaimer, this isn't a common approach that is recommended anywhere in the official documentation, this is just something that works for me. Simply removing the item from the FragmentStatePagerAdapter wouldn't be handled correctly by the ViewPager for some reason. By using the approach above I get the ability to remove items dynamically, and keep the smooth sliding animation to the next item in a queue.

Conclusion

This wasn't a walk in a park really, but it could have been worse. Android's maturity came into play once more, since I found a detailed documentation that guides me through the steps of creating an activity with a list of full screen fragments that you can swipe through. However, I did had some problems to implement a dynamic ViewPager from which I can remove pages and slide to next one simultaneously, without leaving garbage.

Discover Users Screen

References

Commits

Clone this wiki locally