从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

Android UI Design、Activity 布局、WebView组件、悬浮窗、动画渐变


综合/最新

死记:
  子控件通知父控件自己如何布局,入参类型要匹配父级的布局参数:child.setLayoutParams(new LinearLayout.LayoutParams(180, 180)); // 等同 wm.updateViewLayout(layout, lp);

内容

基础:
  栈、任务:
    每个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>

其他