一.在状态栏之上弹自定义吐司
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坐标不断变化的同时还要执行组合动画?
思路:逻辑分为两部分:
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和功能在尝试使用,后续会继续做一些专题的总结,比如性能优化或者是代码重构,某些特殊功能点等等。笔者水平有限,如有错误请指正,谢谢。