Исключение NullPointerException на устройствах Meizu в Editor.updateCursorPositionMz
В последнее время были сбои на моем Android приложение, на Meizu устройств только (M5c, M5S, M5 Примечание). Версия для Android: 6.0.
Вот полная трассировка стека:
Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.text.Layout.getLineForOffset(int)' on a null object reference
at android.widget.Editor.updateCursorPositionMz(Editor.java:6964)
at android.widget.Editor.updateCursorsPositions(Editor.java:1760)
at android.widget.TextView.getUpdatedHighlightPath(TextView.java:5689)
at android.widget.TextView.onDraw(TextView.java:5882)
at android.view.View.draw(View.java:16539)
at android.view.View.updateDisplayListIfDirty(View.java:15492)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:286)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:292)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:327)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:3051)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2855)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2464)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1337)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6819)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:894)
at android.view.Choreographer.doCallbacks(Choreographer.java:696)
at android.view.Choreographer.doFrame(Choreographer.java:631)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:880)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:5969)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:830)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:720)
Прямого отношения к моему коду нет (даже в других каналах). Я знаю только, что это происходит каждый раз во Фрагменте, в котором есть TextViews. Это может происходить, когда TextView набирает фокус, но я не могу быть уверенным. Конечно, я не могу воспроизвести ошибку, если не куплю Meizu.
Кроме того, поскольку верхний метод называется updateCursorPositionMz
, мне кажется, что это может быть внутренней проблемой в Meizu FlymeOS ("Mz" = "Meizu"?).
Кто-нибудь уже имел эту проблему, знает причину и как ее исправить?
Благодарю.
Ответы
Ответ 1
Наконец, у меня была возможность положить руку на Мэйдзу. Как я думал, авария происходит каждый раз, когда пользователь нажимает на поле, чтобы получить фокус.
В моем случае у меня был android.support.design.widget.TextInputEditText
внутри TextInputLayout
s. Просто заменив эти TextInputEditText
на AppCompatEditText
исправлена проблема:
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="...">
<android.support.v7.widget.AppCompatEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.TextInputLayout>
Поведение остается неизменным (поскольку TextInputEditText
расширяет AppCompatEditText
). Однако я все еще не нашел основной причины проблемы.
Ответ 2
Это было исправлено в компонентах материала для Android lib, см. Https://github.com/material-components/material-components-android/pull/358.
Ответ 3
В моем случае я убедился, что использование AppCompatEditText
вместо TextInputEditText
действительно предотвращает сбои, но мы не могли использовать это решение. Мы используем SDK с представлениями, которые расширяют TextInputEditText
, поэтому переключение на AppCompatEditText
потребует копирования/изменения небольшого количества кода SDK в наш проект.
Я попытался установить подсказку как для TextInputEditText
и для TextInputLayout
, но в итоге я увидел двойную подсказку (например, размытый текст, и я уверен, что не выпил слишком много).
Я взглянул на проблему GitHub, на которую ссылается @Andrew: https://github.com/android-in-china/Compatibility/issues/11
В этой проблеме они объясняют, что основной причиной является проблема в Meizu, когда TextInputEditText.getHint()
отличается от TextInputEditText.mHint
.
Когда TextInputEditText
находится внутри TextInputLayout
, а подсказка указывается в xml для TextInputEditText
, вспомогательная библиотека в основном "перемещает" подсказку к содержащему TextInputLayout
: она устанавливает его в контейнере, а затем устанавливает его равным нулю в тексте редактирования.
Этот источник, который делает это, находится в TextInputLayout.setEditText():
// If we do not have a valid hint, try and retrieve it from the EditText, if enabled
if (hintEnabled) {
if (TextUtils.isEmpty(hint)) {
// Save the hint so it can be restored on dispatchProvideAutofillStructure();
originalHint = this.editText.getHint();
setHint(originalHint);
// Clear the EditText hint as we will display it ourselves
this.editText.setHint(null);
}
Затем, когда вы вызываете TextInputEditText.getHint()
, он возвращает подсказку контейнера.
Это несоответствие между getHint()
(значение подсказки) и mHint
(null), кажется, создает проблему для устройств Meizu
Я нашел другой способ избежать этой проблемы.
На устройствах Meizu я:
1) программно сбросить подсказку TextInputEditText
обратно к тому, к чему он был изначально установлен из xml (вызывая переопределенный getHint()
который возвращает подсказку контейнера).
2) установите цвет подсказки TextInputEditText
на прозрачный, чтобы избежать эффекта двойной/размытой подсказки:
private void hackFixHintsForMeizu(TextInputEditText... editTexts) {
String manufacturer = Build.MANUFACTURER.toUpperCase(Locale.US);
if (manufacturer.contains("MEIZU")) {
for (TextInputEditText editText : editTexts) {
editText.setHintTextColor(Color.TRANSPARENT);
editText.setHint(editText.getHint());
}
}
}
Ответ 4
Я основал свое решение на FixedTextInputEditText
как указано в https://github.com/android-in-china/Compatibility/issues/11#issuecomment-427560370.
Прежде всего, я создал фиксированный экземпляр TextInputEditText
:
public class MeizuTextInputEditText extends TextInputEditText {
public MeizuTextInputEditText(Context context) {
super(context);
}
public MeizuTextInputEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MeizuTextInputEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public CharSequence getHint() {
try {
return getMeizuHintHack();
} catch (Exception e) {
return super.getHint();
}
}
private CharSequence getMeizuHintHack() throws NoSuchFieldException, IllegalAccessException {
Field textView = TextView.class.getDeclaredField("mHint");
textView.setAccessible(true);
return (CharSequence) textView.get(this);
}
}
Но тогда я должен был бы заменить все мои использования TextInputEditText
на MeizuTextInputEditText
который вы не можете легко сделать на большей кодовой базе. Также при создании будущих представлений вы всегда должны рассмотреть возможность использования MeizuTextInputEditText
вместо "сломанного". Забыв об этом, вы легко столкнетесь с проблемами производства.
Таким образом, окончательное исправление состоит из пользовательского класса представления, и вместе с библиотекой ViewPump (https://github.com/InflationX/ViewPump) мы можем легко это сделать. Как описано в документации, вам нужно зарегистрировать специальный перехватчик, который выглядит следующим образом:
public class TextInputEditTextInterceptor implements Interceptor {
@Override
public InflateResult intercept(Chain chain) {
InflateRequest request = chain.request();
View view = inflateView(request.name(), request.context(), request.attrs());
if (view != null) {
return InflateResult.builder()
.view(view)
.name(view.getClass().getName())
.context(request.context())
.attrs(request.attrs())
.build();
} else {
return chain.proceed(request);
}
}
@Nullable
private View inflateView(String name, Context context, AttributeSet attrs) {
if (name.endsWith("TextInputEditText")) {
return new MeizuTextInputEditText(context, attrs);
}
return null;
}
}
И регистрация этого пользовательского перехватчика выполняется так же, как в документации, путем установки ViewPump в onCreate вашей деятельности:
@Override
@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewPump.Builder viewPumpBuilder = ViewPump.builder();
if (isMeizuDevice()) {
viewPumpBuilder.addInterceptor(new TextInputEditTextInterceptor());
}
ViewPump.init(viewPumpBuilder.build());
}
Как вы можете видеть, я только раздуваю MeizuTextInputEditText
если обнаруживается устройство Meizu. Таким образом, отражение не срабатывает для устройств, которые в нем не нуждаются. Также этот метод является базовым классом Activity, который у меня есть, и который расширяет все остальные действия в моем проекте, так что каждое действие, которое запускается в моем проекте И где устройство Meizu, будет исправлено автоматически!
Ответ 5
Добавление подсказки как к TextInputLayout
и к TextInputEditText
сбой для меня:
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login"
app:hintAnimationEnabled="false">
<android.support.design.widget.TextInputEditText
android:id="@+id/text_input_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login" />
</android.support.design.widget.TextInputLayout>
Наконец, сбросьте подсказку TextInputEditText
программно, чтобы избежать очень темного цвета текста подсказки:
editText = findViewById(R.id.text_input_edit_text);
editText.setHint("");
Проверено на Meizu MX6 с Android 6.0
Ответ 6
Удалить подсказку из xml: либо из TextInputLayout, либо TextInputEditText.
Для материальных компонентов
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_input_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
Для поддержки дизайна
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/text_input_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.TextInputLayout>
В коде программный код программно:
val myTextInputEditText = findViewById<TextInputEditText>(R.id.text_input_edit_text)
myTextInputEditText.hint = "Your hint"
Протестировано на Meizu M5S, Android 6.0
Ответ 7
Я использую Kotlin и Fragments, и я просто рекурсивно исправляю все текстовые вводы в onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fixTextInputEditText(view) // call this in onViewCreated
}
private fun fixTextInputEditText(view: View) {
val manufacturer = Build.MANUFACTURER.toUpperCase(Locale.US)
if ("MEIZU" in manufacturer) {
val views = getAllTextInputs(view)
views.forEach(::hackFixHintsForMeizu)
}
}
private fun getAllTextInputs(v: View): List<TextInputEditText> {
if (v !is ViewGroup) {
val editTexts = mutableListOf<TextInputEditText>()
(v as? TextInputEditText)?.let {
editTexts += it
}
return editTexts
}
val result = mutableListOf<TextInputEditText>()
for (i in 0 until v.childCount) {
val child = v.getChildAt(i)
result += getAllTextInputs(child)
}
return result
}
private fun hackFixHintsForMeizu(editText: TextInputEditText) {
if (editText.hint != null) {
editText.setHintTextColor(Color.TRANSPARENT)
editText.hint = editText.hint
}
}
Ответ 8
Ни один из вышеперечисленных вариантов не работал у меня без изменений.
Мое приложение использует фрагменты, TextInputEditText иногда используется без TextInputLayout, в то время обновление до последней версии AndroidX не было возможным, замена TextInputEditText также не была возможной в настоящее время.
Моя версия (на основе этих решений и Google Fix):
import android.os.Build
import java.util.*
import android.content.Context
import android.support.design.widget.TextInputEditText
import android.util.AttributeSet
import android.widget.TextView
import android.support.design.widget.TextInputLayout
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import java.lang.reflect.Field
import java.lang.reflect.Method
import android.support.design.R
class MyInputEditText(context: Context?, attrs: AttributeSet?,defStyleAttr:Int) : TextInputEditText(context, attrs,defStyleAttr){
constructor(context: Context?, attrs: AttributeSet?):this(context,attrs,R.attr.editTextStyle)
constructor(context: Context?):this(context,null,R.attr.editTextStyle)
private val buggyMeizu = ("meizu") in Build.MANUFACTURER.toLowerCase(Locale.US)
private lateinit var getTextInputLayoutMethod:Method
private lateinit var providesHintMethod:Method
private lateinit var mHintField:Field
init {
if (buggyMeizu) {
getTextInputLayoutMethod=TextInputEditText::class.java.getDeclaredMethod("getTextInputLayout")
getTextInputLayoutMethod.isAccessible=true
providesHintMethod=TextInputLayout::class.java.getDeclaredMethod("isProvidingHint")
providesHintMethod.isAccessible=true
mHintField=TextView::class.java.getDeclaredField("mHint")
mHintField.isAccessible=true
}
}
private fun getTILProvidesHint():Boolean {
val layout=getTIL()
if (layout!=null) {
val result=providesHintMethod.invoke(layout) as Boolean
return result;
} else {
return false
}
}
private fun getTIL():TextInputLayout? = getTextInputLayoutMethod.invoke(this) as TextInputLayout?
private fun getBaseHint():CharSequence? = mHintField.get(this) as CharSequence?
override fun getHint(): CharSequence? {
if (!buggyMeizu) {
return super.getHint()
} else {
val layout=getTIL()
return if (layout != null && (getTILProvidesHint()) )
layout.hint
else
provideHintWrapped()
}
}
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
val needHint=(outAttrs.hintText==null)
val ic = super.onCreateInputConnection(outAttrs)
if (buggyMeizu) {
if (ic != null && needHint) {
outAttrs.hintText = this.provideHintWrapped()
}
}
return ic
}
private fun provideHintWrapped():CharSequence? {
val hintFromLayout=getHintFromLayoutMine()
if (hintFromLayout!=null) {
return hintFromLayout
} else {
val baseHint=getBaseHint()
if (baseHint!=null) {
return baseHint
} else {
return null
}
}
}
private fun getHintFromLayoutMine(): CharSequence? {
val layout = getTIL()
return layout?.hint
}
override fun onAttachedToWindow() {
if (buggyMeizu) {
val baseHint=getBaseHint()
if (getTIL() != null
&& getTILProvidesHint()
&& baseHint == null) {
this.hint=""
}
}
super.onAttachedToWindow()
}
}
После этого найдите и замените TextInputEditText на MyInputEditText во всех файлах макета и кода.