Yesterday I encountered a problem when building the latest version of an Android app on which I am working.
That problem was that when uploading a new photo to the site, subject to the user pressing 'Continue' before the upload is complete, I wanted to show the new photo (and all its information) with a 'Currently uploading overlay'.
My initial thought was that simply placing a text view at the bottom stating as much would be a suitable resolution. It was - it looks good. The problem was that the photo screen displays buttons which open the profile of the uploader, the profile of the photos location, and the profile of the applied tags respectively. Whilst in most cases we have this information (after all the user did just choose it..), in some cases we do not.
For example, we do not require the user to login to upload a photo, but we do create a user account for them (behind the scenes) the first time that they upload. As such we do not know who the uploader is for certain until the upload is complete.
It was important to me that the app had a consistent user experience, and as such I don't want clickable buttons which sometimes pop up with ugly and confusing alerts such as 'We cannot go to the uploaders profile because we don't know who you are'. Allowing people to press 'continue' before the upload is complete is purely for the sake of user experience, but we still want the upload to complete..
As such, I opted to extend the overlay such that it covers the whole of the photo activities tab content. The intention being that the user not be able to click or action any control until the upload is complete. That means no adding comments (you can't add a comment if the photo isn't saved in the database), no rating photos, and no profile clicks.
Adding an overlay view
Adding an overlay view was in itself less simple than I had expected.
Having gained some insight from this StackOverflow answer, I ended up with the following:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/overlay"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#66000000" />
<TextView
android:id="@+id/statusMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#c0000000"
android:gravity="center_horizontal"
android:padding="5dp"
android:text="Uploading"
android:textColor="@color/tw__solid_white"
android:visibility="visible" />
</LinearLayout>
</FrameLayout>
<com.appnamespace.com.Custom.AppFragmentTabHost
android:id="@android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/top_border" />
</LinearLayout>
This is the view for my tab host which hosts three fragment tabs. Simply setting the overlay visibility property to visible
will display a semi transparent overlay over the content of whichever tab is being displayed. Great !
I can still click
One surprising problem hat i encountered at this point was that I could still click buttons that were underneath my overlay but I could not scroll the content (scroll view) below.
Given that I still want the uploader to be able to see the information that we are uploading, continuing to allow scrolling the content underneath would be great BUT under no circumstances do we want any buttons to be clickable. That would defeat the point of the exercise.
Implementation
Having previously built my own Pull to Refresh component, I was well aware (albeit a little rusty) as to how Android works in regards to intercepting touches.
Now.. the reason that the Button
elements below receive the clicks is because by default they have the property android:clickable=true
. Our overlay does not, and as such it does not intercept and act on the touches. A simple resolution is to add the clickable
property to our overlay view. This intercepts clicks, and prevents the buttons from being triggered.
The problem is that this also prevents any touches reaching the ScrollView
- we cannot scroll the content underneath.
As such we need to intercept the touches on the overlay view and conditionally pass them to the child ScrollView when needed whilst not passing clicks to the buttons below.
The code that I came up with (based heavily on the discussion here is as follows:
final FrameLayout tabContent = (FrameLayout) view.findViewById(android.R.id.tabcontent);
View overlay = (View) view.findViewById(R.id.statusLayout);
overlay.setOnTouchListener(new View.OnTouchListener() {
private float mDownX;
private float mDownY;
private final float SCROLL_THRESHOLD = 10;
private boolean isOnClick;
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mDownX = motionEvent.getX();
mDownY = motionEvent.getY();
isOnClick = true;
tabContent.dispatchTouchEvent(motionEvent);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (isOnClick) {
Log.i("LOG", "onClick ");
//TODO onClick code
} else {
tabContent.dispatchTouchEvent(motionEvent);
}
break;
case MotionEvent.ACTION_MOVE:
if ((Math.abs(mDownX - motionEvent.getX()) > SCROLL_THRESHOLD || Math.abs(mDownY - motionEvent.getY()) > SCROLL_THRESHOLD)) {
isOnClick = false;
tabContent.dispatchTouchEvent(motionEvent);
}
break;
default:
break;
}
return true;
}
});
I don't like spoiling the fun, and feel that there is certainly a good learning experience to be had here. As such I have opted not to 'over-comment' the code.
The basic premise is that we discern as to whether the user is clicking or swiping (based on a movement threshold). If the latter, we want to pass it to our tab content to handle (the scroll view will move). If however the user has clicked, we do not want to pass it to the tab content below.
Because an MotionEvent.ACTION_MOVE
event requires a MotionEvent.ACTION_DOWN
event to have occurred before, and an MotionEvent.ACTION_UP
event to occur afterwards, we make sure to pass those down to the tab content at the appropriate times.
Alternatives
Depending on how often you intend to utilise such an overlay, it may well be worth implementing the above listeners in a custom View subclass such that you do not have to implement complex onTouchListeners
in every place you want to use it.
Benefits
The reason that this solution works so well for me is because I can control the overlay in the Activity which displays my tab host/content. I do not need to give any consideration to the views of the fragments actually hosted in the tab host. Simply passing the touches to the tab contents FrameLayout
(as appropriate) simply passes the ball back to the Android framework in the appropriate place to allow for it to work its magic.
We could have absolutely any tab fragment content below and the specified touch messages would still be passed down to it.
So...
Hopefully that has provided a little further insight should you wish to implement something similar. If you come up with an alternative interesting use case for such an overlay, please do share - I would love to hear what cool functionality people are implementing.
Any questions or comments..? Just let me know !