内容
基础:
栈、任务:
每个App应用包含多个TaskRecord,然后通过其字段ArrayList来管理该任务所有Activities。
最早入栈的称为root activity,最晚入栈的则为top activity;入栈后就无法调整activity间顺序,只能出栈再重新入栈。
系统启动后会创建一个Launcher专用AcitivtyStack,若有其他App启动,则会将这些TaskRecord归并到全局性的非Launcher AcitivtyStack中。
Activity启动模式: <activity android:launchMode="standard" ... />:
launchMode |
无FLAG |
FLAG_ACTIVITY_NEW_TASK |
其他 |
standard |
创建新实例并添加至当前栈顶 |
创建新实例并添加至新增任务栈 |
无复用故永不调用onNewIntent() |
singleTop |
当前栈顶若已存在则重用,否则等同standard |
|
栈顶复用 |
singleTask |
当前栈若已存在则连续出栈至露出自身 不存在则新增任务栈 |
|
栈中复用 |
singleInstance |
当前栈只允许存在单个自身Activity |
|
存在即复用 |
singleInstancePerTask |
当前栈不存在,则创建新实例并添加至新增任务栈,且允许入栈 永远处于栈底;若已存在,再次启动自身则连续出栈露出自身 |
|
用于多窗口并排? |
布局:
根布局 - var vg = (ViewGroup) activity.getWindow().getDecorView().getRootView();
LEFT永远处于左侧,而START在RTL布局则会处于右侧,由于LEFT需要注解抑制,故首选START,遇到RTL情况再考虑用LEFT。
宽高设为MATCH_PARENT后,再设置X\Y就无效了。
在 onCreate() 获取布局后宽高:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override public void onGlobalLayout() {
int width = v.getWidth(); int height = v.getHeight();
System.out.println(width+" x "+height);
// 移除监听器,避免重复回调
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
多页面首选简单的TabLayout(可选划切ViewPager2+FragmentStateAdapter),次选需配置xml项的BottomNavigationView(Tabs区)+NavHostFragment(内容区)
implementation("androidx.viewpager2:viewpager2:1.0.0")
<androidx.viewpager2.widget.ViewPager2 android:id="@+id/vp2"
android:layout_width="match_parent" android:layout_height="match_parent" />
注意 - Fragment布局为ConstraintLayout时会被遮挡,故ViewPager2必须设为android:layout_height="match_parent"。
ViewPager2 vp2=findViewById(R.id.vp2); vp2.setAdapter(new VP2Adapter(this)); // 或用RecyclerView.Adapter
public class VP2Adapter extends FragmentStateAdapter {
public VP2Adapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); }
List<Fragment> list = List.of(VP2Fragment.newInstance(null, null), VP2Fragment.newInstance(null, null));
@NonNull @Override // 延迟实例化可将List.of(f...)改为在createFragment中new对象。
public Fragment createFragment(int position) { return list.get(position); }
@Override public int getItemCount() { return list.size(); }
}
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
联动ViewPager2和TabLayout:
TabLayout tl = findViewById(R.id.tabs);
new TabLayoutMediator(tl, vp2, (tab, position) ->
tab.setText("Tab " + (position + 1))).attach();
自定义tab视图:tab.setCustomView(view); 或 tl.getTabAt(0).setCustomView(view);
最简Fragment:
public class MiniFragment extends Fragment {
@Nullable @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_vp2, container, false);
}
}
View控件:
根View对象(ViewGroup/含标题栏) - activity.this.getWindow().getDecorView() 等同 view.getRootView();
内容视图(布局文件/不含标题栏) - activity.this.findViewById(android.R.id.content)
Toast前台替代 - Snackbar.make(view.getRootView(), "msg", Snackbar.LENGTH_SHORT).show();
FloatingActionButton(FAB/基于ImageView) - 比普通控件多一个交互动画。
通过名字获取resId:
@SuppressLint("DiscouragedApi") // 允许用但性能差,为0则无此resId。
getResources().getIdentifier("google_app_id", "string", getPackageName());
应用栏:Toolbar 取代了 ActionBar,外层还可套个AppBarLayout进行动态互动。
setSupportActionBar((Toolbar) findViewById(R.id.my_toolbar));
悬浮窗:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// TYPE_APPLICATION_OVERLAY 已取代了废弃的 TYPE_SYSTEM_ALERT;FLAG_NOT_FOCUSABLE指不拦截自身视图外的事件;TRANSLUCENT指修改视图默认背景黑色至透明色
var lp = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); lp.width = WRAP_CONTENT; lp.height = WRAP_CONTENT;
// 布局使用资源文件 或 动态生成:var l = new LinearLayout(this);
var l = LayoutInflater.from(ServiceContext.getApplicationContext()).inflate(R.layout.alert_float_layout, null);
wm.addView(l, lp); // 父级类型参数必须是WindowManager.LayoutParams
// 插值动画;非悬浮布局则用:view.animate().x(event.getRawX() + dX).y(event.getRawY() + dY).setDuration(0).start();
var va = ValueAnimator.ofFloat(lp.x, event.getRawX() + dX); // 首参为起值,尾参为动画终值。
//va.setDuration(0);
va.addUpdateListener(animation -> {
lp.x = Math.round((Float) animation.getAnimatedValue());
wm.updateViewLayout(layout, lp);
}); va.start();
悬浮窗拖动示例:ACTION_DOWN记忆起点,ACTION_MOVE进行移动,ACTION_UP可选,区分点击和拖动时才会用到。
注意 - 若父容器布局参数为 LayoutParams(MATCH_PARENT, MATCH_PARENT),修改x/y则不会移位,故应改为WRAP_CONTENT。
实例教程 - https://blog.csdn.net/nihaoqiulinhe/article/details/51579853
app.getFloatLayout().getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
var whRect = new Rect();
app.getFloatLayout().getWindowVisibleDisplayFrame(whRect);
System.out.println(whRect.width() + " x " + whRect.height());
// 移除监听器,避免重复回调
app.getFloatLayout().getViewTreeObserver().removeOnGlobalLayoutListener(this);
var layout = app.getFloatLayout();
var lp = (WindowManager.LayoutParams) layout.getLayoutParams();
var wh = new float[2];
// 布局参数设置Gravity.END或BOTTOM后需翻转下:
if ((lp.gravity & Gravity.END) == Gravity.END) {
wh[0] = -(whRect.width() * 2);
}
if ((lp.gravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
wh[1] = -(whRect.height() * 2);
}
System.out.println(Arrays.toString(wh));
btn.setOnTouchListener((view, event) -> {
// 合并写法: event.getRawX() 和翻转条件 whRect.width() - event.getRawX();
var dX = Math.abs(event.getRawX() + wh[0]);
var dY = Math.abs(event.getRawY() + wh[1]);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("ACTION_DOWN - " + Arrays.toString(wh));
view.setTag(Pair.create(Pair.create(lp.x - dX, lp.y - dY), true));
break;
case MotionEvent.ACTION_MOVE:
//System.out.println("ACTION_MOVE");
var tag = (Pair<Pair<Float, Float>, Boolean>) view.getTag();
var va = ValueAnimator.ofFloat(lp.x, tag.first.first + dX);
va.addUpdateListener(a -> {
lp.x = Math.round((Float) a.getAnimatedValue());
wm.updateViewLayout(layout, lp); // ValueAnimator仅用于动画插值,可去除。
});
va.setDuration(0);
va.start();
var vava = ValueAnimator.ofFloat(lp.y, tag.first.second + dY);
vava.addUpdateListener(a -> {
lp.y = Math.round((Float) a.getAnimatedValue());
wm.updateViewLayout(layout, lp); // layout.setX(xx) 会卡住?
});
vava.setDuration(0);
vava.start();
break;
case MotionEvent.ACTION_UP:
System.out.println("ACTION_UP");
var t = (Pair<Pair<Float, Float>, Boolean>) view.getTag();
if (t.second) { // 拖动标志 - [未实现]区分点击和拖动,关键是记录按下抬起时长,低于500ms可判定为点击。
view.setTag(Pair.create(t.first, false));
//view.performClick(); // 抬起才触发;纯拖动无点击则用不到该标志。
}
break;
default:
return false;
}
return true;
});
}
});
关闭菜单文字大写:
<resources ...>
<style name="MyTheme" parent="...">
<item name="android:textAllCaps">false</item><!- 全局 ->
<item name="actionMenuTextAppearance">@style/amta</item><!- 仅菜单按钮 ->
</style>
<style name="amta" parent="@style/TextAppearance.AppCompat.Widget.ActionBar.Menu">
<item name="android:textAllCaps">false</item>
</style>
</resources>