Android RecyclerView Analytics
Android Impression Tracking
In this tutorial, I will be teaching you about android impression tracking in a recycler view. Tracking impressions allows you to create data that can be used to analyze a user’s behavior e.g., screenviews, clicks, and impressions.
Pre-requisites:
- Understanding of recycler view
- Understanding of ViewHolder pattern
- Basic understanding of handlers
Overview:
- Visibility Tracker
- RecyclerView
- Visibility Tracker (Revisited)
- Registering A Listener
- Demo!
Here is the finished version of the project if you’d like to download the code. There are tags that mirror the tutorial. https://github.com/ColeMurray/Android-Impression-Tracking-Tutorial
Visibility Tracker
The visibility tracker is responsible for calculating all visible and invisible views.
Let’s break it down a bit more.
Create a class VisibilityTracker.java:
In this snippet, we’ve
- created a map that will hold tracked views,
- created a listener interface
- set a PreDraw listener to our activity’s rootview
Now create a member variable for our listener.
public class VisibilityTracker { private VisibilityTrackerListener mVisibilityTrackerListener;
We’ll create a method, addView, in our VisibilityTracker class that will create/update the tracked views and schedule a visibility check if it has no tracking info. We’ve also added methods for our listener.
public void addView(@NonNull View view, int minVisiblePercentageViewed) {
TrackingInfo trackingInfo = mTrackedViews.get(view); if (trackingInfo == null) { // view is not yet being tracked trackingInfo = new TrackingInfo(); mTrackedViews.put(view, trackingInfo); scheduleVisibilityCheck(); }
trackingInfo.mRootView = view; trackingInfo.mMinVisiblePercent = minVisiblePercentageViewed; }
public void setVisibilityTrackerListener(VisibilityTrackerListener listener) { mVisibilityTrackerListener = listener; }
public void removeVisibilityTrackerListener() { mVisibilityTrackerListener = null; }
We’ll come back to how to calculate the visibility later on in the tutorial.
Complete code up to this point can be found here: Milestone 0
RecyclerView
Our next few code snippets will focus on setting up our RecyclerView. We’ll soon have a glimpse of success!
Getting Started
Add the recyclerView library into your build.gradle file.
compile 'com.android.support:recyclerview-v7:23.1.1'
Layout XMLs
First let’s create our recycler view layout in fragment_main.xml:
Now create our item view for our recyclerView product_item_layout.xml:
ProductViewHolder
This viewholder will be used in our recyclerView. It will hold the view of product_item.layout.xml.
In our main project package, create ProductViewHolder.java.
public class ProductViewHolder extends RecyclerView.ViewHolder { public final TextView mTitleTextView;
public ProductViewHolder(View itemView) { super(itemView); mTitleTextView = (TextView) itemView.findViewById(R.id.title_textview); } }
ImpressionAdapter
Lastly, Create ImpressionAdapter.java:
In this class, we take in an activity (we’ll use this later), a list of data and inflate our ProductViewHolder for each item in the list.
In our onBindViewHolder method, we take a title from our dataset according to the position in the recycler view and set the title to textview in the view holder. I added in the alternate background to make the view distinction more prominent; it is not necessary.
MainActivityFragment
We need to find our recycler view in the view hierarchy, and set our adapter to it. In MainActivityFragment.java:
Build and run the project and you should see this:
At this point, we’ve got a basic recycler view setup that’s binding our data to views, Sweet. Let’s start wiring up our tracking.
Tracking
Still in our ImpressionAdapter class, add a member variable for our visibilityTracker and viewPositionMap. We’ll also need to take in our activity as a parameter in the constructor to initialize our tracker.
private List<String> mDataSet; private final VisibilityTracker mVisibilityTracker; private final WeakHashMap<View, Integer> mViewPositionMap = new WeakHashMap<>();
public ImpressionAdapter(Activity activity, List<String> dataSet) { mDataSet = dataSet; mVisibilityTracker = new VisibilityTracker(activity); }
In our onBindViewHolder we’ll add two lines. Our first line is creating a mapping of our view to it’s position in the recyclerView.We’ll use this later on in the tutorial. Our second line adds the view to mVisibilityTracker’s tracked views.
@Override public void onBindViewHolder(ProductViewHolder productViewHolder, int position) { String title = mDataSet.get(position);
//alternate view background color productViewHolder.itemView.setBackgroundResource(position % 2 == 0 ? android.R.color.white : android.R.color.darker_gray); productViewHolder.mTitleTextView.setText(title);
mViewPositionMap.put(productViewHolder.itemView, position); mVisibilityTracker.addView(productViewHolder.itemView, 0); }
That’s it for now in these files. Let’s jump back to our VisibilityTracker. Complete code to this point can be found here: Milestone 1
VisibilityTracker (Revisited)
Now that we have the basic foundation, we’re moving back to our tracker. Here we’re creating the class that actually calculates if the view is ‘impressed’.
Create an inner class VisibilityChecker within VisibilityTracker.java:
As this is our most important file, let’s break it down a bit.
if (!view.getGlobalVisibleRect(mClipRect)) { return false; }
final long visibleArea = (long) mClipRect.height() * mClipRect.width(); final long totalViewArea = (long) view.getHeight() * view.getWidth();
return totalViewArea > 0 && 100 * visibleArea >= minPercentageViewed * totalViewArea;
Our ‘if’ statement is the key to the rest of this method. view.getGlobalVisibleRect doesn’t return a Rectangle like we’d expect. It returns true if any part of our view is visible within its parent. Additionally, it places the visible dx and dy coordinates into mClipRect. Confusing, to say the least.
In our last line, minPercentageViewed * totalViewArea represents the area required for a view to be an qualified as impression.
Here’s an example of two valid impressions if our minPercentageViewed was 100%.
In both examples, our visible area is 5px*9px, and we require 100% of the view to be visible for impression. With this, we can see the two max positions for this view to be registered as an impression.
Cool, now we have a way to calculate if a view is visible, time to start scheduling some checks! Since we don’t need/want to be continuously checking our views, we’ll use a handler and runnable to throttle the checks.
VisibilityRunnable
First, create a boolean member field and VisibilityChecker member field:
private boolean mIsVisibilityCheckScheduled; private VisibilityChecker mVisibilityChecker;
Create an inner class VisibilityRunnable within VisibilityTracker:
Here our runnable iterates over our list of tracked views, determines the status of each view and sends the results to our listener.
Our last step is to implement scheduleVisibilityCheck: Here we add a few member variables and instantiate them within our constructor.
And we’re done! Almost…… Let’s see it in action.
Registering a Listener
We’ve built:
- a recycler view
- an impression adapter that adds views to a visibility tracker in our onBindViewHolder method,
- a visibility tracker that can determine if a view is visible and will notify a listener of any visible / invisible views.
Our final step is to register a listener to be notified when our views visibility changes. In this example, we’ll print the titles of Any (we set the value as zero earlier) visible view. Jump back into our Impression Adapter:
In this snippet we’ve created a listener that will be notified and call handleVisibleViews. In handleVisibleViews, we access our view position map we placed our views in earlier. We access the data for that view from our dataset and print the title to log.
Demo:
Final Words:
I hope you enjoyed the adventure through learning android impression tracking on a RecyclerView.
In the next tutorial, I’ll focus on how to visualize our new impression tracker with Mixpanel.
If you liked this tutorial, share and recommend to others. Problems or suggestions, reach out on Twitter : ColeMurray or in the comments below