How can achieve pinch zoom behavior like Gmail app? I've put header container in ScrollView followed by WebView
. Seems It's very complex behavior.
Here is without zoom.
When we pinch Webview upper container scrolled up as per zoom:
So far here is my initials:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/white">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/appbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"></FrameLayout>
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:scrollbars="none" />
</LinearLayout>
</ScrollView>
</RelativeLayout>
</FrameLayout>
GMail uses a Chrome WebView
with pinch zoom enabled. the zoom only applies to the single thread view. WebSettings
setBuiltInZoomControls() is by default false
and setDisplayZoomControls() is by default true
. by changing both, the zoom works and there are no zoom controls being displayed:
webview.getSettings().setBuiltInZoomControls(true);
webview.getSettings().setDisplayZoomControls(false);
and that toolbar is a transparently styled ActionBar, with style windowActionBarOverlay
set true
:
Flag indicating whether this window's Action Bar should overlay application content.
the ActionBar's bottom shadow is being removed in the top-most scroll position. this one listens for vertical scroll events and not for any scaling gestures. this effect works about like this (initially that shadow has to be hidden):
webView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if(scrollY == 0) {
/* remove the ActionBar's bottom shadow */
} else {
/* apply the ActionBar's bottom shadow */
}
}
}
depending how often the OnScrollChangeListener
is being triggered, even checking for scrollY == 0
and scrollY == 1
might already suffice to switch the shadow on and off.
when scaling, this seems to be a ScaleGestureDetector.SimpleOnScaleGestureListener
(see the docs), where .getScaleFactor()
is being used to animate the secondary "toolbar" vertical top position, which then shoves it outside of the visible view-port. and this secondary "toolbar" appears to be a nested vertical DrawerLayout
- which cannot be manually moved - that's why it moves that smooth... a DrawerLayout
is not limited to be a horizontal drawer; and I think this is the answer.
Edit: I'd relatively certain now, that this is AndroidX with MDC Motion.
I think I understood your question. You want to push the subject line in the upward direction and the other emails in the downward direction when an email is being expanded. I tried to implement the idea of showing an email in the Gmail app. I think I am very close to the solution as the pushing is not smooth enough. However, I wanted to share the answer here to present my thought about your question.
I have created a GitHub repository from where you can see my implementation. I have added a readme there as well to explain the overall idea.
I tried to implement the whole thing using a RecyclerView
have different ViewType
s. I have added an adapter which is like the following.
public class RecyclerViewWithHeaderFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int HEADER_VIEW = 1;
private static final int GROUPED_VIEW = 2;
private static final int EXPANDED_VIEW = 3;
private ArrayList<Integer> positionTracker; // Take any list that matches your requirement.
private Context context;
private ZoomListener zoomListener;
// Define a constructor
public RecyclerViewWithHeaderFooterAdapter(Context context, ZoomListener zoomListener) {
this.context = context;
this.zoomListener = zoomListener;
positionTracker = Utilities.populatePositionsWithDummyData();
}
// Define a ViewHolder for Header view
public class HeaderViewHolder extends ViewHolder {
public HeaderViewHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Do whatever you want on clicking the item
}
});
}
}
// Define a ViewHolder for Expanded view
public class ExpandedViewHolder extends ViewHolder {
public ExpandedViewHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Do whatever you want on clicking the item
}
});
}
}
// Define a ViewHolder for Expanded view
public class GroupedViewHolder extends ViewHolder {
public GroupedViewHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Do whatever you want on clicking the item
}
});
}
}
// And now in onCreateViewHolder you have to pass the correct view
// while populating the list item.
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v;
if (viewType == EXPANDED_VIEW) {
v = LayoutInflater.from(context).inflate(R.layout.list_item_expanded, parent, false);
ExpandedViewHolder vh = new ExpandedViewHolder(v);
return vh;
} else if (viewType == HEADER_VIEW) {
v = LayoutInflater.from(context).inflate(R.layout.list_item_header, parent, false);
HeaderViewHolder vh = new HeaderViewHolder(v);
return vh;
} else {
v = LayoutInflater.from(context).inflate(R.layout.list_item_grouped, parent, false);
GroupedViewHolder vh = new GroupedViewHolder(v);
return vh;
}
}
// Now bind the ViewHolder in onBindViewHolder
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
try {
if (holder instanceof ExpandedViewHolder) {
ExpandedViewHolder vh = (ExpandedViewHolder) holder;
vh.bindExpandedView(position);
} else if (holder instanceof GroupedViewHolder) {
GroupedViewHolder vh = (GroupedViewHolder) holder;
} else if (holder instanceof HeaderViewHolder) {
HeaderViewHolder vh = (HeaderViewHolder) holder;
}
} catch (Exception e) {
e.printStackTrace();
}
}
// Now the critical part. You have return the exact item count of your list
// I've only one footer. So I returned data.size() + 1
// If you've multiple headers and footers, you've to return total count
// like, headers.size() + data.size() + footers.size()
@Override
public int getItemCount() {
return DEMO_LIST_SIZE; // Let us consider we have 6 elements. This can be replaced with email chain size
}
// Now define getItemViewType of your own.
@Override
public int getItemViewType(int position) {
if (positionTracker.get(position).equals(HEADER_VIEW)) {
// This is where we'll add the header.
return HEADER_VIEW;
} else if (positionTracker.get(position).equals(GROUPED_VIEW)) {
// This is where we'll add the header.
return GROUPED_VIEW;
} else if (positionTracker.get(position).equals(EXPANDED_VIEW)) {
// This is where we'll add the header.
return EXPANDED_VIEW;
}
return super.getItemViewType(position);
}
// So you're done with adding a footer and its action on onClick.
// Now set the default ViewHolder for NormalViewHolder
public class ViewHolder extends RecyclerView.ViewHolder {
// Define elements of a row here
public ViewHolder(View itemView) {
super(itemView);
// Find view by ID and initialize here
}
public void bindExpandedView(final int position) {
// bindExpandedView() method to implement actions
final WebView webView = itemView.findViewById(R.id.email_details_web_view);
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setDisplayZoomControls(false);
webView.loadUrl("file:///android_asset/sample.html");
webView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
zoomListener.onZoomListener(position);
}
});
}
}
}
And the expanded list item contains a WebView
which has a wrapper which is wrap_content
. You will find the following layout in the list_item_expanded.xml
.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<WebView
android:id="@+id/email_details_web_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:scrollbars="none"
tools:ignore="WebViewLayout" />
</RelativeLayout>
I tried to add some dummy data for the experiment and hence the Utility
class was written. The RecyclerView
is set to have a reverse layout as this is the common expectation of showing a conversation in a RecyclerView
.
The key idea is to scrollToPosition
when the WebView
is being expanded. So that it feels like the items are push upwards and downwards to accommodate the expansion. Hope you get the idea.
I am adding some screenshots here to give you an idea about what I could achieve so far.
Please note that the pushing mechanism is not smooth. I will be working on this. However, I thought I should post it here as this might help you in your thinking. I would like to suggest you clone the repository and run the application to check the overall implementation. Let me know if there is any feedback.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With