????????Android 11以后,系统Toast的text toast被设置为固定在屏幕下方显示,通过setGravity已经不会生效。
????????为了解决这个问题,去看了Toast源码。从show()方法开始,
????????
/**
* Show the view for the specified duration.
*/
public void show() {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
checkState(mNextView != null || mText != null, "You must either set a text or a view");
} else {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();
try {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
// It's a custom toast
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
} else {
// It's a text toast
ITransientNotificationCallback callback =
new CallbackBinder(mCallbacks, mHandler);
service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
}
} else {
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
}
} catch (RemoteException e) {
// Empty
}
}
到TN的handleShow()方法,
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
mHorizontalMargin, mVerticalMargin,
new CallbackBinder(getCallbacks(), mHandler));
}
}
最终跟踪到ToastPresenter的show()方法,
/**
* Shows the toast in {@code view} with the parameters passed and callback {@code callback}.
*/
public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
@Nullable ITransientNotificationCallback callback) {
checkState(mView == null, "Only one toast at a time is allowed, call hide() first.");
mView = view;
mToken = token;
adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,
horizontalMargin, verticalMargin);
if (mView.getParent() != null) {
mWindowManager.removeView(mView);
}
try {
mWindowManager.addView(mView, mParams);
} catch (WindowManager.BadTokenException e) {
// Since the notification manager service cancels the token right after it notifies us
// to cancel the toast there is an inherent race and we may attempt to add a window
// after the token has been invalidated. Let us hedge against that.
Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);
return;
}
trySendAccessibilityEvent(mView, mPackageName);
if (callback != null) {
try {
callback.onToastShown();
} catch (RemoteException e) {
Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
}
}
}
对比android11之前的源码,发现基本没有变动,都是通过WindowManager的addView方法添加toast,而且LayoutParams也有设置Gravity属性,
/**
* Customizes {@code params} according to other parameters, ready to be passed to {@link
* WindowManager#addView(View, ViewGroup.LayoutParams)}.
*/
private void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken,
int duration, int gravity, int xOffset, int yOffset, float horizontalMargin,
float verticalMargin) {
Configuration config = mResources.getConfiguration();
int absGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
params.gravity = absGravity;
if ((absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
params.horizontalWeight = 1.0f;
}
if ((absGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
params.verticalWeight = 1.0f;
}
params.x = xOffset;
params.y = yOffset;
params.horizontalMargin = horizontalMargin;
params.verticalMargin = verticalMargin;
params.packageName = mContext.getPackageName();
params.hideTimeoutMilliseconds =
(duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
params.token = windowToken;
}
但是Gravity属性为什么没有生效呢?
? ? ? ? 考虑到是不是Toast的默认view的布局文件设置了match_parent属性,导致Gravity属性不生效。去Toast默认view的布局文件查看(默认view初始化在Toast类的makeText方法里,最终走的是ToastPresenter的getTextToastView方法),果然,发现默认view的布局文件宽高属性,均为match_parent,
/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used.
*
* @hide
*/
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
Toast result = new Toast(context, looper);
result.mText = text;
result.mDuration = duration;
return result;
} else {
Toast result = new Toast(context, looper);
View v = ToastPresenter.getTextToastView(context, text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
}
/**
* Returns the default text toast view for message {@code text}.
*/
public static View getTextToastView(Context context, CharSequence text) {
View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT, null);
TextView textView = view.findViewById(com.android.internal.R.id.message);
textView.setText(text);
return view;
}
<?xml version="1.0" encoding="utf-8"?>
<!--
/* //device/apps/common/res/layout/transient_notification.xml
**
** Copyright 2006, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">
<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="15dp"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/primary_text_default_material_light"
/>
</LinearLayout>
即虽然我们看到屏幕上的toast只是一个小小的文字部分,但实际其view是布满全局的,这个时候设置gravity就没有任何意义了,因为无论你设置gravity是什么值,view都是铺满全屏的。
? ? ? ? 解决方案:用到的解决方案就是给toast设置自己的布局文件,当然,自己的布局文件要注意不是铺满全屏的,然后把样式设置成跟系统自带的一样就可以了,此时,设置gravity为Gravity.CENTER就可以生效了(注意,不设置的话,默认还是在屏幕下方),参考代码
Toast toast = new Toast(getApplication());
View view = LayoutInflater.from(getApplication()).inflate(R.layout.layout_toast, null);
//根据自己需要对view设置text或其他样式
toast.setView(view);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.setDuration(Toast.LENGTH_SHORT);
toast.show();
参考布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="72dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#66000000"
android:paddingStart="40dp"
android:paddingEnd="40dp"
android:paddingTop="18dp"
android:paddingBottom="18dp">
<TextView
android:id="@+id/tv_toast_message"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:gravity="center_vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="24sp"
android:textColor="#FFFFFF"
android:text="测试toast"/>
</androidx.constraintlayout.widget.ConstraintLayout>
题外话:这次Android11修改toast,不仅把setGravity设置成不可用,还把setView设置成废弃方法了,但实际上我测试发现,无论android11还是android12,调用setView方法后,再设置setGravity还是可以用的,使用默认的setView时不可以用,最终什么时候这个方法不可用无法保证。
|