What is 3D Touch?

The 3D Touch technology commonly known as Force Touch is developed by Apple Inc. which uses force sensors in track pads and touchscreens to distinguish between different levels of pressure in touch and respond accordingly.

3D Touch was introduced as a more sensitive version of Force Touch in Apple’s iPhone 6s and can be seen in all latest iPhone devices.

Why 3D Touch?

Basically 3D Touch was developed for giving user more control over their touch and to provide following features:

  • Quick Actions
    • Just hold the icon of any app and you can see the quick shortcuts of that app.
  • Peek and Pop
    • Preview contents without actually opening them.
  • Pressure Sensing
    • Provide feedback depending on touch pressure, can be used in drawing where user can draw thick or thin lines depending on pressure.

How to implement 3D Touch in Android?

First, let me answer your curious questions like:

  • Does Android devices support 3D Touch technology?
    • No, Android devices (as of now) don’t have hardware like Force Sensors.
  • So, how can we use 3D Touch without Hardware support in Android?
    • Actually, we can :).
    • Yes! We can simulate 3D Touch-like behaviour in Android devices with no additional hardware sensor required.
  • Okay, so where to start?
    • To get answer to this question, keep reading, it’s going to be fun.

Implementing Peek And Pop

Today, we are going to simulate the peek and pop behaviour of 3D Touch by creating a sample app which lets you peek any animal image from the list when you hold any one of them.

Peek and Pop Implementation Image
Peek and Pop Implementation

Following things are needed to be implemented to make it look like the demo image shown above:

  1. Implement a RecyclerView with GridLayout.
  2. Show animal images in that RecyclerView.
  3. Set up a method to detect the touch pressure.
  4. Open the dialog showing animal image and its name when any image from the list is touched with pressure.
  5. Blur the background.
  6. Dismiss the dialog when user lifts up the finger from the screen.
  7. Add animation to dialog opening and closing events.

Before we start, add following dependencies in build.gradle file of your app module :

compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support:recyclerview-v7:26.+'
compile 'com.android.support:cardview-v7:26.+'
compile 'com.squareup.picasso:picasso:2.5.2'

Create a new project named “3dTouchDemo” and create Animal model class inside “model” package.

public class Animal implements Serializable {

    private String name;
    private String description;
    private String url;

    public Animal(String name, String description, String url) {
        this.name = name;
        this.description = description;
        this.url = url;
    }
    
    //add getters and setters
}

Create a new Empty Activity named “AnimalListActivity” inside “view” package.

In the layout file of AnimalListActivity, add the following code:

<?xml version="1.0" encoding="utf-8"?>
 <FrameLayout
        android:id="@+id/frame_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:scaleType="centerCrop"
            android:clickable="true"
            android:id="@+id/img_bg"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <android.support.v7.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            android:id="@+id/recycler_list"
            android:clipToPadding="false"
            android:paddingBottom="16dp"/>
</FrameLayout>


We have added a FrameLayout with an ImageView and RecyclerView. ImageView is used because we are going to use it to show background blur when any item from the list is peeked.

Now that we’ve created the activity, create the list item layout for RecyclerView adapter.

<ImageView
       android:id="@+id/list_item_image"
       android:scaleType="centerCrop"
       android:layout_width="match_parent"
       android:layout_height="200dp"
       android:layout_margin="5dp"/>

Create a CustomRecyclerAdapter where we’ll provide the ArrayList<Animal>.

Load the image in onBindView method from url of current Animal object using Picasso.

Picasso.with(holder.imageView.getContext()).load(animal.getUrl()).into(holder.imageView);

Set GridLayoutManager with span count 2 for RecyclerView:

GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 2);
recyclerList.setLayoutManager(gridLayoutManager);

Now set dummy data into the CustomRecyclerAdapter:

ArrayList<Animal> animalArrayList = new ArrayList<>();
       animalArrayList.add(
               new Animal("Lion","Desc of Lion","https://i.pinimg.com/736x/07/29/97/072997211c5621934716a261d321034e--photos.jpg")
       );
       animalArrayList.add(
               new Animal("Tiger","Desc of Tiger","https://c402277.ssl.cf1.rackcdn.com/photos/1620/images/carousel_small/bengal-tiger-why-matter_7341043.jpg?1345548942")
       );
       animalArrayList.add(
               new Animal("Elephant","Desc of Elephant","https://img.purch.com/h/1000/aHR0cDovL3d3dy5saXZlc2NpZW5jZS5jb20vaW1hZ2VzL2kvMDAwLzAzNi85ODgvb3JpZ2luYWwvZWxlcGhhbnRzLmpwZw==")
       );
       animalArrayList.add(
               new Animal("Rabbit","Desc of Rabbit","https://www.thesun.co.uk/wp-content/uploads/2016/06/nintchdbpict000171898291-e1466207066744.jpg?w=960&strip=all")
       );
       animalArrayList.add(
               new Animal("Monkey","Desc of Monkey","http://assets.nydailynews.com/polopoly_fs/1.2753191.1471352717!/img/httpImage/image.jpg_gen/derivatives/article_750/483572672.jpg")
       );
       for(int i=0;i<4;i++)
       {
           for(int j=0;j<5;j++) {
               animalArrayList.add(animalArrayList.get(j));
           }
       }
       recyclerList.setAdapter(new CustomRecyclerAdapter(this,animalArrayList,mContext));

Now Create custom dialog layout which will be displayed when image is peeked:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardCornerRadius="15dp"
    app:cardUseCompatPadding="true"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/img"
            android:scaleType="centerCrop"
            android:layout_width="300dp"
            android:layout_height="300dp" />

        <TextView
            android:id="@+id/text_img_name"
            tools:text="AnimalName"
            android:gravity="center"
            android:padding="10dp"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:textStyle="italic"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</android.support.v7.widget.CardView>

We have used CardView for dialog where the animal image and animal name will be displayed.

To create a background blur, we’ll use the following logic:

  1. Before opening the dialog, create a Bitmap from the FrameLayout of AnimalListActivity.
  2. Blur that Bitmap using Renderscript.
  3. Set the blurred Bitmap to ImageView inside that FrameLayout and set the visibility of RecyclerView to GONE.
  4. Open the dialog.

To remove the background blur, we’ll use the following logic:

  1. Close the dialog.
  2. Set the visibility of RecyclerView to VISIBLE so that the blurred background image will not be visible as it is covered by the RecyclerView.

NOTE: Android by default dims the background when a dialog is opened. We don’t want that behaviour because we want the background to be blurred. So, to avoid background dim, add the following line to the AppTheme style in styles.xml.

<item name="android:backgroundDimEnabled">false</item>

Add the following method in your activity for background blur:

public Bitmap blurBitmap(Bitmap bitmap) {

        //Let's create an empty bitmap with the same size of the bitmap we want to blur
        Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);

        //Instantiate a new Renderscript
        RenderScript rs = RenderScript.create(mContext);

        //Create an Intrinsic Blur Script using the Renderscript
        ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

        //Create the in/out Allocations with the Renderscript and the in/out bitmaps
        Allocation allIn = Allocation.createFromBitmap(rs, bitmap);
        Allocation allOut = Allocation.createFromBitmap(rs, outBitmap);

        //Set the radius of the blur
        blurScript.setRadius(25f);

        //Perform the Renderscript
        blurScript.setInput(allIn);
        blurScript.forEach(allOut);

        //Copy the final bitmap created by the out Allocation to the outBitmap
        allOut.copyTo(outBitmap);

        //After finishing everything, we destroy the Renderscript.
        rs.destroy();

        return outBitmap;
    }

This method has one input parameter where we’ll pass the original screen bitmap and we’ll get the blurred Bitmap in return.

Now let’s write code for the blurBackground method of AnimalListActivity:

//Enable drawing cache so that we can get bitmap of the layout
frameContainer.setDrawingCacheEnabled(true);
frameContainer.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_LOW);
frameContainer.buildDrawingCache();

if (frameContainer.getDrawingCache() == null) return;

//Save the bitmap of the layout
Bitmap snapshot = Bitmap.createBitmap(frameContainer.getDrawingCache());
frameContainer.setDrawingCacheEnabled(false);
frameContainer.destroyDrawingCache();

//blur the bitmap and create bitmap drawable from it
BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), blurBitmap(snapshot));
//set blurred bitmap drawable in ImageView inside the FrameLayout container
imgBg.setImageDrawable(bitmapDrawable);
//hide the recycler view so that the blurred background image is displayed
recyclerList.setVisibility(View.GONE);

To remove the blur, we’ll use the following code in removeBlur method:

/*making RecyclerView visible will automatically hide the background blur because ImageView and RecyclerView are overlapping children of FrameLayout container*/
recyclerList.setVisibility(View.VISIBLE);

It’s time to write code for opening dialog for Peek and Pop, but before we start coding to open dialog, let’s add animation files to our project so that our dialog can be opened and closed with animation.

Add “anim” resource directory inside  “res” directory, add anim_in.xml and anim_out.xml as follows:

anim_in.xml: (opening dialog)

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"

        android:fillAfter="true"
        android:interpolator="@android:anim/bounce_interpolator">

        <scale
            android:duration="1000"
            android:fromXScale="0.0"
            android:fromYScale="0.0"
            android:pivotX="50%"
            android:pivotY="50%"
            android:toXScale="1.0"
            android:toYScale="1.0" />

        <alpha
            android:duration="500"
            android:fromAlpha="0.0"
            android:toAlpha="1" />
</set>

We have added Bounce Interpolator so that we’ll get bounce animation while opening the dialog.

anim_out.xml: (closing dialog)

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >

    <scale  xmlns:android="http://schemas.android.com/apk/res/android"
        android:fromXScale="1"
        android:toXScale="0"
        android:fromYScale="1"
        android:toYScale="0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="300"
        android:fillAfter="true">
    </scale>

    <alpha
        android:duration="200"
        android:fromAlpha="1.0"
        android:toAlpha="0" />
</set>

Now go to styles.xml and add the following lines:

<style name="MyAnimation.Window" parent="@android:style/Animation.Activity">
  <item name="android:windowEnterAnimation">@anim/anim_in</item>
  <item name="android:windowExitAnimation">@anim/anim_out</item>
</style>

We’ll set this style in setWindowAnimations method of Dialog’s window.

Now, navigate to the ViewHolder of the CustomRecyclerAdapter. Set OnTouchListener for the ImageView of the list item.

Here resides the main logic of 3D Touch. The logic to sense the pressure is to create a Handler which will start and wait for 50 milliseconds as soon as you touch the image. If your finger is still there after 50 milliseconds, the following code will be executed by the handler. You can change the milliseconds in order to detect various levels of pressures.

final Handler handler = new Handler();
Runnable mLongPressed = new Runnable(){
  public void run(){
    //Get view of dialog layout
    View dialog = LayoutInflater.from(context).inflate(R.layout.layout_dialog, null);
    //Initialize Dialog declared inside CustomRecyclerAdapter
    CustomRecyclerAdapter.this.dialog = new Dialog(context);
    ImageView imageView = dialog.findViewById(R.id.img);
    TextView textView = dialog.findViewById(R.id.text_img_name);
    //Code to don't show dialog title
    CustomRecyclerAdapter.this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    //Code to make dialog background transparent
    CustomRecyclerAdapter.this.dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
    CustomRecyclerAdapter.this.dialog.setContentView(dialog);
    //Set animation for open and close defined in styles.xml
    CustomRecyclerAdapter.this.dialog.getWindow().setWindowAnimations(R.style.MyAnimation_Window);
    CustomRecyclerAdapter.this.dialog.setOnCancelListener(new DialogInterface.OnCancelListener(){
      @Override
      public void onCancel(DialogInterface dialogInterface) {
        //remove blur on cancel
        animalListActivity.removeBlur();
      }
    });
  //set image and name of animal in dialog
  textView.setText(mAnimalsList.get(getAdapterPosition()).getName());
  Picasso.with(context).load(mAnimalsList.get(getAdapterPosition()).getUrl()).into(imageView);
  //blur background
  animalListActivity.blurBackground();
  //open dialog
  CustomRecyclerAdapter.this.dialog.show();
  }
};

So, the onTouchListener for list item image will look like this:

listItemImage.setOnTouchListener(new View.OnTouchListener(){
  //Add Handler code as written in above snippet
  @Override
  public boolean onTouch(View view, MotionEvent motionEvent) {
    if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
      //Remove handler callbacks if finger is lifted up
      handler.removeCallbacks(mLongPressed);
      //remove background blur
      animalListActivity.removeBlur();
      //dismiss the dialog
      hideQuickView();
      return false;
    } else if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
      //start the handler if finger is pressed for 50 milliseconds
      handler.postDelayed(mLongPressed, 50);
      return true;
    };
    //Remove handler callbacks for all other motion events
    handler.removeCallbacks(mLongPressed);
    return false;
 }
});

The hideQuickView method inside the CustomRecylerAdapter looks like this:

public void hideQuickView(){
  if (dialog != null) dialog.dismiss();
}

It’s almost done, just one thing to do.

If we open the dialog by pressing the finger on the image and move it around the screen, the RecyclerView in the background still gets the touch events and it scrolls in the background, which is an unwanted behaviour. Only Dialog should be controlled by the touch when it is open, not the background components.

So, the logic is:

  1. Before opening the dialog, we are going to disallow the scroll events by a method of RecyclerView:
    recyclerList.requestDisallowInterceptTouchEvent(true);
  2. Set the onItemTouchListener to RecyclerView. We have added the ItemTouchListener because after opening the dialog and moving the finger, the onTouchListener of ImageView of the RecyclerView won’t get the ACTION_UP motion event, so to dismiss dialog when we lift the finger up after moving the finger, we need to add onItemTouchListener to RecyclerView which will get ACTION_UP motion event in its onInterceptTouchEvent method.
    recyclerList.addOnItemTouchListener(new RecyclerView.OnItemTouchListener(){
      @Override
      public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        if (e.getAction() == MotionEvent.ACTION_UP) {
          CustomRecyclerAdapter customRecyclerAdapter = (CustomRecyclerAdapter) rv.getAdapter();
          //Remove blur on lifting up the finger
          removeBlur();
          //hide the dialog
          customRecyclerAdapter.hideQuickView();
          //allow the intercept touch event so that now user can scroll the RecyclerView
          rv.requestDisallowInterceptTouchEvent(false);
          return false;
        }
        return false;
     }
    
     @Override
     public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    
     }
    
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    
     }
    });

Congratulations! You’ve done it. Show your friends having iPhones how you developed the 3D Touch without hardware support. 🙂

I have created a demo using MVVM architecture, you can see it for reference at: https://github.com/LNAndroid/Android3dTouchDemo

Want to work with us? We're hiring!