NullPointerException на устройствах Meizu в Editor.updateCursorPositionMz
В последнее время произошли сбои в моем приложении Android, только на устройствах Meizu (M5c, M5s, M5 Note). Версия 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.
Кроме того, так как вызывается метод top updateCursorPositionMz
мне кажется, это может быть внутренней проблемой в Memeu FlymeOS ("Mz" = "Meizu"?).
Кто-нибудь уже имел эту проблему, знает причину и как ее исправить?
Благодарю.
3 ответа
Наконец-то у меня появилась возможность положить руки на Meizu. Как я и думал, сбой происходит каждый раз, когда пользователь нажимает на поле, чтобы получить фокус.
В моем случае у меня были некоторые 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
). Я до сих пор не нашел причину проблемы, хотя.
Это было исправлено в компонентах материала для Android lib, см. https://github.com/material-components/material-components-android/pull/358
Я основал свое решение на FixedTextInputEditText
как уже упоминалось в https://github.com/android-in-china/Compatibility/issues/11.
Прежде всего, я создал исправленный 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, будет исправлено автоматически!
В моем случае я проверил, что с помощью 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
: он устанавливает его в контейнере, а затем устанавливает его равным нулю в тексте редактирования.
Затем, когда вы звоните 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());
}
}
}
Добавление подсказки на обоих 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
Удалить подсказку из 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
Я использую 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
}
}
Это исправление теперь включено в новый выпуск компонентов материалов здесь: https://github.com/material-components/material-components-android/releases/tag/1.1.0-alpha09
Ни один из вышеперечисленных вариантов не работал у меня без изменений.
Мое приложение использует фрагменты, TextInputEditText иногда используется без TextInputLayout, в то время обновление до последней версии AndroidX не было возможным, замена TextInputEditText также не была возможной в настоящее время.
Моя версия (на основе этого решения и исправления Google):
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 во всех файлах макета и кода.