-
Notifications
You must be signed in to change notification settings - Fork 24
Android 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.
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.
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:
<?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>
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:
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:
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.
Now let's move to the layout and activity that will host the 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:
<?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.
Before we implement the Slider Activity, let's add one more crucial piece of the puzzle, the FragmentStatePagerAdapter
that we'll call DiscoverUsersPagerAdapter
:
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.
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.
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.
- Android
- Getting Started
- Project Structure
- Gradle
- Menu
- Theming
- Login Screen
- Menu & Navigation
- Importing Library From GitHub
- Creating Reusable Layouts
- Adding New Icons
- Profile Screen
- Chat Screen
- Contacts Screen
- Pending Invites Screen
- Settings Screen
- Add Library Dependency
- Discover Users Screen
- Splash Screen
- Auth0
- Authentication Logic
- Profile Screen Logic
- Send Feedback
- Authenticated to Firebase via Auth0
- Saving User Info to Firebase
- Storing User Connection Info to Firebase
- Calculating Distance Between Users
- Chat Logic
- Handling Device Rotation
- Android Other
- Ionic
- Getting Started
- Project Structure
- Menu
- Theming
- Login Screen
- Adding Images
- Creating Reusable Layouts
- Adding New Icons
- Profile Screen
- Contact Screen
- Elastic Textarea
- Chat Bubble
- Chat Screen
- Contacts Screen
- Pending Invites Screen
- Settings Screen
- Discover Users Screen
- Splash Screen
- Animations
- Auth0
- Storing User Data
- Profile Screen Logic
- Send Feedback
- Update to Ionic RC0
- Reimplemented Auth0 with Ionic RC0
- Authenticated to Firebase via Auth0
- Saving User Info to Firebase
- Storing User Connection Info to Firebase
- Calculating Distance Between Users
- Chat Logic
- Handling Device Rotation
- Ionic Other
- Version Updating
- Cordova
- Other
- Messaging