Android Tinder Swipe View Example

Android Tinder Swipe View Example

Tutorial using PlaceHolderView

In this example we will develop views and architecture to simulate the Tinder Swipe.

Objectives of This Tutorial

  1. We would be creating the swipe view as used in the Tinder. Swipe right is considered accepted and swipe left is rejected.
  2. As we can see in the above gif that there are a lot of stuffs happening in the scene. Gestures, animations, view integrity model, dynamic data, view management and lots more. All these are implemented in the SwipePlaceHolderView class in the PlaceHolderView library.
  3. We will load the images from urls and set it in the view display. For this purpose we will use a library Glide.
  4. The profile data list will be seeded in the application and this seed json file will be stored in the assets folder.
  5. The seed file will be parsed into Profile object using another library gson.
  6. This structure will also be compatible if we are pulling url json data from a live server.

SwipePlaceHolderView:

This view is a generic implementation of views in stacked form. The detail about this class can be found here

As we have been doing in my other tutorials, we will take step oriented approach to implement this view in a detailed fashion.

Let’s start the construction:

Step 1:

Set up the project in android studio with default activity.

In app’s build.gradle add the dependencies.

android {
    ...
    sourceSets {
        main {
            assets.srcDirs = ['src/main/assets', 'src/main/assets/']
            res.srcDirs = ['src/main/res', 'src/main/res/drawable']
        }
    }
}

dependencies {
    ...
    compile 'com.mindorks:placeholderview:0.7.1'
    compile 'com.android.support:cardview-v7:25.3.1'
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.google.code.gson:gson:2.7'
}

Notes:

  1. Add an assets folder in the src/main directory and point to it in gradle assets.srcDirs
  2. CardView is used to display the image in the deck

Add Internet permission in the app’s AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

Step 2:

Create src/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/grey">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:layout_gravity="bottom"
        android:gravity="center"
        android:orientation="horizontal">
        <ImageButton
            android:id="@+id/rejectBtn"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@drawable/ic_cancel"/>
        <ImageButton
            android:id="@+id/acceptBtn"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginLeft="30dp"
            android:background="@drawable/ic_heart"/>
    </LinearLayout>
    <com.mindorks.placeholderview.SwipePlaceHolderView
        android:id="@+id/swipeView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</FrameLayout>

Step 3:

Create src/layout/tinder_card_view.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="350dp"
    android:layout_height="425dp"
    android:layout_marginBottom="50dp"
    android:orientation="vertical">
    <android.support.v7.widget.CardView
        android:orientation="vertical"
        android:background="@color/white"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"
        app:cardCornerRadius="7dp"
        app:cardElevation="4dp">
        <ImageView
            android:id="@+id/profileImageView"
            android:scaleType="centerCrop"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="75dp"/>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="75dp"
            android:orientation="vertical"
            android:layout_gravity="bottom"
            android:gravity="center|left"
            android:paddingLeft="20dp">
            <TextView
                android:id="@+id/nameAgeTxt"
                android:layout_width="wrap_content"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="18dp"
                android:textStyle="bold"
                android:layout_height="wrap_content"/>
            <TextView
                android:id="@+id/locationNameTxt"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="14dp"
                android:textStyle="normal"/>
        </LinearLayout>
    </android.support.v7.widget.CardView>
</FrameLayout>

Notes:

  1. Used fixed size width so get a smooth interaction behavior with the card.

Step 4:

Create src/layout/tinder_swipe_in_msg_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="350dp"
    android:layout_height="425dp">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="32sp"
        android:textStyle="bold"
        android:layout_margin="40dp"
        android:textColor="@android:color/holo_green_light"
        android:text="Accept"/>
</LinearLayout>

Notes:

  1. This layout will be provided as swipe state accept/reject indicator on the card.
  2. To avoid text wrapping issue. Make the view of same size as the card in the previous step and then provide a message text at position wherever display is required.

Step 5:

Create src/layout/tinder_swipe_out_msg_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="350dp"
    android:gravity="right"
    android:layout_height="425dp">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="32sp"
        android:textStyle="bold"
        android:layout_margin="40dp"
        android:textColor="@android:color/holo_red_light"
        android:text="Reject"/>
</LinearLayout>

Step 6:

Place profiles.json file in the assets folder created in the above step 1.

[  
   {  
      "url":"https://pbs.twimg.com/profile_images/572905100960485376/GK09QnNG.jpeg",
      "name":"Sofia",
      "age":20,
      "location":"New York"
   },
   {  
      "url":"http://cdn.cavemancircus.com//wp-content/uploads/images/2015/june/pretty_girls_3/pretty_girls_15.jpg",
      "name":"Roma",
      "age":22,
      "location":"California"
   },
   {  
      "url":"http://i.imgur.com/N6SaAlZ.jpg",
      "name":"Zoya",
      "age":23,
      "location":"Atlantic City"
   },
   {  
      "url":"http://cdn.cavemancircus.com//wp-content/uploads/images/2015/january/pretty_girls_2/pretty_girls_5.jpg",
      "name":"Carol",
      "age":19,
      "location":"New City"
   },
   {  
      "url":"http://www.qqxxzx.com/images/pretty-girls-pictures/pretty-girls-pictures-17.jpg",
      "name":"Saansa",
      "age":20,
      "location":"Manhattan"
   },
   {  
      "url":"http://www.qqxxzx.com/images/pretty-girls-pictures/pretty-girls-pictures-18.jpg",
      "name":"Khaleesi",
      "age":24,
      "location":"Future Westros"
   },
   {  
      "url":"http://oddpad.com/wp-content/uploads/2015/05/pretty_girls_51.jpg",
      "name":"Heena",
      "age":21,
      "location":"India"
   },
   {  
      "url":"http://oddpad.com/wp-content/uploads/2014/12/pretty_girls_13.jpg",
      "name":"Jasmin",
      "age":23,
      "location":"Taxes"
   },
   {  
      "url":"http://www.spyderonlines.com/images/wallpapers/pretty-girls-pictures/pretty-girls-pictures-16.jpg",
      "name":"Monica",
      "age":23,
      "location":"New York"
   },
   {  
      "url":"http://cdn.cavemancircus.com//wp-content/uploads/images/2015/june/pretty_girls_3/pretty_girls_20.jpg",
      "name":"Phoebe",
      "age":22,
      "location":"New York City"
   },
   {  
      "url":"http://cdn.cavemancircus.com//wp-content/uploads/images/2015/march/pretty_girls_2/pretty_girls_12.jpg",
      "name":"Rachel",
      "age":22,
      "location":"Central Perk"
   },
   {  
      "url":"https://s-media-cache-ak0.pinimg.com/736x/de/87/7b/de877bcccc2295a58fe8758fee0ebc7d.jpg",
      "name":"Jainis",
      "age":24,
      "location":"New York"
   },
   {  
      "url":"https://scontent.cdninstagram.com/hphotos-xpf1/t51.2885-15/e15/10986280_404995676329336_1177563605_n.jpg",
      "name":"Samia",
      "age":19,
      "location":"India"
   },
   {  
      "url":"http://scontent-a.cdninstagram.com/hphotos-xaf1/t51.2885-15/10624158_694652173966291_65999198_n.jpg",
      "name":"Hornol",
      "age":24,
      "location":"Caltech"
   },
   {  
      "url":"http://i.imgur.com/wqsvWT4.jpg",
      "name":"Korial",
      "age":21,
      "location":"Japan"
   },
   {  
      "url":"http://data.whicdn.com/images/60103426/large.jpg",
      "name":"Chin Si",
      "age":23,
      "location":"China"
   },
   {  
      "url":"http://oddpad.com/wp-content/uploads/2015/03/pretty_girls_31.jpg",
      "name":"SDF",
      "age":21,
      "location":"America"
   }
]

Notes:

  1. This strategy is very useful in bundling app with seed files. Seed files contain data build in the app package and can be used to populate database or use to display default data to the user. Placing seed files in the form of json makes is extremely easy to parse into models.

Step 7:

Create Utils.java

public class Utils {

    private static final String TAG = "Utils";

    public static List<Profile> loadProfiles(Context context){
        try{
            GsonBuilder builder = new GsonBuilder();
            Gson gson = builder.create();
            JSONArray array = new JSONArray(loadJSONFromAsset(context, "profiles.json"));
            List<Profile> profileList = new ArrayList<>();
            for(int i=0;i<array.length();i++){
                Profile profile = gson.fromJson(array.getString(i), Profile.class);
                profileList.add(profile);
            }
            return profileList;
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    private static String loadJSONFromAsset(Context context, String jsonFileName) {
        String json = null;
        InputStream is=null;
        try {
            AssetManager manager = context.getAssets();
            Log.d(TAG,"path "+jsonFileName);
            is = manager.open(jsonFileName);
            int size = is.available();
            byte[] buffer = new byte[size];
            is.read(buffer);
            is.close();
            json = new String(buffer, "UTF-8");
        } catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
        return json;
    }
}

Note:

  1. Utils contain methods required to parse seed json file and also populate the model Profile.java

Step 8:

Create model Profile.java

public class Profile {

    @SerializedName("name")
    @Expose
    private String name;

    @SerializedName("url")
    @Expose
    private String imageUrl;

    @SerializedName("age")
    @Expose
    private Integer age;

    @SerializedName("location")
    @Expose
    private String location;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getImageUrl() {
        return imageUrl;
    }

    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }
}

Notes:

  1. @SerializedName annotation belongs to gson class and used to read json file variable and bind it to the model variable.
  2. @Expose is used to make the variable readable to the gson

Step 9:

We will now create the class to bind the card view and its operations on layout.

Create TinderCard.java for the profile views.

@Layout(R.layout.tinder_card_view)
public class TinderCard {

    @View(R.id.profileImageView)
    private ImageView profileImageView;

    @View(R.id.nameAgeTxt)
    private TextView nameAgeTxt;

    @View(R.id.locationNameTxt)
    private TextView locationNameTxt;

    private Profile mProfile;
    private Context mContext;
    private SwipePlaceHolderView mSwipeView;

    public TinderCard(Context context, Profile profile, SwipePlaceHolderView swipeView) {
        mContext = context;
        mProfile = profile;
        mSwipeView = swipeView;
    }

    @Resolve
    private void onResolved(){
        Glide.with(mContext).load(mProfile.getImageUrl()).into(profileImageView);
        nameAgeTxt.setText(mProfile.getName() + ", " + mProfile.getAge());
        locationNameTxt.setText(mProfile.getLocation());
    }

    @SwipeOut
    private void onSwipedOut(){
        Log.d("EVENT", "onSwipedOut");
        mSwipeView.addView(this);
    }

    @SwipeCancelState
    private void onSwipeCancelState(){
        Log.d("EVENT", "onSwipeCancelState");
    }

    @SwipeIn
    private void onSwipeIn(){
        Log.d("EVENT", "onSwipedIn");
    }

    @SwipeInState
    private void onSwipeInState(){
        Log.d("EVENT", "onSwipeInState");
    }

    @SwipeOutState
    private void onSwipeOutState(){
        Log.d("EVENT", "onSwipeOutState");
    }
}

Notes:

  1. @layout is used to bind the layout with this class.
  2. @View is used to bind the views in a layout we want to refer to.
  3. @Resolve annotation bind a method to be executed when the view is ready to be used. Any operation we want to perform on view references should be written in a method and annotated with this.
  4. @SwipeOut calls the annotated method when the card has been rejected.
  5. @SwipeIn calls the annotated method when the card has be accepted.
  6. @SwipeCancelState calls the annotated method when the card is put back in the deck/canceled.
  7. @SwipeInState pings the annotated method till the card is moving in accepted state.
  8. @SwipeOutState pings the annotated method till the card is moving in rejected state.
  9. IMPORTANT: If we don’t plan to re add a view then the class should be annotated with @NonReusable so that the references are released and memory is optimises. For the demonstration of this tutorial we are adding a view back in the deck if rejected, so we have not used @NonReusable.

For detailed explanations view PlaceHolderView at GitHub repository

Step 10:

Create MainActivity.java

public class MainActivity extends AppCompatActivity {

    private SwipePlaceHolderView mSwipeView;
    private Context mContext;

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

        mSwipeView = (SwipePlaceHolderView)findViewById(R.id.swipeView);
        mContext = getApplicationContext();

        mSwipeView.getBuilder()
                .setDisplayViewCount(3)
                .setSwipeDecor(new SwipeDecor()
                        .setPaddingTop(20)
                        .setRelativeScale(0.01f)
                        .setSwipeInMsgLayoutId(R.layout.tinder_swipe_in_msg_view)
                        .setSwipeOutMsgLayoutId(R.layout.tinder_swipe_out_msg_view));


        for(Profile profile : Utils.loadProfiles(this.getApplicationContext())){
            mSwipeView.addView(new TinderCard(mContext, profile, mSwipeView));
        }

        findViewById(R.id.rejectBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mSwipeView.doSwipe(false);
            }
        });

        findViewById(R.id.acceptBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mSwipeView.doSwipe(true);
            }
        });
    }
}

Notes:

  1. We obtain the instance of the SwipePlaceHolderView.
  2. We then use the getBuilder() method to modify the default view configurations. In this example we are adding 3 cards in the display and keep adding next card when top card is removed.
  3. SwipeDecor class is used to adjust the visual elements of the view. Here paddingTop and relativeScale gives the perception of a card being placed in stack. The message for card stated is added through setSwipeInMsgLayoutId()and setSwipeOutMsgLayoutId().
  4. We load the json data and parse into Profile Object and add it to the SwipePlaceHolderView list using addView() method.
  5. To programmatically do swiping we call doSwipe() method with flag to indicating accepted or rejected swipe.

PlaceHolderView GitHub repository is here

Note: If you want to auto resize the cards and fix issue of card overlapping the like/dislike buttons for mobiles having bottom navigation bar. Then go through below link for the solution:

https://medium.com/@janishar.ali/modifying-android-tinder-swipe-view-example-to-support-auto-resize-94f9c64f641e

The source code for this example is here

Important developments since the 0.2.7 version:

  1. Dynamic view margin, Undo last swipe, Putback swiped view, Lock view
  2. Disable Swipe on Touch
  3. ItemRemovedListener added for SwipePlaceHolderView
  4. SwipeDirectionalView(Swipe Directions: useful for features like super like), Swipe Touch Callback, Animated Undo, Programmatically Expand/Collapse ExpandablePlaceHolderView
Android Tinder Swipe View Example

All the remaining release version information can be found here: Link

Also, Let’s become friends on Twitter, Linkedin, Github, and Facebook.

Learning is a journey, let’s learn together!