EditText, foco claro no toque externo

100

Meu layout contém ListView, SurfaceViewe EditText. Quando clico em EditText, ele recebe o foco e o teclado na tela é exibido. Quando clico em algum lugar fora do EditText, ele ainda tem o foco (não deveria). Acho que poderia configurar o OnTouchListener's nas outras visualizações no layout e limpar o EditTextfoco manualmente . Mas parece muito hackeado ...

Eu também tenho a mesma situação no outro layout - exibição de lista com diferentes tipos de itens, alguns dos quais têm EditTextdentro. Eles agem exatamente como escrevi acima.

A tarefa é fazer com que EditTextperca o foco quando o usuário tocar algo fora dele.

Já vi perguntas semelhantes aqui, mas não encontrei nenhuma solução ...

nota173
fonte

Respostas:

66

Tentei todas essas soluções. O edc598 foi o mais próximo de funcionar, mas os eventos de toque não foram acionados em outrosView s contidos no layout. Caso alguém precise desse comportamento, acabei fazendo o seguinte:

Eu criei um (invisível) FrameLayoutchamado touchInterceptor como o último Viewno layout para que ele se sobreponha a tudo ( editar: você também deve usar a RelativeLayoutcomo layout pai e fornecer os atributos touchInterceptor fill_parent ). Então eu usei para interceptar toques e determinar se o toque estava em cima EditTextou não:

FrameLayout touchInterceptor = (FrameLayout)findViewById(R.id.touchInterceptor);
touchInterceptor.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            if (mEditText.isFocused()) {
                Rect outRect = new Rect();
                mEditText.getGlobalVisibleRect(outRect);
                if (!outRect.contains((int)event.getRawX(), (int)event.getRawY())) {
                    mEditText.clearFocus();
                    InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 
                    imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
                }
            }
        }
        return false;
    }
});

Retorne false para permitir que o manuseio de toque falhe.

É hacky, mas é a única coisa que funcionou para mim.

Ken
fonte
20
Você pode fazer a mesma coisa sem adicionar um layout adicional, substituindo dispatchTouchEvent (MotionEvent ev) em sua atividade.
pcans
obrigado pela dica clearFocus (), também funcionou para mim no SearchView
Jimmy Ilenloa
1
Eu gostaria de dar uma dica para a resposta de @ zMan abaixo. É baseado nisso, mas não precisa de uma visualização stackoverflow.com/a/28939113/969016
Menino de
Além disso, se alguém se deparar com uma situação em que o teclado não está escondendo, mas limpando o foco, primeiro invoque o hideSoftInputFromWindow()foco claro
Ahmet Gokdayi
230

Com base na resposta de Ken, aqui está a solução de copiar e colar mais modular.

Nenhum XML necessário.

Coloque-o em sua atividade e ele se aplicará a todos os EditTexts, incluindo aqueles dentro dos fragmentos dessa atividade.

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        View v = getCurrentFocus();
        if ( v instanceof EditText) {
            Rect outRect = new Rect();
            v.getGlobalVisibleRect(outRect);
            if (!outRect.contains((int)event.getRawX(), (int)event.getRawY())) {
                v.clearFocus();
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
            }
        }
    }
    return super.dispatchTouchEvent( event );
}
zMan
fonte
12
Parece bom. Ainda estou tendo um problema, todo o meu texto de edição está dentro de uma visualização de rolagem e o texto de edição superior sempre tem o cursor visível. Mesmo quando clico fora, o foco é perdido e o teclado desaparece, mas o cursor ainda está visível no texto de edição superior.
Vaibhav Gupta de
1
Melhor solução que encontrei !! Anteriormente, eu estava usando @Override public boolean onTouch (View v, MotionEvent event) {if (v.getId ()! = Search_box.getId ()) {if (search_box.hasFocus ()) {InputMethodManager imm = (InputMethodManager) getSystemService ( Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow (search_box.getWindowToken (), 0); search_box.clearFocus (); } retorna falso; } retorna falso; }
Pradeep Kumar Kushwaha
Simples, conciso, funcional! Melhor solução com certeza. Obrigado
Aleksandar
13
A melhor solução que nunca vi antes! Vou guardar este código na minha essência para passar ao meu filho e ao meu neto.
Fan Applelin de
2
Se o usuário clicar em outro EditText, o teclado fecha e reabre imediatamente. Para resolver isso, mudei MotionEvent.ACTION_DOWNpara MotionEvent.ACTION_UPno seu código. Ótima resposta btw!
Thanasis1101
35

Para a visualização pai do EditText, deixe os 3 atributos a seguir serem " verdadeiros ":
clickable , focusable , focusableInTouchMode .

Se uma visão deseja receber o foco, ela deve satisfazer estas 3 condições.

Veja android.view :

public boolean onTouchEvent(MotionEvent event) {
    ...
    if (((viewFlags & CLICKABLE) == CLICKABLE || 
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        ...
        if (isFocusable() && isFocusableInTouchMode()
            && !isFocused()) {
                focusTaken = requestFocus();
        }
        ...
    }
    ...
}

Espero que ajude.

suber
fonte
2
Para limpar o foco, a outra visão focalizável deve ser parente da visão focalizada. Não funcionará se a visão a focar estiver em outra hierarquia.
AxeEffect
Tecnicamente, sua afirmação está incorreta. A visualização do pai deve ser (clickable) *OR* (focusable *AND* focusableInTouchMode);)
Martin Marconcini
24

Basta colocar essas propriedades na parte superior do pai.

android:focusableInTouchMode="true"
android:clickable="true"
android:focusable="true" 
Chandan
fonte
obrigado cara. Eu estava lutando contra esse problema nas últimas duas horas. Adicionar essas linhas no pai superior funcionou.
Däñish Shärmà
Para sua informação: Embora funcione na maioria dos casos, não está funcionando para alguns elementos internos do layout.
SV Madhava Reddy
17

A resposta de Ken funciona, mas é engenhosa. Como pcans alude no comentário da resposta, o mesmo poderia ser feito com dispatchTouchEvent. Essa solução é mais limpa, pois evita a necessidade de hackear o XML com um FrameLayout transparente e fictício. Aqui está o que parece:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    EditText mEditText = findViewById(R.id.mEditText);
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        View v = getCurrentFocus();
        if (mEditText.isFocused()) {
            Rect outRect = new Rect();
            mEditText.getGlobalVisibleRect(outRect);
            if (!outRect.contains((int)event.getRawX(), (int)event.getRawY())) {
                mEditText.clearFocus();
                //
                // Hide keyboard
                //
                InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 
                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
            }
        }
    }
    return super.dispatchTouchEvent(event);
}
Mike Ortiz
fonte
7

Para mim abaixo as coisas funcionaram -

1. Adicionando android:clickable="true"e android:focusableInTouchMode="true"ao parentLayoutde EditTextieandroid.support.design.widget.TextInputLayout

<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:clickable="true"
    android:focusableInTouchMode="true">
<EditText
    android:id="@+id/employeeID"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:ems="10"
    android:inputType="number"
    android:hint="Employee ID"
    tools:layout_editor_absoluteX="-62dp"
    tools:layout_editor_absoluteY="16dp"
    android:layout_marginTop="42dp"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:layout_alignParentEnd="true"
    android:layout_marginRight="36dp"
    android:layout_marginEnd="36dp" />
    </android.support.design.widget.TextInputLayout>

2. Substituindo dispatchTouchEventna classe Activity e inserindo hideKeyboard()função

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            View view = getCurrentFocus();
            if (view != null && view instanceof EditText) {
                Rect r = new Rect();
                view.getGlobalVisibleRect(r);
                int rawX = (int)ev.getRawX();
                int rawY = (int)ev.getRawY();
                if (!r.contains(rawX, rawY)) {
                    view.clearFocus();
                }
            }
        }
        return super.dispatchTouchEvent(ev);
    }

    public void hideKeyboard(View view) {
        InputMethodManager inputMethodManager =(InputMethodManager)getSystemService(Activity.INPUT_METHOD_SERVICE);
        inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
    }

3. Adicionando setOnFocusChangeListenerpara EditText

EmployeeId.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (!hasFocus) {
                    hideKeyboard(v);
                }
            }
        });
Adi
fonte
5

Você provavelmente já encontrou a resposta para este problema, mas estou procurando uma maneira de resolver isso e ainda não consigo encontrar exatamente o que estava procurando, então resolvi postar aqui.

O que fiz foi o seguinte (isso é muito generalizado, o objetivo é dar uma ideia de como proceder, copiar e colar todo o código não vai funcionar O: D):

Primeiro, tenha o EditText e quaisquer outras visualizações que você deseja em seu programa envolvidas por uma única visualização. No meu caso, usei um LinearLayout para embrulhar tudo.

<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/mainLinearLayout">
 <EditText
  android:id="@+id/editText"/>
 <ImageView
  android:id="@+id/imageView"/>
 <TextView
  android:id="@+id/textView"/>
 </LinearLayout>

Então, em seu código, você deve definir um Touch Listener para seu LinearLayout principal.

final EditText searchEditText = (EditText) findViewById(R.id.editText);
mainLinearLayout.setOnTouchListener(new View.OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            // TODO Auto-generated method stub
            if(searchEditText.isFocused()){
                if(event.getY() >= 72){
                    //Will only enter this if the EditText already has focus
                    //And if a touch event happens outside of the EditText
                    //Which in my case is at the top of my layout
                    //and 72 pixels long
                    searchEditText.clearFocus();
                    InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
                }
            }
            Toast.makeText(getBaseContext(), "Clicked", Toast.LENGTH_SHORT).show();
            return false;
        }
    });

Espero que isso ajude algumas pessoas. Ou pelo menos os ajuda a começar a resolver seus problemas.

edc598
fonte
Sim, também cheguei a uma solução semelhante. Em seguida, transferi um subconjunto do sistema de visualização do Android para c ++ no opengl e construí este recurso nele)
note173
Este é realmente um exemplo muito bom. Eu estava procurando por um código como este, mas tudo que consegui encontrar foram soluções complexas. Usei ambas as coordenadas XY para ser mais preciso, já que também tinha poucos EditText com várias linhas. Obrigado!
Sumitk
3

Basta definir duas propriedades do pai dessa EditTextcomo:

android:clickable="true"
android:focusableInTouchMode="true"

Portanto, quando o usuário tocar fora da EditTextárea, o foco será removido porque o foco será transferido para a visualização principal.

Dhruvam Gupta
fonte
2

Eu tenho uma lista ListViewde EditTextpontos de vista. O cenário diz que após editar o texto em uma ou mais linha (s), devemos clicar em um botão chamado "terminar". Usei onFocusChangedna EditTextview dentro do listViewmas após clicar em finalizar os dados não estão sendo salvos. O problema foi resolvido adicionando

listView.clearFocus();

dentro de onClickListenerpara o botão "terminar" e os dados foram salvos com sucesso.

Muhannad A.Alhariri
fonte
Obrigado. Você economizou meu tempo, eu tinha esse cenário em recyclerview e estava perdendo o último valor editText depois de clicar no botão, porque o foco do último editText não mudou e assim por dianteFocusChanged não foi chamado. Gostaria de encontrar uma solução que faça com que o último editText perca o foco quando fora dele, clique no mesmo botão ... e encontre sua solução. Funcionou muito bem!
Reyhane Farshbaf
2

Eu realmente acho que é uma maneira mais robusta de usar do getLocationOnScreenque getGlobalVisibleRect. Porque encontro um problema. Existe um Listview que contém algum Edittext ee definido ajustpanna atividade. Acho getGlobalVisibleRectreturn um valor que parece incluir o scrollY nele, mas o event.getRawY está sempre perto da tela. O código abaixo funciona bem.

public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        View v = getCurrentFocus();
        if ( v instanceof EditText) {
            if (!isPointInsideView(event.getRawX(), event.getRawY(), v)) {
                Log.i(TAG, "!isPointInsideView");

                Log.i(TAG, "dispatchTouchEvent clearFocus");
                v.clearFocus();
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
            }
        }
    }
    return super.dispatchTouchEvent( event );
}

/**
 * Determines if given points are inside view
 * @param x - x coordinate of point
 * @param y - y coordinate of point
 * @param view - view object to compare
 * @return true if the points are within view bounds, false otherwise
 */
private boolean isPointInsideView(float x, float y, View view) {
    int location[] = new int[2];
    view.getLocationOnScreen(location);
    int viewX = location[0];
    int viewY = location[1];

    Log.i(TAG, "location x: " + location[0] + ", y: " + location[1]);

    Log.i(TAG, "location xWidth: " + (viewX + view.getWidth()) + ", yHeight: " + (viewY + view.getHeight()));

    // point is inside view bounds
    return ((x > viewX && x < (viewX + view.getWidth())) &&
            (y > viewY && y < (viewY + view.getHeight())));
}
Victor Choy
fonte
muito obrigado! Funcionou para todos os cenários de layout, como RecyclerView, etc. etc. Tentei outra solução, mas esta serve para todos! :) Bom trabalho!
Woppi
1

Para perder o foco quando outra visualização é tocada, ambas as visualizações devem ser definidas como view.focusableInTouchMode (true).

Mas parece que o uso de focos no modo de toque não é recomendado. Dê uma olhada aqui: http://android-developers.blogspot.com/2008/12/touch-mode.html

forumercio
fonte
Sim, eu li isso, mas não é exatamente o que eu quero. Não preciso da outra visão para me concentrar. Embora eu não veja nenhuma maneira alternativa agora ... Talvez, de alguma forma, fazer a visualização root para capturar o foco quando o usuário clicar em um filho não focável?
nota173 de
Pelo que eu sei, o focus não pode ser gerenciado por uma classe, é gerenciado por android ... sem ideias :(
forumercio
0

A melhor maneira é usar o método padrão clearFocus()

Você sabe como resolver códigos em onTouchListener certo?

Basta ligar EditText.clearFocus(). Isso limpará o foco em last EditText.

Torre Huy
fonte
0

Como @pcans sugeriu, você pode fazer isso substituindo dispatchTouchEvent(MotionEvent event) em sua atividade.

Aqui, obtemos as coordenadas de toque e as comparamos com os limites de visualização. Se o toque for executado fora de uma visão, faça algo.

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        View yourView = (View) findViewById(R.id.view_id);
        if (yourView != null && yourView.getVisibility() == View.VISIBLE) {
            // touch coordinates
            int touchX = (int) event.getX();
            int touchY = (int) event.getY();
            // get your view coordinates
            final int[] viewLocation = new int[2];
            yourView.getLocationOnScreen(viewLocation);

            // The left coordinate of the view
            int viewX1 = viewLocation[0];
            // The right coordinate of the view
            int viewX2 = viewLocation[0] + yourView.getWidth();
            // The top coordinate of the view
            int viewY1 = viewLocation[1];
            // The bottom coordinate of the view
            int viewY2 = viewLocation[1] + yourView.getHeight();

            if (!((touchX >= viewX1 && touchX <= viewX2) && (touchY >= viewY1 && touchY <= viewY2))) {

                Do what you want...

                // If you don't want allow touch outside (for example, only hide keyboard or dismiss popup) 
                return false;
            }
        }
    }
    return super.dispatchTouchEvent(event);
}

Também não é necessário verificar a existência de visualização e visibilidade se o layout de sua atividade não mudar durante o tempo de execução (por exemplo, você não adiciona fragmentos ou substitui / remove visualizações do layout). Mas se você quiser fechar (ou fazer algo semelhante) o menu de contexto personalizado (como na Google Play Store ao usar o menu flutuante do item) é necessário verificar a existência de visualização. Caso contrário, você receberá um NullPointerException.

Sabre
fonte
0

Este simples trecho de código faz o que você deseja

GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                KeyboardUtil.hideKeyboard(getActivity());
                return true;
            }
        });
mScrollView.setOnTouchListener((v, e) -> gestureDetector.onTouchEvent(e));
Andrii
fonte
0

Em Kotlin

hidekeyboard () é uma extensão Kotlin

fun Activity.hideKeyboard() {
    hideKeyboard(currentFocus ?: View(this))
}

Em atividade, adicione dispatchTouchEvent

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    if (event.action == MotionEvent.ACTION_DOWN) {
        val v: View? = currentFocus
        if (v is EditText) {
            val outRect = Rect()
            v.getGlobalVisibleRect(outRect)
            if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) {
                v.clearFocus()
                hideKeyboard()
            }
        }
    }
    return super.dispatchTouchEvent(event)
}

Adicione essas propriedades no primeiro item principal

android:focusableInTouchMode="true"
android:focusable="true"
Webserveis
fonte
0

Esta é minha versão baseada no código zMan. Não ocultará o teclado se a próxima visualização também for um texto de edição. Ele também não ocultará o teclado se o usuário apenas rolar a tela.

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        downX = (int) event.getRawX();
    }

    if (event.getAction() == MotionEvent.ACTION_UP) {
        View v = getCurrentFocus();
        if (v instanceof EditText) {
            int x = (int) event.getRawX();
            int y = (int) event.getRawY();
            //Was it a scroll - If skip all
            if (Math.abs(downX - x) > 5) {
                return super.dispatchTouchEvent(event);
            }
            final int reducePx = 25;
            Rect outRect = new Rect();
            v.getGlobalVisibleRect(outRect);
            //Bounding box is to big, reduce it just a little bit
            outRect.inset(reducePx, reducePx);
            if (!outRect.contains(x, y)) {
                v.clearFocus();
                boolean touchTargetIsEditText = false;
                //Check if another editText has been touched
                for (View vi : v.getRootView().getTouchables()) {
                    if (vi instanceof EditText) {
                        Rect clickedViewRect = new Rect();
                        vi.getGlobalVisibleRect(clickedViewRect);
                        //Bounding box is to big, reduce it just a little bit
                        clickedViewRect.inset(reducePx, reducePx);
                        if (clickedViewRect.contains(x, y)) {
                            touchTargetIsEditText = true;
                            break;
                        }
                    }
                }
                if (!touchTargetIsEditText) {
                    InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
                }
            }
        }
    }
    return super.dispatchTouchEvent(event);
}
Dennis H.
fonte