androidandroid-edittextandroid-popupwindow

Show RecyclerView/PopupWIndow by anchoring below or above edittext depending on where the cursor is and depending on screen available height


I am trying to display @ user mentions like facebook or twitter. I am able to implement this functionality. But i have a ui problem.

If edittext is at the bottom of the screen i need to show suggestion in recyclerview at the top.(above edittext cursor)

If edittext is at the top of the screen i need to show suggestion in recycelrview at the bottom (below edittext cursor)

What have i tried?

I have used recyclerview that is constrained below edittext

Problem

When soft keyboard opens it covers the recyclerview. This happens when edittext cursor is at the bottom.

enter image description here

How can i fix this using existing recyclerview itself?.

When edittext cursor is at the top see suggestions are shown below properly.

enter image description here

Code

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:textStyle="bold"
            android:textSize="16sp"
            android:textColor="@color/black"
            android:fontFamily="@font/source_sans_pro"
            android:id="@+id/titleHeader"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:text="@string/create_feed_post"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <View
            android:id="@+id/line"
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginTop="16dp"
            android:background="#c8c8c8"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/titleHeader" />


        <com.linkedin.android.spyglass.ui.MentionsEditText
            android:padding="8dp"
            android:id="@+id/mentionsEditText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:maxHeight="300dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:background="@null"
            android:gravity="start|top"
            android:hint="Share team wins or recognize colleague for a job well done"
            android:inputType="textMultiLine"
            android:minHeight="50dp"
            android:paddingStart="15dp"
            android:paddingEnd="15dp"
            android:textSize="16sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/line">

        </com.linkedin.android.spyglass.ui.MentionsEditText>

        <ImageView
            android:id="@+id/preview"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:visibility="visible"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/mentionsEditText" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="0dp"
            android:layout_height="250dp"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:elevation="5dp"
            android:orientation="vertical"
            android:overScrollMode="never"
            android:scrollbarStyle="outsideOverlay"
            android:scrollbars="none"
            android:visibility="gone"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/mentionsEditText" />

        <View
            android:id="@+id/line2"
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginTop="8dp"
            android:background="#c8c8c8"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/preview" />


        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/optionsContainer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/line2">

            <ImageView
                android:id="@+id/visibility"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:src="@drawable/toggle_comment"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toEndOf="@+id/attach"
                app:layout_constraintTop_toTopOf="parent" />

            <ImageView
                android:id="@+id/attach"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:srcCompat="@drawable/ic_upload_image" />

            <Button
                android:id="@+id/done"
                android:layout_width="0dp"
                android:layout_height="60dp"
                android:layout_marginStart="32dp"
                android:text="@string/post"
                android:textAllCaps="false"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="@id/visibility"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <ImageView
            android:id="@id/cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            app:layout_constraintBottom_toBottomOf="@+id/titleHeader"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="@+id/titleHeader"
            app:srcCompat="@drawable/ic_close"
            app:tint="@color/black" />

        <FrameLayout
            android:visibility="gone"
            android:elevation="5dp"
            android:id="@+id/container"
            app:layout_constraintBottom_toTopOf="@+id/preview"
            app:layout_constraintEnd_toEndOf="@+id/preview"
            app:layout_constraintTop_toTopOf="@+id/preview"
            android:layout_width="20dp"
            android:layout_height="20dp">
            <com.mikhaellopez.circleview.CircleView
                app:cv_border_width="1dp"
                app:cv_border_color="#EAEAEA"
                app:cv_border="true"
                app:cv_color="@color/white"
                android:layout_gravity="center"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>

            <ImageView
                android:layout_gravity="center"
                android:id="@+id/removePreview"
                android:layout_width="15dp"
                android:layout_height="15dp"
                app:srcCompat="@drawable/ic_close"
                app:tint="#999999" />

        </FrameLayout>
        
    </androidx.constraintlayout.widget.ConstraintLayout>
    </ScrollView>
</layout>

Update: i tried using a pop up window. I am able to show above or below a view depending on where the view is located. But there is still one problem with keyboard opening. The pop up comes over the keyboard when the edittext is at the bottom. The edittext moves up but popup window stays in same place

Update 2:

    if(anchor instanceof EditText) {
        EditText editText = (EditText) anchor;
        int pos = editText.getSelectionStart();
        Layout layout = editText.getLayout();
        int line = layout.getLineForOffset(pos);
        int baseline = layout.getLineBaseline(line);
        int ascent = layout.getLineAscent(line);
        float cursorx = layout.getPrimaryHorizontal(pos);
        cursory = baseline + ascent- editText.getScrollY();
        
    }



    final View contentView = view;

    final Rect windowRect = new Rect();
    contentView.getWindowVisibleDisplayFrame(windowRect);
    final int windowW = windowRect.width();
    final int windowH = windowRect.height();
    contentView.measure(
            makeDropDownMeasureSpec(getWidth(), windowW),
            makeDropDownMeasureSpec(getHeight(), windowH)
    );
    final int measuredW = contentView.getMeasuredWidth();
    final int measuredH = contentView.getMeasuredHeight();
    final int[] anchorLocation = new int[2];
    anchor.getLocationInWindow(anchorLocation);
    final int anchorBottom = anchorLocation[1] + anchor.getHeight();
    

      if (y + anchorBottom < 0) {
            y = -anchorBottom;
        } else  {
          y = (int) (y + cursory );

       }

And then

popupWindow.showAsDropDown(anchor, 0, y);

and the flag in manifest is

android:windowSoftInputMode="adjustResize"

Solution

  • Using pop up window show at location will fix this

    private fun showPopupWindow(anchor: View) {
        var popUpHeight = 0
        PopupWindow(anchor.context).apply {
            isOutsideTouchable = true
            val inflater = from(anchor.context)
            contentView = inflater.inflate(R.layout.popup_layout, null).apply {
                measure(
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
                )
                measuredWidth
                measuredHeight
                popUpHeight = measuredHeight
            }
            height = ViewGroup.LayoutParams.WRAP_CONTENT
            width = ViewGroup.LayoutParams.MATCH_PARENT
        }.also { popupWindow ->
            val location = IntArray(2).apply {
                anchor.getLocationInWindow(this)
            }
            val size = Size(
                popupWindow.contentView.measuredWidth,
                popupWindow.contentView.measuredHeight
            )
            val editText = anchor as EditText
            val pos = editText.selectionStart
            val layout: Layout = editText.layout
            val line: Int = layout.getLineForOffset(pos)
            editText.getLocationOnScreen(location)
            val point = Point()
            point.x = layout.getPrimaryHorizontal(pos).toInt()
            point.y = layout.getLineBottom(line)// location[1] //-  (baseline + ascent )
            val y = point.y
            val top = layout.getLineTop(line)
            val editTextheight = editText.measuredHeight
            val yOffset = if (y > (editTextheight / 2)) {
                 top - (popUpHeight + 50)
             } else {
                  point.y + 50
              }
              popupWindow.showAsDropDown(
                  anchor, 0,
                  yOffset
             )
        }
    }
    

    Based on where the cursor is and also the height of edittext you can calculate the yoffset and show the pop up at a specified position.

    In case edittext scrolls you can use editText.scrolly and re-calculate your offset.