Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can achieve Gmail like pinch zoom and header scrolling

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.

enter image description here

When we pinch Webview upper container scrolled up as per zoom:

enter image description here

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>
like image 845
Ashish Sahu Avatar asked Feb 07 '19 09:02

Ashish Sahu


2 Answers

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.

like image 180
Martin Zeitler Avatar answered Oct 11 '22 16:10

Martin Zeitler


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 ViewTypes. 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.

enter image description here

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.

like image 35
Reaz Murshed Avatar answered Oct 11 '22 16:10

Reaz Murshed