Android 遇坑之路及解决方案

一.在状态栏之上弹自定义吐司

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的含义是忽略状态栏的高度。

二.RecyclerView内部的一个bug

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数据。

三.集合排序的bug,Collection,sorc()

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的情况。

四.把一个透明的Drawable处理成目标颜色的图片?

 /**
     *
     * @param drawable  图片
     * @param colors    颜色数组
     * @return          处理后的图片
     */
    public static Drawable tintDrawable(Drawable drawable, ColorStateList colors) {
        final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
        DrawableCompat.setTintList(wrappedDrawable, colors);
        return wrappedDrawable;
    }

注意:这个操作是耗时操作,所以需要先在Background线程中做处理,然后再切换回UI线程显示图片。

五.View坐标不断变化的同时还要执行组合动画?

思路:逻辑分为两部分:

扫描二维码关注公众号,回复: 1949166 查看本文章

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;
}```

六.Kotlin协程

1.协程是为了解决什么问题?
协程是为了解决各种异步回调带来的代码臃肿。另外协程的根本目的是为了提高系统对资源的利用率。

2.如何使用
需要在build中引入两个包。kotlinx-coroutines-core和kotlinx-coroutines-android
代码如下:


launch {
//运行在工作线程
do some 耗时操作
launch(UI) {
//运行在主线程
do some 更新UI操作
}
}

七.总结

比如kotlin目前也是在不断的学习中,很多新的API和功能在尝试使用,后续会继续做一些专题的总结,比如性能优化或者是代码重构,某些特殊功能点等等。笔者水平有限,如有错误请指正,谢谢。

猜你喜欢

转载自blog.csdn.net/liuxu841911548/article/details/80953727