Search code examples
androidandroid-layoutandroid-softkeyboardsupportmapfragment

How to have the soft keyboard cover a view, yet cause other views to take the available space?


Background

Suppose I have a view that works as the background (MapFragment in my case), and some other views that are the content of the activity.

The problem

Focusing an EditText, it shows the soft keyboard, causing all views to change their sizes, including the background view.

This means that showing the soft keyboard causes the background to "jump" and re-layout itself.

This is especially problematic in my case, as I use MapFragment as the background. That's because I can think of a workaround to detect the keyboard size, and show only the upper area of the background view, but there is little control of the MapFragment in how it looks and work.

Here's a demo of the problem:

enter image description here

Here's a sample xml, used inside the sample of the mapFragment:

basic_demo.xml

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/map"
        class="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <EditText
        android:id="@android:id/edit"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="20dp"
        android:background="#66ffffff"
        android:digits="0123456789()-+*"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:gravity="center_vertical|left"
        android:imeOptions="actionSearch|flagNoExtractUi"
        android:inputType="phone"
        android:singleLine="true"
        android:textColorHint="#aaa"
        android:textSize="18dp"
        tools:hint="Search Phones ..."/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_marginBottom="30dp"
        android:background="#66ffffff"
        android:text="This should always be shown, at the bottom"/>
</FrameLayout>

What I've found

I know that we can set the "windowSoftInputMode" flag in the manifest, but this is an all-or-nothing solution. It affects all of the views, and not a single one.

The question

How can I let the soft keyboard change the layout of all views except specific ones?

Is it even possible?


EDIT: looking at "gio"'s solution, it works. Here's a more optimized way to do it:

    View view=...
    mContainerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        final Rect outGlobalRect = new Rect(), outWindowRect = new Rect();

        @Override
        public void onGlobalLayout() {
            mContainerView.getGlobalVisibleRect(outGlobalRect);
            mContainerView.getWindowVisibleDisplayFrame(outWindowRect);
            int marginBottom = outGlobalRect.bottom - outWindowRect.bottom;
            final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) view.getLayoutParams();
            if (layoutParams.bottomMargin == marginBottom)
                return;
            layoutParams.setMargins(0, 0, 0, marginBottom);
            view.requestLayout();
        }
    });

Solution

  • It's possible according your initial requirements. Idea is to change bottom margin of needed view. Value will be calculated as difference between shown and real height of root view. I wrapped TextView into FrameLayout for case if you need to control more views there.

    xml layout

    <FrameLayout
    android:id="@+id/ac_mp_container"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <fragment
        android:id="@+id/ac_mp_map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
    
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.tivogi.so.MapsActivity" />
    
    <EditText
        android:layout_width="100dp"
        android:layout_height="wrap_content" />
    
    <FrameLayout
        android:id="@+id/ac_mp_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:padding="16dp">
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="This should always be shown, at the bottom" />
    
    </FrameLayout>
    

    activity class

    public class MapsActivity extends FragmentActivity implements OnMapReadyCallback {
    
    private View mContainerView;
    private GoogleMap mMap;
    
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_maps);
        // Obtain the SupportMapFragment and get notified when the map is ready to be used.
        SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
                .findFragmentById(R.id.ac_mp_map);
        mapFragment.getMapAsync(this);
        mContainerView = findViewById(R.id.ac_mp_container);
        mContainerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                View view = findViewById(R.id.ac_mp_view);
                int marginBottom;
                Rect outGlobalRect = new Rect();
                Rect outWindowRect = new Rect();
                DisplayMetrics metrics = new DisplayMetrics();
                getWindowManager().getDefaultDisplay().getMetrics(metrics);
                mContainerView.getGlobalVisibleRect(outGlobalRect);
                mContainerView.getWindowVisibleDisplayFrame(outWindowRect);
                marginBottom = outGlobalRect.bottom - outWindowRect.bottom;
                FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM);
                layoutParams.setMargins(0, 0, 0, marginBottom);
                view.setLayoutParams(layoutParams);
            }
        });
    
    }
    
    /**
     * Manipulates the map once available.
     * This callback is triggered when the map is ready to be used.
     * This is where we can add markers or lines, add listeners or move the camera. In this case,
     * we just add a marker near Sydney, Australia.
     * If Google Play services is not installed on the device, the user will be prompted to install
     * it inside the SupportMapFragment. This method will only be triggered once the user has
     * installed Google Play services and returned to the app.
     */
    @Override
    public void onMapReady(GoogleMap googleMap) {
        mMap = googleMap;
    
        // Add a marker in Sydney and move the camera
        LatLng sydney = new LatLng(-34, 151);
        mMap.addMarker(new MarkerOptions().position(sydney).title("Marker in Sydney"));
        mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney));
    }
    

    }

    Result:

    enter image description here