RecyclerView+SnapHelper实现ViewPager滑动效果

RecyclerView+SnapHelper实现ViewPager滑动效果

SnapHelper结合RecyclerView使用,能很方便的实现ViewPager滑动效果。SnapHelper是一个抽象类,Google内置了两个默认实现类,LinearSnapHelper和PagerSnapHelper。

LinearSnapHelper的使用方法

使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动多个条目。

1
2
3
4
5
LinearLayoutManager manager = new LinearLayoutManager(getContext());
manager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(manager);
LinearSnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

PagerSnapHelper的使用方法

使RecyclerView像ViewPager一样的效果,每次只能滑动一页。

1
2
3
4
5
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(linearLayoutManager);
PagerSnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

原文地址:https://developer.aliyun.com/article/665537

RoundImageView圆角控件

RoundImageView圆角控件

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;

public class RoundImageView extends AppCompatImageView {
private static final String TAG = "RoundImageView";

private int radius = 0;

public RoundImageView(Context context) {
this(context, null);
}

public RoundImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public RoundImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup(context, attrs, defStyleAttr);
}

private void setup(Context context, AttributeSet attrs, int defStyleAttr) {
try {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
radius = a.getDimensionPixelSize(R.styleable.RoundImageView_riv_radius, 0);
Log.d(TAG, "RoundImageView: radius=" + radius);
a.recycle();
} catch (Exception e) {
e.printStackTrace();
}
}

public void setRadius(int radius) {
this.radius = radius;
}

@Override
protected void onDraw(Canvas canvas) {
if (radius > 0) {
Path path = new Path();
path.addRoundRect(new RectF(0, 0, getWidth(), getHeight()), radius, radius, Path.Direction.CW);
canvas.clipPath(path);//设置可显示的区域,canvas四个角会被剪裁掉
}
super.onDraw(canvas);
}
}

attrs.xml 文件中定义控件的圆角dp值属性:

1
2
3
<declare-styleable name="RoundImageView">
<attr name="riv_radius" format="dimension" />
</declare-styleable>

使用示例

1
2
3
4
5
6
<com.xx.ui.widget.RoundImageView
android:id="@+id/image_view"
android:layout_width="120dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
app:riv_radius="8dp" />

自定义TextView实现多个文案切换炫酷动画

当显示2个或2个以上文案时,每隔2秒切换气泡文案
4C3FBC04FF667DAAEF17AA6B8F8F7A46

核心实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import androidx.appcompat.widget.AppCompatTextView

class TextViewSwitcher @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

private var strs: List<String>? = null
private var startPos: Int = 0
private val timeStep = 2000L // 2S
private val TAG = "TextViewSwitcher"

private val showNext = Runnable {
showNextStr()
}

override fun onAttachedToWindow() {
Log.d(TAG, "onAttachedToWindow: ")
super.onAttachedToWindow()
val size = strs?.size ?: 0
if (size > 1) {
handler.postDelayed(showNext, timeStep)
}
}

override fun onDetachedFromWindow() {
Log.d(TAG, "onDetachedFromWindow: ")
handler.removeCallbacks(showNext)
super.onDetachedFromWindow()
}

fun setTextList(strs: List<String>?, startPos: Int = 0) {
Log.d(TAG, "setTextList: ")
if (strs.isNullOrEmpty()) return
this.strs = strs
this.startPos = startPos
if (strs.size == 1) {
text = strs[0]
} else {
this.startPos = startPos % strs.size
text = strs[this.startPos]
}
}

private fun showNextStr() {
var startPos = this.startPos + 1
val size = strs?.size ?: 0
if (size <= 1) return
if (startPos >= size) startPos %= size

this.startPos = startPos
changeTextWithAnimator(this, strs?.get(startPos))

handler.postDelayed(showNext, timeStep)
}

private fun changeTextWithAnimator(
textView: AppCompatTextView?,
nextContent: String?
) {
if (textView != null) {
val animator = ValueAnimator.ofFloat(0f, 2f)
animator.duration = 400
var changed = false
textView.pivotX = 0f
val height = textView.measuredHeight
if (height > 0) {
textView.pivotY = height.toFloat()
}

val startWidth = textView.measuredWidth
var endWidth = 0
val params = textView.layoutParams
animator.addUpdateListener { animation ->
val value = animation.animatedValue as Float
when {
value < 1f -> {
textView.rotation = 360 - value * 60
textView.alpha = 1 - value
}
value > 1f -> {
textView.alpha = value - 1
textView.rotation = 360 - (2 - value) * 60
if (!changed) {
changed = true
textView.text = nextContent

val measureSpec = MeasureSpec.makeMeasureSpec(
0,
MeasureSpec.UNSPECIFIED
)
textView.measure(measureSpec, measureSpec)
endWidth = textView.measuredWidth

Log.d(TAG, "changeTextWithAnimator: endWidth=$endWidth")
} else {
if (endWidth > 0) {
params.width =
(startWidth + (endWidth - startWidth) * (value - 1)).toInt()
textView.layoutParams = params
}
}
}
}
}
animator.start()
}
}
}

由于动画要在顶部浮层,这样动画才能不被父类容器的大小所限制和切割,所以,直接在PopWindow中显示。
具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupWindow

object HomePromptView {
@JvmStatic
fun showTipPopView(view: View, typedStr: String?): PopupWindow {

val rootView = LayoutInflater.from(view.context).inflate(R.layout.home_prompt_layout, null)
val promptTv = rootView.findViewById<TextViewSwitcher>(R.id.tv_prompt)
promptTv.setTextList(typedStr?.split("|"))
rootView.isClickable = false

// PopWindow
val popTipWid = PopupWindow(
rootView,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
popTipWid.isTouchable = false

// android.view.WindowManager$BadTokenException:
// Unable to add window -- token null is not valid;
// is your activity running?
try {
popTipWid.showAtLocation(view.rootView, 0, 0, 0)
layoutPromptLocation(promptTv, view)
// popTipWid.showAsDropDown(view, UiUtils.dip2px(39), -UiUtils.dip2px(48))
view.addOnLayoutChangeListener { view, i, i2, i3, i4, i5, i6, i7, i8 ->
layoutPromptLocation(promptTv, view)
}
} catch (e: Exception) {
e.printStackTrace()
}
return popTipWid
}


private fun layoutPromptLocation(
promptTv: TextViewSwitcher,
view: View
) {
try {
val params = promptTv.layoutParams as ViewGroup.MarginLayoutParams
val location = IntArray(2)
view.getLocationInWindow(location)
params.topMargin =
location[1] - UiUtils.getStatusBarHeight(view.context) + UiUtils.dip2px(4)
params.leftMargin = location[0] + UiUtils.dip2px(25)
promptTv.layoutParams = params
} catch (e: Exception) {
e.printStackTrace()
}
}

@JvmStatic
fun dismissTipPopView(popTipWid: PopupWindow?) {
popTipWid?.dismiss()
}

}

添加布局代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent">

<******.TextViewSwitcher
android:id="@+id/tv_prompt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/common_ui_shape_red_heavy_prompt"
android:maxLines="1"
android:paddingStart="5dp"
android:paddingTop="1.5dp"
android:paddingEnd="5dp"
android:paddingBottom="1.5dp"
android:textColor="@color/white"
android:textSize="10sp"
tools:text="硬核安利">

</******.TextViewSwitcher>

</FrameLayout>

drawable:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomRightRadius="9dp"
android:topLeftRadius="9dp"
android:topRightRadius="9dp" />
<solid android:color="#FF5E79" />
<stroke
android:width="0.5dp"
android:color="@color/white" />
</shape>

SpannableString 之显示查看全文

SpannableString 之显示查看全文

显示内容超出规定的行数之后,显示 展开收起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.text.Layout;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.AlignmentSpan;
import android.text.style.ClickableSpan;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;

import java.lang.reflect.Field;

/**
* Description : 显示展开和收起
* PackageName : com.mrtrying.widget
* Created by mrtrying on 2019/4/17 17:21.
* e_mail : ztanzeyu@gmail.com
*/
public class ExpandableTextView extends AppCompatTextView {
private static final String TAG = ExpandableTextView.class.getSimpleName();

public static final String ELLIPSIS_STRING = new String(new char[]{'\u2026'});
private static final String DEFAULT_OPEN_SUFFIX = " 展开";
private static final String DEFAULT_CLOSE_SUFFIX = " 收起";
volatile boolean animating = false;
boolean isClosed = false;
private int mMaxLines = getMaxLines();
private int initWidth = 0;
private CharSequence originalText;

private SpannableStringBuilder mOpenSpannableStr, mCloseSpannableStr;

private boolean hasAnimation = false;
private Animation mOpenAnim, mCloseAnim;
private int mOpenHeight, mCLoseHeight;
private boolean mExpandable;
private boolean mCloseInNewLine;
@Nullable
private SpannableString mOpenSuffixSpan, mCloseSuffixSpan;
private String mOpenSuffixStr = DEFAULT_OPEN_SUFFIX;
private String mCloseSuffixStr = "";
private int mOpenSuffixColor, mCloseSuffixColor;
private int mNormalColor = Color.parseColor("#FF222222");

private View.OnClickListener mOnClickListener;

private CharSequenceToSpannableHandler mCharSequenceToSpannableHandler;
private IOnOpenSuffixSpanListener onOpenSuffixSpanListener;

public ExpandableTextView(Context context) {
super(context);
initialize();
}

public ExpandableTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}

public ExpandableTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}

/**
* 初始化
*/
private void initialize() {
mOpenSuffixColor = mCloseSuffixColor = Color.parseColor("#FF0091FF");
setMovementMethod(CustomLinkMovementMethod.getInstance());
// setIncludeFontPadding(false);
updateOpenSuffixSpan();
updateCloseSuffixSpan();
}

@Override
public boolean hasOverlappingRendering() {
return false;
}

public void setOriginalText(CharSequence originalText) {
this.originalText = originalText;
mExpandable = false;
mCloseSpannableStr = new SpannableStringBuilder();
final int maxLines = mMaxLines;
SpannableStringBuilder tempText = charSequenceToSpannable(originalText);
mOpenSpannableStr = charSequenceToSpannable(originalText);

if (maxLines != -1) {
Layout layout = createStaticLayout(tempText);
mExpandable = layout.getLineCount() > maxLines;
if (mExpandable) {
//拼接展开内容
if (mCloseInNewLine) {
mOpenSpannableStr.append("\n");
}
if (mCloseSuffixSpan != null) {
mOpenSpannableStr.append(mCloseSuffixSpan);
}
//计算原文截取位置
int endPos = layout.getLineEnd(maxLines - 1);
if (originalText.length() <= endPos) {
mCloseSpannableStr = charSequenceToSpannable(originalText);
} else {
mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, endPos));
}
SpannableStringBuilder tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
if (mOpenSuffixSpan != null) {
tempText2.append(mOpenSuffixSpan);
}
//循环判断,收起内容添加展开后缀后的内容
Layout tempLayout = createStaticLayout(tempText2);
while (tempLayout.getLineCount() > maxLines) {
int lastSpace = mCloseSpannableStr.length() - 1;
if (lastSpace == -1) {
break;
}
if (originalText.length() <= lastSpace) {
mCloseSpannableStr = charSequenceToSpannable(originalText);
} else {
mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));
}
tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
if (mOpenSuffixSpan != null) {
tempText2.append(mOpenSuffixSpan);
}
tempLayout = createStaticLayout(tempText2);

}
int lastSpace = mCloseSpannableStr.length();
// - mOpenSuffixSpan.length();
// if(lastSpace >= 0 && originalText.length() > lastSpace){
// CharSequence redundantChar = originalText.subSequence(lastSpace, lastSpace + mOpenSuffixSpan.length());
// int offset = hasEnCharCount(redundantChar) - hasEnCharCount(mOpenSuffixSpan) + 1;
// lastSpace = offset <= 0 ? lastSpace : lastSpace - offset;
// mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));
// }
//计算收起的文本高度
mCLoseHeight = tempLayout.getHeight() + getPaddingTop() + getPaddingBottom();
// mCloseSpannableStr.setSpan(new ClickableSpan() {
// @Override
// public void onClick(@NonNull View widget) {
// if (mOnClickListener != null) {
// mOnClickListener.onClick(widget);
// }
// }
//
// @Override
// public void updateDrawState(@NonNull TextPaint ds) {
// super.updateDrawState(ds);
// ds.setColor(mNormalColor);
// ds.setUnderlineText(false);
// }
// }, 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
}
isClosed = mExpandable;
if (mExpandable) {
// mCloseSpannableStr.setSpan(new ClickableSpan() {
// @Override
// public void onClick(@NonNull View widget) {
// if(listener!=null){
// listener.onClick(widget);
// }
// }
//
// @Override
// public void updateDrawState(@NonNull TextPaint ds) {
// super.updateDrawState(ds);
// ds.setColor(mNormalColor);
// ds.setUnderlineText(false);
// }
// }, 0, mCloseSpannableStr.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mCloseSpannableStr.append(ELLIPSIS_STRING);
if (mOpenSuffixSpan != null) {
mCloseSpannableStr.append(mOpenSuffixSpan);
}
setMovementMethod(CustomLinkMovementMethod.getInstance());
setClickable(false);
setLongClickable(false);
setText(mCloseSpannableStr);

} else {
// mOpenSpannableStr.setSpan(new ClickableSpan() {
// @Override
// public void onClick(@NonNull View widget) {
// if (mOnClickListener != null) {
// mOnClickListener.onClick(widget);
// }
// }
//
// @Override
// public void updateDrawState(@NonNull TextPaint ds) {
// super.updateDrawState(ds);
// ds.setColor(mNormalColor);
// ds.setUnderlineText(false);
// }
// }, 0, mOpenSpannableStr.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
setText(mOpenSpannableStr);
}
}

private int hasEnCharCount(CharSequence str) {
int count = 0;
if (!TextUtils.isEmpty(str)) {
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c >= ' ' && c <= '~') {
count++;
}
}
}
return count;
}

private void switchOpenClose() {
if (mExpandable) {
isClosed = !isClosed;
if (isClosed) {
close();
} else {
open();
}
}
}

/**
* 设置是否有动画
*
* @param hasAnimation
*/
public void setHasAnimation(boolean hasAnimation) {
this.hasAnimation = hasAnimation;
}

/**
* 展开
*/
private void open() {
if (hasAnimation) {
Layout layout = createStaticLayout(mOpenSpannableStr);
mOpenHeight = layout.getHeight() + getPaddingTop() + getPaddingBottom();
executeOpenAnim();
} else {
ExpandableTextView.super.setMaxLines(Integer.MAX_VALUE);
setText(mOpenSpannableStr);
if (mOpenCloseCallback != null) {
mOpenCloseCallback.onOpen();
}
}
}

/**
* 收起
*/
private void close() {
if (hasAnimation) {
executeCloseAnim();
} else {
ExpandableTextView.super.setMaxLines(mMaxLines);
setText(mCloseSpannableStr);
if (mOpenCloseCallback != null) {
mOpenCloseCallback.onClose();
}
}
}

/**
* 执行展开动画
*/
private void executeOpenAnim() {
//创建展开动画
if (mOpenAnim == null) {
mOpenAnim = new ExpandCollapseAnimation(this, mCLoseHeight, mOpenHeight);
mOpenAnim.setFillAfter(true);
mOpenAnim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
ExpandableTextView.super.setMaxLines(Integer.MAX_VALUE);
setText(mOpenSpannableStr);
}

@Override
public void onAnimationEnd(Animation animation) {
// 动画结束后textview设置展开的状态
getLayoutParams().height = mOpenHeight;
requestLayout();
animating = false;
}

@Override
public void onAnimationRepeat(Animation animation) {

}
});
}

if (animating) {
return;
}
animating = true;
clearAnimation();
// 执行动画
startAnimation(mOpenAnim);
}

/**
* 执行收起动画
*/
private void executeCloseAnim() {
//创建收起动画
if (mCloseAnim == null) {
mCloseAnim = new ExpandCollapseAnimation(this, mOpenHeight, mCLoseHeight);
mCloseAnim.setFillAfter(true);
mCloseAnim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {

}

@Override
public void onAnimationEnd(Animation animation) {
animating = false;
ExpandableTextView.super.setMaxLines(mMaxLines);
setText(mCloseSpannableStr);
getLayoutParams().height = mCLoseHeight;
requestLayout();
}

@Override
public void onAnimationRepeat(Animation animation) {

}
});
}

if (animating) {
return;
}
animating = true;
clearAnimation();
// 执行动画
startAnimation(mCloseAnim);
}

/**
* @param spannable
* @return
*/
private Layout createStaticLayout(SpannableStringBuilder spannable) {
int contentWidth = initWidth - getPaddingLeft() - getPaddingRight();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(spannable, 0, spannable.length(), getPaint(), contentWidth);
builder.setAlignment(Layout.Alignment.ALIGN_NORMAL);
builder.setIncludePad(getIncludeFontPadding());
builder.setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier());
return builder.build();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
getLineSpacingMultiplier(), getLineSpacingExtra(), getIncludeFontPadding());
} else {
return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
getFloatField("mSpacingMult", 1f), getFloatField("mSpacingAdd", 0f), getIncludeFontPadding());
}
}

private float getFloatField(String fieldName, float defaultValue) {
float value = defaultValue;
if (TextUtils.isEmpty(fieldName)) {
return value;
}
try {
// 获取该类的所有属性值域
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
if (TextUtils.equals(fieldName, field.getName())) {
value = field.getFloat(this);
break;
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return value;
}


/**
* @param charSequence
* @return
*/
private SpannableStringBuilder charSequenceToSpannable(@NonNull CharSequence charSequence) {
SpannableStringBuilder spannableStringBuilder = null;
if (mCharSequenceToSpannableHandler != null) {
spannableStringBuilder = mCharSequenceToSpannableHandler.charSequenceToSpannable(charSequence);
}
if (spannableStringBuilder == null) {
spannableStringBuilder = new SpannableStringBuilder(charSequence);
}
return spannableStringBuilder;
}

/**
* 初始化TextView的可展示宽度
*
* @param width
*/
public void initWidth(int width) {
initWidth = width;
}

@Override
public void setMaxLines(int maxLines) {
this.mMaxLines = maxLines;
super.setMaxLines(maxLines);
}

/**
* 设置展开后缀text
*
* @param openSuffix
*/
public void setOpenSuffix(String openSuffix, IOnOpenSuffixSpanListener onOpenSuffixSpanListener) {
mOpenSuffixStr = openSuffix;
this.onOpenSuffixSpanListener = onOpenSuffixSpanListener;
updateOpenSuffixSpan();
}

public void setNewOpenSuffix(String openSuffix, IOnOpenSuffixSpanListener onOpenSuffixSpanListener) {
mOpenSuffixStr = openSuffix;
this.onOpenSuffixSpanListener = onOpenSuffixSpanListener;
updateNewOpenSuffixSpan();
}


/**
* 设置展开后缀文本颜色
*
* @param openSuffixColor
*/
public void setOpenSuffixColor(@ColorInt int openSuffixColor) {
mOpenSuffixColor = openSuffixColor;
updateOpenSuffixSpan();
}

/**
* 设置收起后缀text
*
* @param closeSuffix
*/
public void setCloseSuffix(String closeSuffix) {
mCloseSuffixStr = closeSuffix;
updateCloseSuffixSpan();
}

/**
* 设置收起后缀文本颜色
*
* @param closeSuffixColor
*/
public void setCloseSuffixColor(@ColorInt int closeSuffixColor) {
mCloseSuffixColor = closeSuffixColor;
updateCloseSuffixSpan();
}

/**
* 收起后缀是否另起一行
*
* @param closeInNewLine
*/
public void setCloseInNewLine(boolean closeInNewLine) {
mCloseInNewLine = closeInNewLine;
updateCloseSuffixSpan();
}

public interface IOnOpenSuffixSpanListener {
void onClick();
}

/**
* 更新展开后缀Spannable
*/
private void updateOpenSuffixSpan() {
if (TextUtils.isEmpty(mOpenSuffixStr)) {
mOpenSuffixSpan = null;
return;
}
mOpenSuffixSpan = new SpannableString(mOpenSuffixStr);

mOpenSuffixSpan.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
if (onOpenSuffixSpanListener != null) {
onOpenSuffixSpanListener.onClick();
} else {
switchOpenClose();
}
}

@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(mOpenSuffixColor);
ds.setUnderlineText(false);
}
}, 0, mOpenSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

private void updateNewOpenSuffixSpan() {
if (TextUtils.isEmpty(mOpenSuffixStr)) {
mOpenSuffixSpan = null;
return;
}
mOpenSuffixSpan = new SpannableString(mOpenSuffixStr);

mOpenSuffixSpan.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
if (onOpenSuffixSpanListener != null) {
onOpenSuffixSpanListener.onClick();
} else {
switchOpenClose();
}
}

@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(mOpenSuffixColor);
ds.setUnderlineText(false);
}
}, 0, mOpenSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}


/**
* 更新收起后缀Spannable
*/
private void updateCloseSuffixSpan() {
if (TextUtils.isEmpty(mCloseSuffixStr)) {
mCloseSuffixSpan = null;
return;
}
mCloseSuffixSpan = new SpannableString(mCloseSuffixStr);
mCloseSuffixSpan.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, mCloseSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (mCloseInNewLine) {
AlignmentSpan alignmentSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE);
mCloseSuffixSpan.setSpan(alignmentSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mCloseSuffixSpan.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
switchOpenClose();
}

@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(mCloseSuffixColor);
ds.setUnderlineText(false);
}
}, 1, mCloseSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

public void setContentClickListener(View.OnClickListener onClickListener) {
mOnClickListener = onClickListener;
}

public OpenAndCloseCallback mOpenCloseCallback;

public void setOpenAndCloseCallback(OpenAndCloseCallback callback) {
this.mOpenCloseCallback = callback;
}

public interface OpenAndCloseCallback {
void onOpen();

void onClose();
}

/**
* 设置文本内容处理
*
* @param handler
*/
public void setCharSequenceToSpannableHandler(CharSequenceToSpannableHandler handler) {
mCharSequenceToSpannableHandler = handler;
}

public interface CharSequenceToSpannableHandler {
@NonNull
SpannableStringBuilder charSequenceToSpannable(CharSequence charSequence);
}

class ExpandCollapseAnimation extends Animation {
private final View mTargetView;//动画执行view
private final int mStartHeight;//动画执行的开始高度
private final int mEndHeight;//动画结束后的高度

ExpandCollapseAnimation(View target, int startHeight, int endHeight) {
mTargetView = target;
mStartHeight = startHeight;
mEndHeight = endHeight;
setDuration(400);
}

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
mTargetView.setScrollY(0);
//计算出每次应该显示的高度,改变执行view的高度,实现动画
mTargetView.getLayoutParams().height = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight);
mTargetView.requestLayout();
}
}

}

控件使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
val tvContent = holder.getView<ExpandableTextView>(R.id.tv_content)
tvContent.movementMethod = LinkMovementMethod.getInstance()
tvContent.isClickable = false
tvContent.isLongClickable = false
val builder = SpannableStringBuilder()
if (item.spoiler && spoilerEnable) builder.append(" ")
if (item.talkList != null && item.talkList.size > 0) {
val talk = item.talkList[0]
val startIndex = builder.length
builder.append("#")
builder.append(talk.name)
builder.append(" ")
val clickableSpan = object : ClickableSpan() {
override fun onClick(view: View) {
talkClickListener?.invoke(item, holder.adapterPosition)
}

override fun updateDrawState(ds: TextPaint) {
ds.color =
if (talk.enable) Color.parseColor("#1890FF")
else Color.parseColor("#85888F")
}
}
builder.setSpan(
clickableSpan,
startIndex,
builder.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
builder.append(it)
tvContent.initWidth(UiUtils.getScreenWidth() - UiUtils.dip2px(16 * 2 + 36 + 9))
tvContent.setOpenSuffix("查看全文") {
listener.invoke(item, null)
}
tvContent.setOriginalText(builder)

SettingsHelper

SettingsHelper

SettingsHelper 自启动设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log

/**
* 跳转自启动页面
*/
object AutoStartHelper {
private val hashMap = mutableMapOf<String, List<String>>().apply {
put(
"Xiaomi", listOf(
"com.miui.securitycenter/com.miui.permcenter.autostart.AutoStartManagementActivity",
"com.miui.securitycenter"
)
)
put(
"samsung", listOf(
"com.samsung.android.sm_cn/com.samsung.android.sm.ui.ram.AutoRunActivity",
"com.samsung.android.sm_cn/com.samsung.android.sm.ui.appmanagement.AppManagementActivity",
"com.samsung.android.sm_cn/com.samsung.android.sm.ui.cstyleboard.SmartManagerDashBoardActivity",
"com.samsung.android.sm_cn/.ui.ram.RamActivity",
"com.samsung.android.sm_cn/.app.dashboard.SmartManagerDashBoardActivity",
"com.samsung.android.sm/com.samsung.android.sm.ui.ram.AutoRunActivity",
"com.samsung.android.sm/com.samsung.android.sm.ui.appmanagement.AppManagementActivity",
"com.samsung.android.sm/com.samsung.android.sm.ui.cstyleboard.SmartManagerDashBoardActivity",
"com.samsung.android.sm/.ui.ram.RamActivity",
"com.samsung.android.sm/.app.dashboard.SmartManagerDashBoardActivity",
"com.samsung.android.lool/com.samsung.android.sm.ui.battery.BatteryActivity",
"com.samsung.android.sm_cn",
"com.samsung.android.sm"
)
)
put(
"HUAWEI", listOf(
"com.huawei.systemmanager/.startupmgr.ui.StartupNormalAppListActivity",
"com.huawei.systemmanager/.appcontrol.activity.StartupAppControlActivity",
"com.huawei.systemmanager/.optimize.process.ProtectActivity",
"com.huawei.systemmanager/.optimize.bootstart.BootStartActivity",
// "com.huawei.systemmanager/com.huawei.permissionmanager.ui.MainActivity", // 这个是隐私-权限管理,但是没有自启动权限!!!
"com.android.settings/com.android.settings.Settings$" + "AppAndNotificationDashboardActivity", // 鸿蒙系统,应用和服务,列表中有应用启动管理
"com.huawei.systemmanager"
)
)
put(
"vivo", listOf(
"com.iqoo.secure/.ui.phoneoptimize.BgStartUpManager",
"com.vivo.permissionmanager/.activity.BgStartUpManagerActivity",
"com.vivo.permissionmanager/.activity.SoftPermissionDetailActivity",
"com.iqoo.secure/.safeguard.PurviewTabActivity",
"com.iqoo.secure",
"com.vivo.permissionmanager"
)
)
put(
"Meizu", listOf(
"com.meizu.safe/.permission.SmartBGActivity",
"com.meizu.safe/.permission.PermissionMainActivity",
"com.meizu.safe"
)
)
put(
"OPPO", listOf(
"com.coloros.safecenter/.startupapp.StartupAppListActivity",
"com.coloros.safecenter/.permission.startup.StartupAppListActivity",
"com.oppo.safe/.permission.startup.StartupAppListActivity",
"com.coloros.oppoguardelf/com.coloros.powermanager.fuelgaue.PowerUsageModelActivity",
"com.coloros.safecenter/com.coloros.privacypermissionsentry.PermissionTopActivity",
"com.coloros.safecenter",
"com.oppo.safe",
"com.coloros.oppoguardelf"
)
)
put(
"oneplus", listOf(
"com.oneplus.security/.chainlaunch.view.ChainLaunchAppListActivity",
"com.oneplus.security"
)
)
put(
"letv", listOf(
"com.letv.android.letvsafe/.AutobootManageActivity",
"com.letv.android.letvsafe/.BackgroundAppManageActivity",
"com.letv.android.letvsafe"
)
)
put(
"zte", listOf(
"com.zte.heartyservice/.autorun.AppAutoRunManager",
"com.zte.heartyservice"
)
)
//金立
put(
"F", listOf(
"com.gionee.softmanager/.MainActivity",
"com.gionee.softmanager"
)
)
//以下为未确定(厂商名也不确定)
put(
"smartisanos", listOf(
"com.smartisanos.security/.invokeHistory.InvokeHistoryActivity",
"com.smartisanos.security"
)
)
//360
put(
"360", listOf(
"com.yulong.android.coolsafe/.ui.activity.autorun.AutoRunListActivity",
"com.yulong.android.coolsafe"
)
)
//360
put(
"ulong", listOf(
"com.yulong.android.coolsafe/.ui.activity.autorun.AutoRunListActivity",
"com.yulong.android.coolsafe"
)
)
//酷派
put(
"coolpad" /*厂商名称不确定是否正确*/, listOf(
"com.yulong.android.security/com.yulong.android.seccenter.tabbarmain",
"com.yulong.android.security"
)
)
//联想
put(
"lenovo" /*厂商名称不确定是否正确*/, listOf(
"com.lenovo.security/.purebackground.PureBackgroundActivity",
"com.lenovo.security"
)
)
put(
"htc" /*厂商名称不确定是否正确*/, listOf(
"com.htc.pitroad/.landingpage.activity.LandingPageActivity",
"com.htc.pitroad"
)
)
//华硕
put(
"asus" /*厂商名称不确定是否正确*/, listOf(
"com.asus.mobilemanager/.MainActivity",
"com.asus.mobilemanager"
)
)
//酷派
put(
"YuLong", listOf(
"com.yulong.android.softmanager/.SpeedupActivity",
"com.yulong.android.security/com.yulong.android.seccenter.tabbarmain",
"com.yulong.android.security"
)
)
}

fun startAutoBootSetting(context: Context?) {
Log.e("AutoStartHelper", "当前手机型号为:" + Build.MANUFACTURER)
var result = false

run start0@{
for ((manufacturer, componentNameList) in hashMap) {
if (Build.MANUFACTURER.equals(manufacturer, ignoreCase = true)) {
for (actName in componentNameList) {
try {
var intent: Intent? = null
if (actName.contains("/")) {
intent = Intent()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.component = ComponentName.unflattenFromString(actName)
if (actName.contains("SoftPermissionDetailActivity")) {
intent.putExtra("packagename", context?.packageName)
}
}
// else {
// // 跳转到对应的安全管家/安全中心
// intent = context?.packageManager?.getLaunchIntentForPackage(actName)
// }
intent?.let {
context?.startActivity(intent)
result = true
return@start0
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}

if (!result) {
try {
// 跳转到app详情设置
val intent = Intent()
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.data = Uri.fromParts("package", context?.packageName, null)
context?.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

SpannableString 之居中显示 ImageSpan

自定义布局:SpannableString 之居中显示 ImageSpan

image-20220104185114603
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CenteredImageSpan(context: Context, drawableRes: Int) : ImageSpan(context, drawableRes) {
override fun draw(
canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
// image to draw
val b = drawable
// font metrics of text to be replaced
val fm = paint.fontMetricsInt
var transY = ((y + fm.descent + y + fm.ascent) / 2 - b.bounds.bottom / 2)
// to check the last line.(当 image 在单独一行显示时可能会存在这个问题)
if (transY > bottom - b.bounds.bottom) transY = bottom - b.bounds.bottom
canvas.save()
canvas.translate(x, transY.toFloat())
b.draw(canvas)
canvas.restore()
}
}
1
2
3
4
5
6
7
8
9
10
11
val spanStr = SpannableStringBuilder()
spanStr.append("# ")
spanStr.append(title)
val imageSpan = CenteredImageSpan(this, R.mipmap.ic_topic_detail_jinghao_black)
spanStr.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// corner
spanStr.append(" #")
val len = spanStr.length
val cornerSpan = CenteredImageSpan(this, R.mipmap.ic_topic_detail_remen)
spanStr.setSpan(cornerSpan, len - 1, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
binding.ctTalkDetailInfo.talkNameTv.text = spanStr

Kotlin单例

Kotlin单例

下面介绍一下kotlin 线程安全的几种单例写法。

0x01 饿汉模式

1
2
3
// Kotlin实现
object Singleton {
}
1
2
3
4
5
6
7
8
9
10
11
12
// 反编译Kotlin实现的Java代码
public final class Singleton {
public static final Singleton INSTANCE;

private Singleton() {
}

static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}

0x02 双重校验锁式

双重校验锁式(Double Check)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Kotlin实现
class Singleton private constructor() {
companion object {
@Volatile
private var instance: Singleton? = null

@JvmStatic
fun getInstance(): Singleton {
if (instance == null) {
synchronized(Singleton::class.java) {
if (instance == null) {
instance = Singleton()
}
}
}
return instance!!
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 反编译Kotlin实现的Java代码
public final class Singleton {
private static volatile Singleton instance;
public static final Singleton.Companion Companion = new Singleton.Companion((DefaultConstructorMarker)null);

private Singleton() {
}

// $FF: synthetic method
public Singleton(DefaultConstructorMarker $constructor_marker) {
this();
}

@JvmStatic
@NotNull
public static final Singleton getInstance() {
return Companion.getInstance();
}

public static final class Companion {
@JvmStatic
@NotNull
public final Singleton getInstance() {
if (Singleton.instance == null) {
Class var1 = Singleton.class;
boolean var2 = false;
boolean var3 = false;
synchronized(var1) {
int var4 = false;
if (Singleton.instance == null) {
Singleton.instance = new Singleton((DefaultConstructorMarker)null);
}

Unit var6 = Unit.INSTANCE;
}
}

Singleton var10000 = Singleton.instance;
if (var10000 == null) {
Intrinsics.throwNpe();
}

return var10000;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

0x03 静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
//Java实现
public class SingletonDemo {
private static class SingletonHolder{
private static SingletonDemo instance=new SingletonDemo();
}
private SingletonDemo(){
System.out.println("Singleton has loaded");
}
public static SingletonDemo getInstance(){
return SingletonHolder.instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
// Kotlin实现
class Singleton private constructor() {
companion object {
@JvmStatic
fun getInstance() = SingletonHolder.instance
}
private object SingletonHolder {
@JvmStatic
val instance: Singleton = Singleton()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 反编译Kotlin实现的Java代码
public final class Singleton {
public static final Singleton.Companion Companion = new Singleton.Companion((DefaultConstructorMarker)null);
private Singleton() {
}
@JvmStatic
@NotNull
public static final Singleton getInstance() {
return Companion.getInstance();
}
private static final class SingletonHolder {
@NotNull
private static final Singleton instance;
public static final Singleton.SingletonHolder INSTANCE;
@NotNull
public static final Singleton getInstance() {
return instance;
}
static {
Singleton.SingletonHolder var0 = new Singleton.SingletonHolder();
INSTANCE = var0;
instance = new Singleton((DefaultConstructorMarker)null);
}
}
public static final class Companion {
@JvmStatic
@NotNull
public final Singleton getInstance() {
return Singleton.SingletonHolder.getInstance();
}
private Companion() {
}
}
}

Android专栏-JavaCrash默认处理

Java Crash 默认处理

CrashHandler 处理 Java 异常流程:

  • 区分Debug模式和Release模式、主进程和子进程、主线程和子线程来处理
  • 捕获Activity的生命周期内异常,并主动杀死Activity
  • View绘制流程异常捕获
  • 自定义上报

CrashHandler 源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.util.Log
import com.alibaba.ha.adapter.AliHaAdapter
import com.rrtv.action.manager.ThreadPoolManager
import com.rrtv.utils.utils.UiUtils
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.lang.reflect.Field
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*


/**
* Java Crash 默认处理
*/
class CrashHandler : Thread.UncaughtExceptionHandler {

private var isDebug = false
private var isMainProcess = false

private var sActivityKiller: IActivityKiller? = null

companion object {
private const val TAG = "CrashHandler"
private var instance = CrashHandler()

@Volatile
@JvmStatic
private var hasInit = false


/**
* 初始化
*
* 在 @link Application#onCreate() 方法中 install()
*
* @param isDebug 是否debug模式 BuildConfig.DEBUG
* @param isMainProcess 是否是主进程 android.os.Process.myPid()
*/
@JvmStatic
fun install(isDebug: Boolean, isMainProcess: Boolean) {
if (!hasInit) {
synchronized(CrashHandler::class.java) {
if (!hasInit) {
Log.d(TAG, "install: isDebug = $isDebug, mainPid = $isMainProcess")
hasInit = true
instance.setup(isDebug, isMainProcess)
Log.d(TAG, "install success.")
}
}
}
}
}


private fun setup(isDebug: Boolean, isMainProcess: Boolean) {
Log.d(TAG, "setup:")
this.isDebug = isDebug
this.isMainProcess = isMainProcess

Thread.setDefaultUncaughtExceptionHandler(this)

if (!isDebug && isMainProcess) {
// release 模式防止主线程奔溃
Handler(Looper.getMainLooper()).post {
while (true) {
try {
Looper.loop()
} catch (e: Exception) {
handleLooperException(e)
}
}
}

try {
// 生命周期的 ActivityKiller
initActivityKiller()
} catch (e: Exception) {
Log.e(TAG, "拦截生命周期失败", e)
}
}
}

/**
* 替换ActivityThread.mH.mCallback,实现拦截Activity生命周期,直接忽略生命周期的异常的话会导致黑屏,目前
* 会调用ActivityManager的finishActivity结束掉生命周期抛出异常的Activity
*/
private fun initActivityKiller() {
Log.d(TAG, "initActivityKiller: Build.VERSION.SDK_INT=${Build.VERSION.SDK_INT}")
//各版本android的ActivityManager获取方式,finishActivity的参数,token(binder对象)的获取不一样
if (Build.VERSION.SDK_INT >= 28) {
sActivityKiller = ActivityKillerV28()
} else if (Build.VERSION.SDK_INT >= 26) {
sActivityKiller = ActivityKillerV26()
} else if (Build.VERSION.SDK_INT == 25 || Build.VERSION.SDK_INT == 24) {
sActivityKiller = ActivityKillerV24_V25()
} else if (Build.VERSION.SDK_INT in 21..23) {
sActivityKiller = ActivityKillerV21_V23()
} else if (Build.VERSION.SDK_INT in 15..20) {
sActivityKiller = ActivityKillerV15_V20()
} else if (Build.VERSION.SDK_INT < 15) {
sActivityKiller = ActivityKillerV15_V20()
}
try {
hookMH()
Log.e(TAG, "hookMH Success.")
} catch (e: Throwable) {
Log.e(TAG, "hookMH 失败: ", e)
}
}

@Throws(Exception::class)
private fun hookMH() {
Log.d(TAG, "hookMH: ")
val LAUNCH_ACTIVITY = 100
val PAUSE_ACTIVITY = 101
val PAUSE_ACTIVITY_FINISHING = 102
val STOP_ACTIVITY_HIDE = 104
val RESUME_ACTIVITY = 107
val DESTROY_ACTIVITY = 109
val NEW_INTENT = 112
val RELAUNCH_ACTIVITY = 126
val activityThreadClass =
Class.forName("android.app.ActivityThread")
val activityThread =
activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null)
val mhField: Field = activityThreadClass.getDeclaredField("mH")
mhField.setAccessible(true)
val mhHandler = mhField.get(activityThread) as Handler
val callbackField: Field = Handler::class.java.getDeclaredField("mCallback")
callbackField.setAccessible(true)
callbackField.set(mhHandler, Handler.Callback { msg ->
if (Build.VERSION.SDK_INT >= 28) { //android P 生命周期全部走这
val EXECUTE_TRANSACTION = 159
if (msg.what === EXECUTE_TRANSACTION) {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishLaunchActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
return@Callback false
}
when (msg.what) {
LAUNCH_ACTIVITY -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishLaunchActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
RESUME_ACTIVITY -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishResumeActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
PAUSE_ACTIVITY_FINISHING -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishPauseActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
PAUSE_ACTIVITY -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishPauseActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
STOP_ACTIVITY_HIDE -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishStopActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
DESTROY_ACTIVITY -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
handleLifecycleException(throwable)
}
return@Callback true
}
}
false
})
}

/**
* 生命周期异常处理
*/
private fun handleLifecycleException(e: Throwable) {
Log.e(TAG, "lifecycleException: ", e)
reportCustomThrowable(Thread.currentThread(), RuntimeException("Activity生命周期出现异常", e))
// 给个Toast提示
try {
UiUtils.showToastSafe("小猿刚刚捕获了一只BUG")
} catch (e: Exception) {
e.printStackTrace()
}
}

/**
* 主线程Looper异常
*/
private fun handleLooperException(e: Exception) {
// 主线程内发生异常 主动 catch
Log.e(TAG, "handleLooperException: ", e)
reportCustomThrowable(Thread.currentThread(), e)
handleMainThreadException(e)
}


/**
* 本版本不处理
*
* view measure layout draw时抛出异常会导致Choreographer挂掉
* 建议直接杀死app。以后的版本会只关闭黑屏的Activity
*
* @param e
*/
private fun isChoreographerException(e: Throwable?): Boolean {
val elements = e?.stackTrace ?: return false
for (i in elements.size - 1 downTo -1 + 1) {
if (elements.size - i > 20) {
return false
}
val element = elements[i]
if ("android.view.Choreographer" == element.className && "Choreographer.java" == element.fileName && "doFrame" == element.methodName) {
//View 绘制流程出的问题
return true
}
}
return false
}


override fun uncaughtException(t: Thread, e: Throwable) {
Log.e(TAG, "uncaughtException: ")
if (isDebug) {
handleDebugException(t, e)
} else {
handleReleaseException(t, e)
}
}

/**
* 处理 Debug 模式的异常
*/
private fun handleDebugException(t: Thread, e: Throwable) {
Log.e(TAG, "handleDebugException: $t", e)
// 记录本地日志
saveThrowableMessage(Log.getStackTraceString(e))
}

/**
* 处理 !Debug 模式的异常
*/
private fun handleReleaseException(t: Thread, e: Throwable) {
Log.e(TAG, "handleReleaseException: $t", e)
// 自定义 Bug 上报
reportCustomThrowable(t, e)
// 根据情况来处理异常
if (isMainProcess) {
// 为主进程
if (Looper.myLooper() == Looper.getMainLooper()) {
// 主线程异常处理
handleMainThreadException(e)
} else {
// 非主线程
Log.e(TAG, "子线程异常: finish.")
}
} else {
// 如果是子进程发生异常 直接殺掉子進程
Log.e(TAG, "子进程异常: killProcess.")
android.os.Process.killProcess(android.os.Process.myPid())
}

}

/**
* 主线程异常处理
*
* Looper.loop & MainThreadUncaughtException
*/
private fun handleMainThreadException(e: Throwable) {
Log.e(TAG, "handleMainThreadException: ")
try {
// 主线程
when (e) {
is IllegalArgumentException,
is IllegalStateException,
is IndexOutOfBoundsException,
is UnsupportedOperationException,
is ArithmeticException,
is NumberFormatException,
is NullPointerException,
is ClassCastException,
is AssertionError,
is NoSuchElementException -> {
Log.e(TAG, "主线程异常: handle finish.")
if (isChoreographerException(e)) {
UiUtils.showToastSafe("界面刷新出了个小问题")
}
}
else -> {
Log.e(TAG, "主线程未知异常:System exit.")
// 这里也可以只给个提示,反正不会程序不会奔溃
android.os.Process.killProcess(android.os.Process.myPid())
System.exit(0)
}
}
} catch (e: Exception) {
e.printStackTrace()
}

}

private val logFilePath = Environment.getExternalStorageDirectory().toString() +
File.separator + "Example" +
File.separator + "CrashLog"

private fun saveThrowableMessage(errorMessage: String) {
if (TextUtils.isEmpty(errorMessage)) {
return
}
val file = File(logFilePath)
if (file.exists() && file.isDirectory) {
// fall through
} else {
file.mkdirs()
}
writeToFile(errorMessage, file)
}

private val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA)

private fun writeToFile(errorMessage: String, file: File) {
ThreadPoolManager.getShortPool()?.execute {
var outputStream: FileOutputStream? = null
try {
val timestamp = System.currentTimeMillis()
val time = formatter.format(Date())
val fileName = "crash-$time-$timestamp"
val inputStream = ByteArrayInputStream(errorMessage.toByteArray())
outputStream = FileOutputStream(File(file, "$fileName.txt"))
var len: Int
val bytes = ByteArray(1024)
while (inputStream.read(bytes).also { len = it } != -1) {
outputStream.write(bytes, 0, len)
}
outputStream.flush()
Log.e(TAG, "异常奔溃日志成功写入本地文件:${file.absolutePath}")
} catch (e: Exception) {
Log.e(TAG, "异常奔溃日志写入本地文件失败: ", e)
} finally {
if (outputStream != null) {
try {
outputStream.close()
} catch (e: IOException) {
// nothing
}
}
}
}
}

/**
* 自定义 Bug 上报
*/
private fun reportCustomThrowable(t: Thread, e: Throwable) {
Log.e(TAG, "reportCustomThrowable: ")
try {
AliHaAdapter.getInstance().reportCustomError(e) //配置项:自定义错误
} catch (ex: Exception) {
Log.e(TAG, "上报自定义异常Error: ", ex)
}
}
}

使用方式:

ApplicationonCreate() 方法中, 调用 RrCrashHandler.install(BuildConfig.DEBUG, AppUtils.isMainProgress(this))

参考链接:

https://github.com/android-notes/Cockroach

https://github.com/android-notes/Cockroach/blob/master/%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md

Android专栏-SmartRefreshLayout

Android专栏-SmartRefreshLayout

0x01 加载结束之后底部多出一段空白位置

SmartRefreshLayout 嵌套 ViewPager2 上拉加载更多,在 finishLoadMore() 方法之后,底部加载 Loading 位置会多出一段空白不消失。

解决方案:

1
smartRefreshLayout.setEnableScrollContentWhenLoaded(false)

0x02 下拉刷新+PAG动画

自定义下拉刷新头部,使用 PAGView 做动画,可以在 onMoving(boolean b, float v, int i, int i1, int i2) 方法中设置 pagView.setProgress(v); 添加手势动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;

import com.scwang.smartrefresh.layout.api.RefreshHeader;
import com.scwang.smartrefresh.layout.api.RefreshKernel;
import com.scwang.smartrefresh.layout.api.RefreshLayout;
import com.scwang.smartrefresh.layout.constant.RefreshState;
import com.scwang.smartrefresh.layout.constant.SpinnerStyle;

import org.libpag.PAGFile;
import org.libpag.PAGView;

public class CommonRefreshHeader extends RelativeLayout implements RefreshHeader {
public static final String DEFAULT_LOADING_FILE = "load_bubble.pag";

protected View mView;
protected ImageView sdv_background;
private int mFinishDuration = 300;
private ViewGroup rootLayout;
private PAGView pagView;
private TextView toastTv;

public CommonRefreshHeader(Context context) {
super(context);
initView(context);
}

public CommonRefreshHeader(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}

public CommonRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}

public int getLayoutId() {
return R.layout.layout_refresh_head;
}

protected void initView(Context context) {
mView = LayoutInflater.from(context).inflate(getLayoutId(), this);
toastTv = mView.findViewById(R.id.tv_toast);
rootLayout = mView.findViewById(R.id.rootLayout);
sdv_background = mView.findViewById(R.id.sdv_background);
PAGFile file = PAGFile.Load(context.getAssets(), DEFAULT_LOADING_FILE);
pagView = new PAGView(getContext());
LayoutParams params = new LayoutParams(UiUtils.dip2px(39), UiUtils.dip2px(39));
params.addRule(RelativeLayout.CENTER_IN_PARENT);
pagView.setLayoutParams(params);
pagView.setFile(file);
pagView.setRepeatCount(0);
rootLayout.addView(pagView);
}

public void setMarginTop(int marginTop) {
if (mView == null) return;
ViewGroup root = mView.findViewById(R.id.rootLayout);
if (root != null && root.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams params = (MarginLayoutParams) root.getLayoutParams();
params.topMargin = marginTop;
root.setLayoutParams(params);
}
}

@NonNull
@Override
public View getView() {
return mView;
}

@Override
public SpinnerStyle getSpinnerStyle() {
return SpinnerStyle.Translate;
}

@Override
public void setPrimaryColors(@ColorInt int... colors) {

}

@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {

}

@Override
public void onMoving(boolean b, float v, int i, int i1, int i2) {
/**
* 【仅限框架内调用】手指拖动下拉(会连续多次调用,添加isDragging并取代之前的onPulling、onReleasing)
* @param isDragging true 手指正在拖动 false 回弹动画
* @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+maxDragHeight) / footerHeight )
* @param offset 下拉的像素偏移量 0 - offset - (footerHeight+maxDragHeight)
* @param height 高度 HeaderHeight or FooterHeight (offset 可以超过 height 此时 percent 大于 1)
* @param maxDragHeight 最大拖动高度 offset 可以超过 height 参数 但是不会超过 maxDragHeight
*/
if (pagView != null && !pagView.isPlaying()) {
pagView.setProgress(v);
pagView.flush();
}
}

@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int i, int i1) {

}

@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {

}

@Override
public void onStartAnimator(RefreshLayout layout, int height, int extendHeight) {
LogUtils.e("RefreshHeader", "onStartAnimator");
if (pagView != null) pagView.play();
}

@Override
public int onFinish(RefreshLayout layout, boolean success) {
LogUtils.e("RefreshHeader", "onFinish");
if (pagView != null) pagView.stop();
return mFinishDuration;//延迟500毫秒之后再弹回
}

@Override
public boolean isSupportHorizontalDrag() {
return false;
}

@Override
public void onStateChanged(RefreshLayout refreshLayout, RefreshState oldState, RefreshState newState) {
switch (newState) {
case None:
case PullDownToRefresh:
case Refreshing:
if (pagView != null) pagView.setVisibility(VISIBLE);
if (toastTv != null) toastTv.setVisibility(GONE);
break;
case ReleaseToRefresh:
break;
case RefreshFinish:
if (pagView != null) pagView.setVisibility(GONE);
if (toastTv != null) toastTv.setVisibility(VISIBLE);
break;
}
}

public void setToastText(String str) {
if (StringUtils.isEmpty(str)) return;
if (toastTv != null) {
toastTv.setText(str);
}
}

public void setFinishDuration(int finishDuration) {
this.mFinishDuration = finishDuration;
}
}

Gson 数据解析

Gson 数据解析

0x01 Kotlin Gson 解析 data class 两条黄金法则:

1、 String 必须是可空类型 String?

2、 需要使用默认值,则全部字段都必须给予默认值,以满足kotlin对象有空的构造函数

0x02 手动解析Gson基础字段

1、msg 可空String解析 jsonReader.peek() == JsonToken.NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import okhttp3.ResponseBody
import retrofit2.Converter
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

class ResponseBodyConverter(val gson: Gson, val type: Type) :
Converter<ResponseBody, BaseResponse<Any>> {
override fun convert(value: ResponseBody): BaseResponse<Any> {
val dataType = GenericsUtils.getParameterUpperBound(
0,
type as ParameterizedType
)
val baseResponse = BaseResponse<Any>()
val jsonReader = JsonReader(value.charStream())
try {
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val name: String = jsonReader.nextName()
when (name) {
"code" -> {
baseResponse.code = jsonReader.nextString()
}
"msg" -> {
// this works, but do not do this.
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull()
baseResponse.msg = null
} else {
baseResponse.msg = jsonReader.nextString()
}
}
"data" -> {
val mapped: TypeAdapter<*>? = gson.getAdapter(TypeToken.get(dataType))
baseResponse.data = mapped?.read(jsonReader)
}
else -> {
jsonReader.skipValue()
}
}
}
} catch (e: IllegalStateException) {
throw JsonSyntaxException(e)
} catch (e: IllegalAccessException) {
throw AssertionError(e)
}
jsonReader.endObject()
return baseResponse
}
}

大量文本的浏览进度和浏览时长统计

大量文本的浏览进度和浏览时长统计

埋点需求,Android App 需要在onResume 和 onPause 方法中计算浏览的时长,同时上报浏览的进度。

浏览进度Rate具体的计算方式的具体过程:

1、在滚动屏幕过程中,通过 textContent?.viewTreeObserver?.addOnScrollChangedListener 来记录屏幕滚动的位置

2、在滚动监听里通过textContent?.getLocationOnScreen(location)获取在屏幕的具体位置,同时,

计算出visibleHeight = screenHeight - location[1] 当前文本的可见高度

3、当可见高度超过目标的高度,则认为已经全部浏览,rate = 100% ,同时移除滚动监听textContent?.viewTreeObserver?.removeOnScrollChangedListener(mScrollChangeListener)

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private var totalHeight: Int = 0
private var visibleHeight: Int = 0
private var screenHeight = 0

fun getViewRate(): String? =
if (totalHeight <= 0 || visibleHeight < 0) null
else "${(visibleHeight * 100 / totalHeight)}%"

private fun bindViewTreeObserver() {
cleanCache()
contentTv.post {
totalHeight = contentTv.height
screenHeight = ScreenUtils.getScreenHeight(context)
contentVisibleRate()
}
contentTv.viewTreeObserver.addOnScrollChangedListener(mScrollChangeListener)
}

private fun cleanCache() {
totalHeight = 0
screenHeight = 0
visibleHeight = 0
}

private val mScrollChangeListener = ViewTreeObserver.OnScrollChangedListener {
contentVisibleRate()
}

private fun contentVisibleRate() {
val location = IntArray(2)
contentTv.getLocationOnScreen(location)
visibleHeight = (screenHeight - location[1]).coerceAtLeast(visibleHeight)
if (visibleHeight >= totalHeight) {
visibleHeight = totalHeight
removeOnScrollChangedListener()
}
}

private fun removeOnScrollChangedListener() {
mScrollChangeListener?.let {
contentTv.viewTreeObserver.removeOnScrollChangedListener(mScrollChangeListener)
}
}

RecyclerViewHelper

RecyclerViewHelper

提供了注册加载更多,和判断是否不足一屏等工具方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager

object RecyclerViewHelper {
/**
* RecyclerView 注册加载更多监听
*/
@JvmStatic
fun addOnScrollListener(recyclerView: RecyclerView, loadMore: () -> Unit) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
val count = layoutManager.itemCount
if (lastPosition >= count - 2) {
loadMore()
return@onScrollStateChanged
}
} else if (layoutManager is StaggeredGridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val result = IntArray(spanCount)
layoutManager.findLastCompletelyVisibleItemPositions(result)
for (it in result) {
if (it >= count - spanCount - 1) {
loadMore()
return@onScrollStateChanged
}
}
} else if (layoutManager is GridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
if (lastPosition >= count - spanCount - 1) {
loadMore()
return@onScrollStateChanged
}

}
}
}
})
}


/**
* 判断是否一屏显示
*
* 错误或者空了返回 false
*/
@JvmStatic
fun isOneScreen(recyclerView: RecyclerView?): Boolean {
recyclerView?.let {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
// include GridLayoutManager
val count = layoutManager.itemCount
return count > 0 &&
layoutManager.findFirstCompletelyVisibleItemPosition() == 0 &&
layoutManager.findLastCompletelyVisibleItemPosition() == count - 1
} else if (layoutManager is StaggeredGridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val last = IntArray(spanCount)
val first = IntArray(spanCount)
layoutManager.findLastCompletelyVisibleItemPositions(last)
layoutManager.findFirstCompletelyVisibleItemPositions(first)
return count > 0 && first.min() == 0 && last.max() == count - 1
}
}

return false
}

private fun IntArray.min(): Int {
if (this.isNotEmpty()) {
var result = this[0]
this.forEach {
if (result > it) result = it
}
return result
}
return -1
}

private fun IntArray.max(): Int {
if (this.isNotEmpty()) {
var result = this[0]
this.forEach {
if (result < it) result = it
}
return result
}
return -1
}
}

自定义布局:西部世界 第一季

自定义布局:西部世界 第一季

这个自定义布局要求显示为 系列名称... + 第一季 ,后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* 系列名称... + 第一季
* 后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示
*/
class FixedEndLinearLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//获取父布局测量size和model
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
if (childCount != 2) throw RuntimeException("FixedEndLinearLayout must have 2 children.")
val wrapChild = getChildAt(0)
val fixedChild = getChildAt(1)

//测量
measureChild(fixedChild, widthMeasureSpec, heightMeasureSpec)
val fixedParams = fixedChild.layoutParams as MarginLayoutParams

val fixedChildWidth =
fixedChild.measuredWidth + fixedParams.leftMargin + fixedParams.rightMargin
val fixedChildHeight =
fixedChild.measuredHeight + fixedParams.topMargin + fixedParams.bottomMargin

val wrapChildWidthSpec = ViewGroup.getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight + fixedChildWidth, wrapChild.layoutParams.width
)
val wrapChildHeightSpec = ViewGroup.getChildMeasureSpec(
heightMeasureSpec, paddingTop + paddingBottom, wrapChild.layoutParams.height
)
wrapChild.measure(wrapChildWidthSpec, wrapChildHeightSpec)

val wrapParams = wrapChild.layoutParams as MarginLayoutParams

val wrapChildWidth =
wrapChild.measuredWidth + wrapParams.leftMargin + wrapParams.rightMargin
val wrapChildHeight =
wrapChild.measuredHeight + wrapParams.topMargin + wrapParams.bottomMargin

val width = wrapChildWidth + fixedChildWidth
val height = fixedChildHeight.coerceAtLeast(wrapChildHeight)

start0 = paddingLeft + wrapParams.leftMargin
start1 = paddingLeft + wrapChildWidth + fixedParams.leftMargin

setMeasuredDimension(
if (widthMode == MeasureSpec.EXACTLY) widthSize else width + paddingLeft + paddingRight,
if (heightMode == MeasureSpec.EXACTLY) heightSize else height + paddingTop + paddingBottom
)

}

private var start0 = 0
private var start1 = 0

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val wrapChild = getChildAt(0)
val fixedChild = getChildAt(1)
val y0 = (measuredHeight - wrapChild.measuredHeight) / 2
val y1 = (measuredHeight - fixedChild.measuredHeight) / 2
wrapChild.layout(
start0,
y0,
start0 + wrapChild.measuredWidth,
y0 + wrapChild.measuredHeight
)
fixedChild.layout(
start1,
y1,
start1 + fixedChild.measuredWidth,
y1 + fixedChild.measuredHeight
)
}
}

Android专栏-BaseQuickAdapterHelper

Android专栏-BaseQuickAdapterHelper

0x01 自动加载更多-LoadingFooterView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import android.view.View
import android.view.ViewGroup
import com.chad.library.adapter.base.loadmore.BaseLoadMoreView
import com.chad.library.adapter.base.loadmore.LoadMoreStatus
import com.chad.library.adapter.base.util.getItemView
import com.chad.library.adapter.base.viewholder.BaseViewHolder

class LoadingFooterView : BaseLoadMoreView() {
private var loadingView: LoadingPagView? = null
override fun getRootView(parent: ViewGroup): View {
val rootView = parent.getItemView(R.layout.ui_footer_adapter_load_more)
loadingView = rootView.findViewById(R.id.loadingView)
return rootView
}

override fun getLoadingView(holder: BaseViewHolder): View {
return holder.getView(R.id.loadingView)
}

override fun getLoadComplete(holder: BaseViewHolder): View {
return holder.getView(R.id.fakeView)
}

override fun getLoadEndView(holder: BaseViewHolder): View {
return holder.getView(R.id.endView)
}

override fun getLoadFailView(holder: BaseViewHolder): View {
return holder.getView(R.id.fakeView)
}

override fun convert(holder: BaseViewHolder, position: Int, loadMoreStatus: LoadMoreStatus) {
super.convert(holder, position, loadMoreStatus)
when (loadMoreStatus) {
LoadMoreStatus.Complete -> {
loadingView?.stopPlay()
}
LoadMoreStatus.Loading -> {
loadingView?.startPlay()
}
LoadMoreStatus.Fail -> {

}
LoadMoreStatus.End -> {
loadingView?.stopPlay()
}
}
}
}

VolumeDialog音量控制自定义

VolumeDialog音量控制自定义

1 使用自定义 AlertDialog 实现

2 window?.setFlags 设置 dialog 的样式

3 window?.attributes 设置 dialog 的位置

4 返回键监听,兼容机型需要使用 setOnKeyListener

5 按一次音量键回调多次的问题,KeyEvent.action 事件分 KeyEvent.ACTION_UP 和 KeyEvent.ACTION_DOWN

6 音量加减需要获取系统音量 max 值来手动控制,不同手机 max 值域不同

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import android.app.Activity
import android.content.Context
import android.content.res.Resources
import android.media.AudioManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.TypedValue
import android.view.Gravity
import android.view.KeyEvent
import android.view.WindowManager
import android.widget.ProgressBar
import androidx.appcompat.app.AlertDialog

/**
* 声音调整控件
*/
class VolumeDialog(activity: Activity) : AlertDialog(activity, R.style.VolumeDialog) {
companion object {
private const val TAG = "VolumeDialog"

@JvmStatic
fun show(activity: Activity) {
activity.let {
if (activity.isFinishing) return
VolumeDialog(activity).show()
}
}
}

private val volumeAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var volume = volumeAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
private val maxVolume = volumeAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
private val step = (maxVolume / 10).coerceAtLeast(1)
private val delayMillis = 1500L

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_volume_progress)

//实现Dialog区域外部事件可以传给Activity
// FLAG_NOT_TOUCH_MODAL作用:即使该window可获得焦点情况下,仍把该window之外的任何event发送到该window之后的其他window
window?.setFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
)

// FLAG_WATCH_OUTSIDE_TOUCH作用:如果点击事件发生在window之外,就会收到一个特殊的MotionEvent,为ACTION_OUTSIDE
window?.setFlags(
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
)

// 顶部显示
// window?.setGravity(Gravity.TOP)
val attrs = window?.attributes
attrs?.apply {
gravity = Gravity.TOP
height = WindowManager.LayoutParams.WRAP_CONTENT
width = WindowManager.LayoutParams.MATCH_PARENT
y = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
35f,
Resources.getSystem().displayMetrics
).toInt()
}
Log.d(TAG, "onCreate() called with: attrs = $attrs")
window?.attributes = attrs
// 按空白处不能取消
setCanceledOnTouchOutside(false)
// 初始化界面控件
initView()
}

private var volumeProgressView: VolumeProgressView? = null
private var progressBar: ProgressBar? = null

private val _handler = Handler(Looper.getMainLooper())
private val r = Runnable {
try {
dismiss()
} catch (e: Exception) {
// sometimes happens windows token error.
e.printStackTrace()
}
}

override fun dismiss() {
_handler.removeCallbacks(r)
super.dismiss()
}

private fun initView() {
Log.d(TAG, "initView: ")
volumeProgressView = findViewById(R.id.vpv_volume)
progressBar = findViewById(R.id.pb_volume)

refreshProgress(volume * 1f / maxVolume)

// 解决不同机型版本兼容问题,onKeyDown 可能被拦截
setOnKeyListener { _, keyCode, event ->
Log.d(TAG, "setOnKeyListener: keyCode = $keyCode, event = $event")
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
handleKeyEvent(keyCode, event)
true
} else {
false
}
}
}

private fun refreshProgress(volumePercent: Float) {
_handler.removeCallbacks(r)
volumeProgressView?.setProgress(volumePercent)
progressBar?.progress = (volumePercent * 1000).toInt()
_handler.postDelayed(r, delayMillis)
}


private fun handleKeyEvent(keyCode: Int, event: KeyEvent): Boolean {
Log.d(TAG, "handleKeyEvent: volume = $volume, maxVolume = $maxVolume, step = $step")
if ((keyCode != KeyEvent.KEYCODE_VOLUME_UP && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN) || event.action != KeyEvent.ACTION_DOWN) return false
volume = if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // up
if (volume == maxVolume) return true
maxVolume.coerceAtMost(volume + step)
} else { // down
if (volume == 0) return true
0.coerceAtLeast(volume - step)
}
val volumePercent = volume * 1f / maxVolume
refreshProgress(volumePercent)

// 变更声音
volumeAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0)
Log.d(TAG, "handleKeyEvent: AudioManager set volume = $volume done.")

return true
}

/**
* 返回事件,仅拦截音量控制事件
*/
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
Log.d(TAG, "onKeyDown() called with: keyCode = $keyCode, event = $event")
return if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
handleKeyEvent(keyCode, event)
return true
} else {
super.onKeyDown(keyCode, event)
}
}
}

自定义Dialog的Style

1
2
3
4
5
6
7
8
9
10
11
12
 <style name="VolumeDialog" parent="android:style/Theme.Dialog">
<!--背景颜色及和透明程度-->
<item name="android:windowBackground">@android:color/transparent</item>
<!--是否去除标题 -->
<item name="android:windowNoTitle">true</item>
<!--是否去除边框-->
<item name="android:windowFrame">@null</item>
<!--是否浮现在activity之上-->
<item name="android:windowIsFloating">true</item>
<!--是否模糊-->
<item name="android:backgroundDimEnabled">false</item>
</style>

观察者模式Kotlin泛型实现消息中心

观察者模式 + Kotlin 泛型实现的简易版消息中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class MessageCenter<T> {
companion object {
private val centers = mutableMapOf<String, Any>()
fun <T> getInstance(clazz: Class<T>): MessageCenter<T> {
return if (centers[clazz.simpleName] != null) {
centers[clazz.simpleName] as MessageCenter<T>
} else {
val messageCenter = MessageCenter<T>()
centers[clazz.simpleName] = messageCenter
messageCenter
}
}
}

fun register(observer: Observer<T>) {
observers.add(observer)
}

fun unregister(observer: Observer<T>) {
if (observers.contains(observer)) {
observers.remove(observer)
}
}

fun post(t: T) {
observers.forEach {
it.receive(t)
}
}

private var observers = mutableListOf<Observer<T>>()
}

interface Observer<T> {
fun receive(t: T)
}

fun main() {
val ob = object : Observer<String> {
override fun receive(t: String) {
println("result is: $t")
}
}
MessageCenter.getInstance(String::class.java).register(ob)
MessageCenter.getInstance(String::class.java).post("txt post.")
MessageCenter.getInstance(String::class.java).unregister(ob)

MessageCenter.getInstance(Int::class.java).post(123)
}
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×