1.需求:UI设计师设计的原型图是在状态栏之上的位置弹一个自定义吐司,我们的应用内全部都是沉浸式状态栏,将状态栏隐藏掉了的。
2.解决方案:首先给toast设置marginTop为负的状态栏高度是无效的,然后查阅相关资料发现Toast是显示在Window之上的,查看Toast的源码发现实际起作用的是Toast的一个静态内部类TN,TN有一个成员变量mParams,实际上起作用的就是WindowManager.LayoutParams。 代码如下:
private static class TN extends ITransientNotification.Stub { private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); private static final int SHOW = 0; private static final int HIDE = 1; private static final int CANCEL = 2; final Handler mHandler; int mGravity; int mX, mY; float mHorizontalMargin; float mVerticalMargin; View mView; View mNextView; int mDuration; WindowManager mWM; String mPackageName; static final long SHORT_DURATION_TIMEOUT = 4000; static final long LONG_DURATION_TIMEOUT = 7000; TN(String packageName, @Nullable Looper looper) { // XXX This should be changed to use a Dialog, with a Theme.Toast // defined that sets up the layout params appropriately. final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; mPackageName = packageName; if (looper == null) { // Use Looper.myLooper() if looper is not specified. looper = Looper.myLooper(); if (looper == null) { throw new RuntimeException( "Can't toast on a thread that has not called Looper.prepare()"); } } mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } case HIDE: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; break; } case CANCEL: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; try { getService().cancelToast(mPackageName, TN.this); } catch (RemoteException e) { } break; } } } }; } /** * schedule handleShow into the right thread */ @Override public void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(SHOW, windowToken).sendToTarget(); } /** * schedule handleHide into the right thread */ @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.obtainMessage(HIDE).sendToTarget(); } public void cancel() { if (localLOGV) Log.v(TAG, "CANCEL: " + this); mHandler.obtainMessage(CANCEL).sendToTarget(); } 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; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); // We can resolve the Gravity here by using the Locale for getting // the layout direction final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; mParams.token = windowToken; if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this); // 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. try { mWM.addView(mView, mParams); trySendAccessibilityEvent(); } catch (WindowManager.BadTokenException e) { /* ignore */ } } } private void trySendAccessibilityEvent() { AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mView.getContext()); if (!accessibilityManager.isEnabled()) { return; } // treat toasts as notifications since they are used to // announce a transient piece of information to the user AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); event.setClassName(getClass().getName()); event.setPackageName(mView.getContext().getPackageName()); mView.dispatchPopulateAccessibilityEvent(event); accessibilityManager.sendAccessibilityEvent(event); } public void handleHide() { if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); if (mView != null) { // note: checking parent() just to make sure the view has // been added... i have seen cases where we get here when // the view isn't yet added, so let's try not to crash. if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeViewImmediate(mView); } mView = null; } } }继续查找源码发现Toast有个成员函数是getWindowParams(),然后想拿toast调用它,仔细看,不对。这个函数是添加了@hide注解的,无语
/** * Gets the LayoutParams for the Toast window. * @hide */ public WindowManager.LayoutParams getWindowParams() { return mTN.mParams; }既然正常途径拿不到,只有放出终极大招反射去获取这个方法了。 关键代码如下
//设置吐司可以在状态栏之上显示 try { Class<?> aClass = Class.forName(name); Method method = aClass.getDeclaredMethod("getWindowParams"); method.setAccessible(true); WindowManager.LayoutParams layoutParams1 = (WindowManager.LayoutParams) method.invoke(toast); layoutParams1.flags = layoutParams1.flags | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; } catch (Exception e) { e.printStackTrace(); }关键就是设置了一个Flag: layoutParams1.flags | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 这个flag的含义是忽略状态栏的高度。
log日志如下
Fatal Exception: java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{57e90c1 position=17 id=17, oldPos=-1, pLpos:-1 no parent} android.support.v7.widget.RecyclerView{78f7e1c VFED..... ........ 55,102-1080,350 #7f09011b app:id/recyclerView} at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5610) at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5792) at android.support.v7.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:285) at android.support.v7.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:342) at android.support.v7.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:358) at android.support.v7.widget.GapWorker.prefetch(GapWorker.java:365) at android.support.v7.widget.GapWorker.run(GapWorker.java:396) at android.os.Handler.handleCallback(Handler.java:754) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:163) at android.app.ActivityThread.main(ActivityThread.java:6401) at java.lang.reflect.Method.invoke(Method.java) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:791)尝试了多种方式,最后同事帮找到了必现的操作,原因如下:应用中多处用到了RecyclerView,一个隐藏时Adapter的List做了clear操作,造成另一个RecyclerView滑动时会崩溃,躺着也中枪,后面修改了逻辑,改成隐藏时不重新初始化数据,只有显示时才重新init数据。
log如下
Fatal Exception: java.lang.IllegalArgumentException: Comparison method violates its general contract! at java.util.TimSort.mergeLo(TimSort.java:777) at java.util.TimSort.mergeAt(TimSort.java:514) at java.util.TimSort.mergeCollapse(TimSort.java:439) at java.util.TimSort.sort(TimSort.java:245) at java.util.Arrays.sort(Arrays.java:1498) at java.util.ArrayList.sort(ArrayList.java:1470) at java.util.Collections.sort(Collections.java:201解决方案:用Comparator接口对集合进行排序时,返回值不要直接返回p0.compareTo(p1),要考虑p0 == p1的情况。
注意:这个操作是耗时操作,所以需要先在Background线程中做处理,然后再切换回UI线程显示图片。
思路:逻辑分为两部分:
1.封装一个带动画的自定义View,View内处理组合动画的显示逻辑 2.View开启一个自定义的属性动画,在回调中不断设置View的位置。
自定义View的代码如下:
“` /** * Created by liuxu on 2018/6/20. */
public class AnimImageView extends View { /** * view的宽度 */ private int width; /** * view的高度 */ private int height;
private int realWidth; //绘制时真正用到的宽度 private int realHeight;//绘制时真正用到的高度 /** * 圆角半径 */ private int circleAngle; /** * 默认两圆圆心之间的距离=需要移动的距离 */ private int default_two_circle_distance; /** * 两圆圆心之间的距离 */ private int two_circle_distance; /** * 背景颜色 */ private int bg_color = 0xffbc7d53; /** * 按钮文字字符串 */ private String buttonString = "确认完成"; /** * 动画执行时间 */ private int duration = 1000; /** * view向上移动距离 */ private int move_distance = 50; /** * 圆角矩形画笔 */ private Paint paint; /** * 文字画笔 */ private Paint textPaint; /** * 文字绘制所在矩形 */ private Rect textRect = new Rect(); /** * 根据view的大小设置成矩形 */ private RectF rectf = new RectF(); /** * 动画集 */ private AnimatorSet animatorSet = new AnimatorSet(); /** * 圆到圆角矩形过度的动画 0.2s */ private ValueAnimator animator_circle_to_square; /** * view上移的动画 动画的全部 */ private ObjectAnimator animator_move_to_up; /** * 渐变动画 透明度由1到0 */ private ObjectAnimator animator_alpha; public void setBg_color(int bg_color) { this.bg_color = bg_color; paint.setColor(bg_color); paint.setAlpha(126); } /** * 保持不变的动画 2000ms */ private ValueAnimator animator_stay; private AnimationButtonListener animationButtonListener; public void setAnimationButtonListener(AnimationButtonListener listener) { animationButtonListener = listener; } public AnimImageView(Context context) { this(context, null); } public void setButtonString(String buttonString) { this.buttonString = buttonString; } public AnimImageView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public AnimImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initPaint(); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (animationButtonListener != null) { animationButtonListener.animationFinish(); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); } public void setCircleAngle(int circleAngle) { this.circleAngle = circleAngle; } private void initPaint() { paint = new Paint(); paint.setStrokeWidth(4); paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); paint.setColor(bg_color); textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint.setTextSize(MoliveKit.getPixels(13f)); textPaint.setColor(Color.WHITE); textPaint.setTextAlign(Paint.Align.CENTER); textPaint.setAntiAlias(true); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = w; height = h; } public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public void setRealWidth(int realWidth) { this.realWidth = realWidth; } public void setRealHeight(int realHeight) { this.realHeight = realHeight; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); draw_oval_to_circle(canvas); //绘制文字 drawText(canvas); } /** * 绘制圆形变成圆角矩形 * * @param canvas 画布 */ private void draw_oval_to_circle(Canvas canvas) { rectf.left = two_circle_distance; rectf.top = (height - realHeight) / 2; rectf.right = realWidth - two_circle_distance; rectf.bottom = height - (height - realHeight) / 2; //画圆角矩形 canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint); } /** * 绘制文字 * * @param canvas 画布 */ private void drawText(Canvas canvas) { textRect.left = 0; textRect.top = 0; textRect.right = width; textRect.bottom = height; Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt(); int baseline = (textRect.bottom + textRect.top - fontMetrics.bottom - fontMetrics.top) / 2; //文字绘制到整个布局的中心位置 canvas.drawText(buttonString, textRect.centerX(), baseline, textPaint); } /** * 初始化所有动画 */ private void initAnimation() { set_circle_to_square(); set_animator_stay(); set_animator_alpha(); set_move_to_up(); animatorSet.play(animator_circle_to_square) .with(animator_stay) .with(animator_alpha) .with(animator_move_to_up); } /** * 上升动画 */ private void set_move_to_up() { final float curTranslationY = this.getTranslationY(); animator_move_to_up = ObjectAnimator.ofFloat(this, "translationY", curTranslationY, curTranslationY - move_distance); animator_move_to_up.setDuration(3000); animator_move_to_up.setInterpolator(new AccelerateDecelerateInterpolator()); animator_move_to_up.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { } }); } /** * 透明度变化动画 */ private void set_animator_alpha() { animator_alpha = ObjectAnimator.ofFloat(this, "alpha", 1f, 0); animator_alpha.setDuration(800); animator_alpha.setStartDelay(2200); animator_alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { invalidate(); } }); } /** * 保持动画 */ private void set_animator_stay() { animator_stay = ValueAnimator.ofInt(0, 0); animator_stay.setDuration(2000); animator_stay.setStartDelay(200); animator_stay.addUpdateListener(animation -> { two_circle_distance = (int) animation.getAnimatedValue(); invalidate(); }); } /** * 拉伸动画 */ private void set_circle_to_square() { animator_circle_to_square = ValueAnimator.ofInt(default_two_circle_distance, 0); animator_circle_to_square.setDuration(200); animator_circle_to_square.addUpdateListener(animation -> { two_circle_distance = (int) animation.getAnimatedValue(); int alpha = 255 - (two_circle_distance * 255) / default_two_circle_distance; textPaint.setAlpha(alpha); invalidate(); }); } /** * 接口回调 */ public interface AnimationButtonListener { /** * 动画完成回调 */ void animationFinish(); } /** * 启动动画 */ public void start() { //一些必要参数的初始化 initAnimation(); animatorSet.start(); } public int getWidthForTextSize() { Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextSize(15f); mTextPaint.setColor(Color.WHITE); // Define the string. // Measure the width of the text string. int textWidth = (int) mTextPaint.measureText(buttonString); return textWidth; }```1.协程是为了解决什么问题? 协程是为了解决各种异步回调带来的代码臃肿。另外协程的根本目的是为了提高系统对资源的利用率。
2.如何使用 需要在build中引入两个包。kotlinx-coroutines-core和kotlinx-coroutines-android 代码如下:
launch { //运行在工作线程 do some 耗时操作 launch(UI) { //运行在主线程 do some 更新UI操作 } }
比如kotlin目前也是在不断的学习中,很多新的API和功能在尝试使用,后续会继续做一些专题的总结,比如性能优化或者是代码重构,某些特殊功能点等等。笔者水平有限,如有错误请指正,谢谢。
