一文学习 Android 原生开发

四大组件

组件是一个Android程序至关重要的构建模块.每一个组件都是系统进入你的应用的不同途径.但并不是所有的组件都是用户进入程序的真实入口, 其中一些要依赖于其它组件, 但是每一个组件都以自己独有的形式存在, 并发挥特殊的作用; 每一个组件都是一个唯一的模块, 帮助你实现程序的各种行为.

应用组件是 Android 应用的基本构建块.每个组件都是一个入口点, 系统或用户可通过该入口点进入您的应用.有些组件会依赖于其他组件.

共有四种不同的应用组件类型:

  1. Activity
  2. 服务
  3. 广播接收器
  4. 内容提供程序

当系统启动某个组件时,它会启动该应用的进程 (如果尚未运行) , 并实例化该组件所需的类 .

例如, 如果您的应用启动相机应用中拍摄照片的 Activity, 则该 Activity 会在属于相机应用的进程 (而非您的应用进程) 中运行.因此, 与大多数其他系统上的应用不同, Android 应用并没有单个入口点 (即没有 main() 函数) .

由于系统在单独的进程中运行每个应用, 且其文件权限会限制对其他应用的访问, 因此您的应用无法直接启动其他应用中的组件, 但 Android 系统可以.如要启动其他应用中的组件, 请向系统传递一条消息,说明启动特定组件的 Intent .系统随后便会为您启动该组件.

启动组件

在四种组件类型中, 有三种 (Activity 服务和广播接收器) 均通过异步消息Intent 进行启动.Intent 会在运行时对各个组件进行互相绑定.您可以将 Intent 视为从其他组件 (无论该组件是属于您的应用还是其他应用) 请求操作的信使.

与 Activity 服务和广播接收器不同,内容提供程序并非由 Intent 启动.相反, 它们会在成为 ContentResolver 的请求目标时启动.内容解析程序会通过内容提供程序处理所有直接事务, 因此通过提供程序执行事务的组件便无需执行事务, 而是改为在 ContentResolver 对象上调用方法.这会在内容提供程序与请求信息的组件之间留出一个抽象层 (以确保安全) .

每种组件都有不同的启动方法:

  • Activity

    如要启动 Activity, 您可以向 startActivity() 或 startActivityForResult() 传递 Intent (当您想让 Activity 返回结果时) , 或者为其安排新任务.

  • Service

    在 Android 5.0 (API 级别 21) 及更高版本中, 您可以使用 JobScheduler 类来调度操作.对于早期 Android 版本, 您可以通过向 startService() 传递 Intent 来启动服务 (或对执行中的服务下达新指令) .您也可通过向将 bindService() 传递 Intent 来绑定到该服务.

  • Broadcast

    您可以通过向 sendBroadcast() sendOrderedBroadcast() 或 sendStickyBroadcast() 等方法传递 Intent 来发起广播.

  • ContentResolver

    您可以通过在 ContentResolver 上调用 query(), 对内容提供程序执行查询.

Android 开发指南 >应用组件

\[N_Android\]

1.Activity

文档

活动代表了一个具有用户界面的单一屏幕, 如 Java 的窗口或者帧.Android 的活动是 ContextThemeWrapper 类的子类.

如果你曾经用 C,C++ 或者 Java 语言编程, 你应该知道这些程序从 main() 函数开始.很类似的, Android 系统初始化它的程序是通过活动中的 onCreate() 回调的调用开始的.存在有一序列的回调方法来启动一个活动, 同时有一序列的方法来关闭活动,

Activity 是与用户交互的入口点.它表示拥有界面的单个屏幕.例如, 电子邮件应用可能有一个显示新电子邮件列表的 Activity 一个用于撰写电子邮件的 Activity 以及一个用于阅读电子邮件的 Activity.

尽管这些 Activity 通过协作在电子邮件应用中形成一种紧密结合的用户体验, 但每个 Activity 都独立于其他 Activity 而存在 .因此, 其他应用可以启动其中任何一个 Activity (如果电子邮件应用允许) .

例如, 相机应用可以启动电子邮件应用内用于撰写新电子邮件的 Activity, 以便用户共享图片.

Android 开发指南 >Activity

注册

AndroidManifest.xml 注册

xml 复制代码
  <activity
    android:name=".MainActivity"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <action android:name="FROM_BOOT" />
        <!-- 意图过滤 , 从LAUNCHER启动 必须!-->
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
  • 声明 intent 过滤器
    Intent 过滤器是 Android 平台的一项非常强大的功能.借助这项功能, 您不但可以根据显式请求启动 Activity, 还可以根据隐式请求启动 Activity.

例如, 显式请求可能会告诉系统"在 Gmail 应用中启动'发送电子邮件'Activity", 而隐式请求可能会告诉系统"在任何能够完成此工作的 Activity 中启动'发送电子邮件'屏幕".当系统界面询问用户使用哪个应用来执行任务时, 这就是 intent 过滤器在起作用.

代码

java 复制代码
public class MainActivity extends AppCompatActivity  {
    private static final String TAG =  "MainActivity";
    public static final String FROM_BOOT = "FROM_BOOT";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UncaughtHandler.getInstance().init(this);
        //在锁屏 界面 显示
//        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
//                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
        setContentView(R.layout.activity_main);
        //start
        final Intent intent = new Intent(this, LinstenService.class);
        startService(intent);

    //scan
        Button but_scan = (Button) findViewById(R.id.but_scan);
        but_scan.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                IntentIntegrator integrator =  new IntentIntegrator(MainActivity.this);
                // 设置要扫描的条码类型, ONE_D_CODE_TYPES: 一维码, QR_CODE_TYPES-二维码
                integrator.setDesiredBarcodeFormats(IntentIntegrator.ONE_D_CODE_TYPES);
                integrator.setCaptureActivity(ScanActivity.class);
                integrator.setPrompt("请扫描条形码"); //底部的提示文字, 设为""可以置空
                integrator.setCameraId(0); //前置或者后置摄像头
                integrator.setBeepEnabled(true); //扫描成功的「哔哔」声, 默认开启
                integrator.setBarcodeImageEnabled(true);
                integrator.initiateScan();
            }
        });
      
    }


    @Override
    protected void onStart() {
       
    }

    public  void onActivityResult(int requestCode, int resultCode, Intent data) {
//        IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
//        String result = scanResult.getContents();
       // Toast.makeText(this, "TTT result="+result, Toast.LENGTH_SHORT).show();
        /*
        if (result != null) {
            Intent it2 = new Intent(this, SampleViewActivity.class);
            //Toast.makeText(this, "result="+result, Toast.LENGTH_SHORT).show();
            Log.i(TAG, "scan result = "+result);
            it2.putExtra("barCode", result);
            startActivity(it2);
        }*/
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

}

回调 描述

onCreate() 这是第一个回调, 在活动第一次创建时调用

onStart() 这个回调在活动为用户可见时被调用

onResume() 这个回调在应用程序与用户开始可交互的时候调用

onPause() 被暂停的活动无法接受用户输入, 不能执行任何代码.当前活动将要被暂停, 上一个活动将要被恢复时调用

onStop() 当活动不在可见时调用

onDestroy() 当活动被系统销毁之前调用

onRestart() 当活动被停止以后重新打开时调用

Activity 的交互

一个交互 例子

  • 传递参数 启动
java 复制代码
Intent confIntent = new Intent(this, ConfigActivity.class);
confIntent.putExtra("id","123");
startActivity(confIntent);
  • 另外一边 接受
    String id = this.getIntent().getStringExtra("id");
  1. 通过一个意图对象(Intent) Intent.putExtra(key,val)传参startActivity(confIntent)启动 Activity (亦可以启动其他组件, 例如服务startService(intent);),
  2. Activity 结束后通过setResult(int resultCode, Intent data)返回结果,
  3. 上一个Activity结束后, 通过onActivityResult(int requestCode, int resultCode, Intent data) 得到结果

使用 Bundle 绑定参数

如果是要从A界面传到B界面, B界面要再传到C界面, Intent就要写两遍添加值的方法 那么 如果我用1个Bundle 直接把值先存里边 然后再存到Intent中 不就更简洁吗?

//传递参数 启动

java 复制代码
Intent intent = new Intent(this, ConfigActivity.class);
Bundle bundle = new Bundle();  
bundle.putString("key1", value1);  
bundle.putString("key2", value2);
intent.putExtras(bundle);
startActivity(bundle);

///////////////////////////////////////////
Intent resultIntent = new Intent();
resultIntent.putExtra("key", "value"); // 设置需要返回的数据,可根据需要添加数据

// 将数据设置到结果Intent中,并指定结果码(RESULT_OK通常表示操作成功)
setResult(Activity.RESULT_OK, resultIntent);

// 结束当前Activity并返回结果Intent
finish();

//另外一边 接受

java 复制代码
Bundle bundle = this.getIntent().getExtras();  
String v = bundle.getString("key1");

一个调用系统拍照Activity的例子

java 复制代码
//启动
Intent intentCamer = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);// MediaStore.ACTION_IMAGE_CAPTURE 系统拍照Activity 常量
String filePath = "/sdcard/" + System.currentTimeMillis() + ".jpg";//要保存照片的绝对路径
try {
    ContentValues contentValues = new ContentValues(2);
    contentValues.put(MediaStore.Images.Media.DATA, filePath);
    //如果想拍完存在系统相机的默认目录,改为
    //contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, "111111.jpg");
    contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");

    Uri mPhotoUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
    intentCamer.putExtra(MediaStore.EXTRA_OUTPUT, mPhotoUri);
    startActivityForResult(intentCamer,0);
}catch(SecurityException e){
    Toast.makeText(MainActivity.this, "没有权限,访问相机.", Toast.LENGTH_LONG).show();
}

//接受结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    Bitmap bitmap= null;
    if (data!= null && data.getData() == null) {
        //获取到的是Thumbnail所以会很小.但是你传了output路径进去, output路径在拍完照会被写入图片数据
        bitmap = (Bitmap) data.getExtras().get("data");
        ImageView imageView = (ImageView) findViewById(R.id.imageView);
        imageView.setImageBitmap( bitmap);
        Log.i(TAG, "height="+  bitmap.getHeight()+", width="+ bitmap.getWidth());
    }
}

需要 访问相机 和 读写文件权限

新API

在Android SDK 23中,startActivityForResultonActivityResult,已经被官方标记为弃用了,继而推出了名为Activity Result API的组件。

java 复制代码
ActivityResultLauncher<Intent> activityLauncher = registerForActivityResult(
    new ActivityResultContracts.StartActivityForResult(),
    result -> {
        if (result.getResultCode() == Activity.RESULT_OK) {
            // 处理返回的数据
            Intent data = result.getData();
            // 可以在这里处理返回的Intent中的数据
        }
    }
);

// 启动目标Activity并处理返回结果
Intent intent = new Intent(this, YourSecondActivity.class);
activityLauncher.launch(intent);

自定义组件

  • xml布局指定类名
xml 复制代码
 <com.journeyapps.barcodescanner.DecoratedBarcodeView
            android:id="@+id/dbv_custom"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            app:zxing_preview_scaling_strategy="fitXY" />
  • 然后继承 安卓组件, 它会回调事件以及方法

注意要重写几个有参构造

java 复制代码
class DecoratedBarcodeView extends FrameLayout{


    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
    }
}

参考 Android-自定义控件开发

获取xml的属性

java 复制代码
public MyImageButton(Context context, AttributeSet attrs) {
    super(context, attrs);
    //第一个参数命名空间, 
    /*取得资源ID号,第一个参数:命名空间名.第二个参数:xml文件里设置的属性名.第三个参数:默认值*/  
    final String xmlns="http://schemas.android.com/apk/res/android";
    int resouceId = attrs.getAttributeResourceValue(xmlns, "text", -1);
    if(resouceId > 0)
        text = context.getResources().getText( resouceId).toString();//xml里 要通过引用的方式 @string/xxx
}

一个手写面板的自定义控件实现

组件源码

搞活动签名的项目源码

  • 使用
xml 复制代码
 <com.joinken.group.activity.HandWritePanel
                android:id="@+id/hwp"
                android:layout_width="wrap_content"
                android:layout_height="250dp"
                android:background="@drawable/sign_bg" />
java 复制代码
HandWritePanel hwp
//找到 实例
hwp = (HandWritePanel) findViewById(R.id.hwp);

/**
* 保存图片到SD卡上
*/
protected boolean saveBitmap(String fileName) {
    boolean ret = false;
    FileOutputStream stream = null;
    Bitmap baseBitmap = hwp.getPic();
    try {
        // 保存图片到SD卡上
        File file = new File(fileName);
        stream = new FileOutputStream(file);
        baseBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
//            Toast.makeText(SignActivity.this, "保存成功", Toast.LENGTH_LONG).show();
        ret = true;
    } catch (Exception e) {
        Toast.makeText(SignActivity.this, "保存失败!"+e.getMessage(), Toast.LENGTH_LONG).show();
    }finally{
        try {
            stream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return ret;
}

//清除
  hwp.clean();

Activity 多xml布局 查找控件

java 复制代码
LayoutInflater layout=this.getLayoutInflater();
View view=layout.inflate(R.layout.layout_sample_view, null);//代码新建布局
setContentView(view);//在设置完布局后

//指定在这个view 中查找
EditText editText = view.findViewById(R.id.id_);
editText.setText(bean.getId());

\[N_Android\]

2. Service

文档

Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件.服务可由其他应用组件启动, 而且即使用户切换到其他应用, 服务仍将在后台继续运行.此外, 组件可通过绑定到服务与之进行交互, 甚至是执行进程间通信 (IPC).例如, 服务可在后台处理网络事务 播放音乐, 执行文件 I/O 或与内容提供程序进行交互.

以下是三种不同的服务类型:

  • 前台

    前台服务执行一些用户能注意到的操作.例如, 音频应用会使用前台服务来播放音频曲目.前台服务必须显示通知.即使用户停止与应用的交互, 前台服务仍会继续运行.

  • 后台

    后台服务执行用户不会直接注意到的操作.例如, 如果应用使用某个服务来压缩其存储空间, 则此服务通常是后台服务.

注意: 如果您的应用面向 API 级别 26 或更高版本, 当应用本身未在前台运行时, 系统会对运行后台服务施加限制.在诸如此类的大多数情况下, 您的应用应改为使用计划作业.

  • 绑定
    当应用组件通过调用 bindService() 绑定到服务时, 服务即处于绑定状态.绑定服务会提供客户端-服务器接口, 以便组件与服务进行交互 发送请求 接收结果, 甚至是利用进程间通信 (IPC) 跨进程执行这些操作.仅当与另一个应用组件绑定时, 绑定服务才会运行.多个组件可同时绑定到该服务, 但全部取消绑定后, 该服务即会被销毁.

Services overview

基础/生命周期

简单地说, 服务是一种即使用户未与应用交互也可在后台运行的组件, 因此, 只有在需要服务时才应创建服务.

如果您必须在主线程之外执行操作, 但只在用户与您的应用交互时执行此操作, 则应创建新线程.例如, 如果您只是想在 Activity 运行的同时播放一些音乐, 则可在 onCreate() 中创建线程, 在 onStart() 中启动线程运行, 然后在 onStop() 中停止线程.您还可考虑使用 AsyncTask 或 HandlerThread, 而非传统的 Thread 类.如需了解有关线程的详细信息, 请参阅进程和线程文档.

请记住, 如果您确实要使用服务,则默认情况下, 它仍会在应用的主线程中运行, 因此, 如果服务执行的是密集型或阻止性操作, 则您仍应在服务内创建新线程.

启动服务由另一个组件通过调用 startService() 启动, 这会导致调用服务的 onStartCommand() 方法.

服务启动后, 其生命周期即独立于启动它的组件.即使系统已销毁启动服务的组件, 该服务仍可在后台无限期地运行.因此, 服务应在其工作完成时通过调用 stopSelf() 来自行停止运行, 或者由另一个组件通过调用 stopService() 来将其停止.

onStartCommand()

当另一个组件 (如 Activity) 请求启动服务时, 系统会通过调用 startService() 来调用此方法.执行此方法时, 服务即会启动并可在后台无限期运行.如果您实现此方法, 则在服务工作完成后, 您需负责通过调用 stopSelf() 或 stopService() 来停止服务. (如果您只想提供绑定, 则无需实现此方法.)

onBind()

当另一个组件想要与服务绑定 (例如执行 RPC) 时, 系统会通过调用 bindService() 来调用此方法.在此方法的实现中, 您必须通过返回 IBinder 提供一个接口, 以供客户端用来与服务进行通信.请务必实现此方法; 但是, 如果您并不希望允许绑定, 则应返回 null.

onCreate()

首次创建服务时, 系统会 (在调用 onStartCommand() 或 onBind() 之前) 调用此方法来执行一次性设置程序.如果服务已在运行, 则不会调用此方法.

onDestroy()

当不再使用服务且准备将其销毁时, 系统会调用此方法.服务应通过实现此方法来清理任何资源, 如线程 注册的侦听器 接收器等.这是服务接收的最后一个调用.

注册

首先在 AndroidManifest.xml 注册

xml 复制代码
<manifest ... >
  ...
  <application ... >
      <service android:name=".serivce.LinstenService" 
      <!-- 前台服务类型 安卓14 必须-->
      android:foregroundServiceType="location"
      />
      ...
  </application>
</manifest>

如果应用以 Android 9(API 级别 28)或更高版本为目标平台并使用前台服务,则需要在应用清单中请求 FOREGROUND_SERVICE,如以下代码段所示。这是普通权限,因此,系统会自动为请求权限的应用授予此权限。

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

    <application ...>
        ...
    </application>
</manifest>

前台 Service

foreground-services

如果您希望用户无法关闭通知,请在使用 Notification.Builder 创建通知时将 true 传入 setOngoing() 方法。

在该服务内(通常在 onStartCommand() 中),您可以请求在前台运行您的服务。为此,请调用 ServiceCompat.startForeground()(在 androidx-core 1.12 及更高版本中提供)。此方法采用以下参数:

这些类型可能是清单中声明的类型的子集,具体取决于特定用例。然后,如果需要添加更多服务类型,可以再次调用 startForeground()

以下是前台服务的示例:

kotlin 复制代码
class ForegroundService : Service() {

    private var notificationManager: NotificationManager? = null

    companion object{
        private const val NOTIFICATION_ID = 1
        private const val CHANNEL_ID = "my_foreground_service_channel"
    }


    override fun onCreate() {
        super.onCreate()
        notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
    }
    
    @SuppressLint("ForegroundServiceType")
    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        XLog.i("前台服务  onStartCommand ")
        val channel = NotificationChannel(
            CHANNEL_ID,
            "前台服务已启用",
            NotificationManager.IMPORTANCE_DEFAULT
        )
        notificationManager?.createNotificationChannel(channel)
        // 构建服务
        val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("服务标题")
            .setContentText("This is a foreground service running.")
            .setSmallIcon(R.drawable.btn_star)
            .build()
        startForeground(NOTIFICATION_ID, notification)
        return START_STICKY
    }
.........

启用

kotlin 复制代码
    private fun initService() {
        var requeset = PermissionRequest.Builder(this, 101, Manifest.permission.FOREGROUND_SERVICE)
            .setRationale("需要通知权限")
            .setPositiveButtonText("允许")
            .setNegativeButtonText("拒绝")
            .build()
        requestPermission(requeset){
            //前台通知
            val serviceIntent = Intent(
                this,
                ForegroundService::class.java
            )
            startForegroundService(serviceIntent)
        }

    }

    public fun requestPermission(request: PermissionRequest, callback: Consumer<Boolean>) {
        val requestCode = request.requestCode.toString()
        if (EasyPermissions.hasPermissions(this, *request.perms) ){
          //已有权限
            callback.accept( true)
        }else{
            EasyPermissions.requestPermissions( request);
            permissionCallbacks[requestCode] = callback
        }
    }

后台 Service

https://developer.android.google.cn/develop/background-work/services?hl=en

kotlin 复制代码
// 启动服务  
Log.i(TAG," === initService")  
val serviceIntent = Intent(this, LatlngService::class.java)  
startService(serviceIntent)

绑定服务

https://developer.android.google.cn/develop/background-work/services/bound-services?hl=zh-cn

绑定服务是客户端-服务器接口中的服务器。它允许 activity 等组件绑定到服务、发送请求、接收响应,以及执行进程间通信 (IPC)。绑定服务通常只在为其他应用组件提供服务时处于活动状态,不会无限期在后台运行。

关键方法:

onBind()

当另一个组件想要与服务绑定 (例如执行 RPC) 时, 系统会通过调用 bindService() 来调用此方法.在此方法的实现中, 您必须通过返回 IBinder 提供一个接口, 以供客户端用来与服务进行通信.请务必实现此方法; 但是, 如果您并不希望允许绑定, 则应返回 null.

您可以将多个客户端同时连接到某项服务。但是,系统会缓存 IBinder 服务通信通道。换句话说,只有在第一个客户端绑定时,系统才会调用服务的 [onBind()](https://developer.android.google.cn/reference/android/app/Service?hl=zh-cn#onBind(android.content.Intent)) 方法以生成 IBinder。然后,系统会将该 IBinder 传递至绑定到同一服务的所有其他客户端,无需再次调用 onBind()

在你的 Activity 中

kotlin 复制代码
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private var myService: MyService? = null
    private var isBound: Boolean = false

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val binder = service as MyService.MyBinder
            myService = binder.getService()
            isBound = true
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            isBound = false
        }
    }

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

        val intent = Intent(this, MyService::class.java)
        bindService(intent, connection, Context.BIND_AUTO_CREATE)
    }

    // 在需要调用服务的地方调用此方法
    private fun callServiceMethod() {
        if (isBound) {
            val result = myService?.performTask()
            // 处理服务返回的结果
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (isBound) {
            unbindService(connection)
            isBound = false
        }
    }
}
kotlin 复制代码
class MyService : Service() {

    // 内部类用于绑定服务的通信
    inner class MyBinder : Binder() {
        fun getService(): MyService = this@MyService
    }
    private val binder = MyBinder()
    
    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }
    // 服务中的方法,可以在 Activity 中调用
    fun performTask(): String {
        return "Task performed by the service"
    }
}

完整前台通知服务代码

前台通知 实现

java 复制代码
public class LinstenService extends Service implements Runnable {
    private static final String TAG = "LinstenService";
    private static final int F_NOTIFY_ID = 1;
    private static final String CHANNEL_ID = "LinstenService";
    private static final String CHANNEL_NAME = "Channel One";

    ....

    /**
     * 电源管理 保证CPU 不休眠 需要  WAKE_LOCK 权限 很耗电量 ...
     * https://developer.android.google.cn/reference/kotlin/android/os/PowerManager
     */
    private PowerManager.WakeLock wakeLock = null;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        /**
         * 前台服务
         * foreground service
         */
        Notification.Builder localBuilder = new Notification.Builder(this);
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        if (android.os.Build.VERSION.SDK_INT > 26) {//android 8+
            NotificationChannel notificationChannel = null;
            notificationChannel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
            notificationChannel.enableLights(true);
            notificationChannel.setLightColor(Color.RED);
            notificationChannel.setShowBadge(true);
            notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
            notificationChannel.setSound(null, null);
            notificationManager.createNotificationChannel(notificationChannel);
            localBuilder.setChannelId(CHANNEL_ID);
        }
        localBuilder.setAutoCancel(false);
        localBuilder.setContentTitle("LinstenService");
        localBuilder.setContentText("正在运行...");
        localBuilder.setSmallIcon(R.mipmap.ic_launcher);
//        localBuilder.setDefaults(Notification.DEFAULT_ALL); // notify sound
//        localBuilder.setLights(Color.GREEN, 1000, 2000); // notify light
        startForeground(F_NOTIFY_ID, localBuilder.build());//foreground service 为前台
    }
	 ....

    /**
     * 简单 通知
     */
    public void takeNotify(SampleBean bean) {
        final String channel_id = "takeNotify";
        final String channel_name = "Channel_2";
        //PARTIAL_WAKE_LOCK only cpu,
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        wakeLock = pm.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_BRIGHT_WAKE_LOCK, LinstenService.class.getName());
        wakeLock.acquire();//保持  点亮屏幕 CPU 不休眠
        wakeLock.release();//释放

        Notification.Builder localBuilder = new Notification.Builder(this);
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        if (android.os.Build.VERSION.SDK_INT > 26) {//android 8+
            NotificationChannel notificationChannel = null;
            notificationChannel = new NotificationChannel(channel_id, channel_name, NotificationManager.IMPORTANCE_HIGH);
            notificationChannel.enableLights(true);
            notificationChannel.setLightColor(Color.RED);
            notificationChannel.setShowBadge(true);
            notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
            notificationChannel.setSound(null, null);
            notificationManager.createNotificationChannel(notificationChannel);
            localBuilder.setChannelId(channel_id);
        }
        localBuilder.setDefaults(Notification.DEFAULT_ALL); // notify sound
        localBuilder.setLights(Color.GREEN, 1000, 2000);// notify light
        localBuilder.setSmallIcon(R.mipmap.ic_launcher);
        localBuilder.setAutoCancel(false);
        localBuilder.setContentTitle("出库通知");
        localBuilder.setContentText(bean.getName() + ":出库待确认");

        notificationManager.notify(bean.getId(), localBuilder.build());
    }

    .....

}

注意: android 9+ 需FOREGROUND_SERVICE权限:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

java 复制代码
Context context = getApplicationContext();
final Intent intent = new Intent(MainActivity.this, AgentxService.class);
context.startForegroundService(intent);

\[N_Android\]

3. 广播接收器

文档

借助广播接收器组件, 系统能够在常规用户流之外向应用传递事件, 从而允许应用响应系统范围内的广播通知.由于广播接收器是另一个明确定义的应用入口,因此系统甚至可以向当前未运行的应用传递广播.

许多广播均由系统发起, 例如, 通知屏幕已关闭 电池电量不足或已拍摄照片的广播.

应用也可发起广播, 例如, 通知其他应用某些数据已下载至设备, 并且可供其使用.尽管广播接收器不会显示界面, 但其可以创建状态栏通知, 在发生广播事件时提醒用户.

注册

  • AndroidManifest.xml 注册接收器

android:name=".receiver.BootBroadcastReceiver" //指定类名

xml 复制代码
    <!-- 指定类名 等属性-->
    <receiver
        android:name=".receiver.BootBroadcastReceiver"
        android:enabled="true"
        android:exported="true"
        android:process="com.joinken.application">

<!-- 意图过滤 感兴趣的广播, 或者说能够触发接受的意图-->
        <intent-filter>
            <!--系统广播 开机启动 -->
            <action android:name="android.intent.action.BOOT_COMPLETED" />
            <action android:name="android.intent.action.QUICKBOOT_POWERON" />
        </intent-filter>

        <intent-filter>
        <!-- 自定义广播 第三方调用-->
            <action android:name="com.joinken.application.ACTION_TEST" />
        </intent-filter>

        <intent-filter>
            <action android:name="android.intent.action.MEDIA_MOUNTED" />
            <action android:name="android.intent.action.MEDIA_UNMOUNTED" />
            <action android:name="android.intent.action.MEDIA_EJECT" />

            <data android:scheme="file" />
        </intent-filter>
    </receiver>

代码

广播接收器作为 BroadcastReceiver 的子类实现, 并且每条广播都作为 Intent 对象进行传递.

java 复制代码
public class BootBroadcastReceiver extends BroadcastReceiver {
    private static final String TAG =  "BootBroadcastReceiver";


    @Override
    public void onReceive(Context context, Intent intent) {
        
        //start service 启动服务组件
        Intent it = new Intent(context, LinstenService.class);
        context.startService(it);

        /***********start activity 启动activity组件********/
        Intent it2 = new Intent(context, MainActivity.class);
        it2.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//需要这个 FLAG_ACTIVITY_NEW_TASK 才能后台启动activity 弹出到前台
        context.startActivity(it2);
    }
}

ADB 发送广播 /测试

//低电量

adb shell am broadcast -a android.intent.action.ACTION_BATTERY_LOW

//扩展卡 已挂载

adb shell am broadcast -a android.intent.action.ACTION_MEDIA_MOUNTED

//已完成开机

adb shell am broadcast -a android.intent.action.BOOT_COMPLETED

//自定义

adb shell am broadcast -a ACTION_TEST

4.内容提供者

内容提供程序有助于应用管理其自身和其他应用所存储数据的访问,并提供与其他应用共享数据的方法。它们会封装数据,并提供用于定义数据安全性的机制。内容提供程序是一种标准接口,可将一个进程中的数据与另一个进程中运行的代码进行连。实现内容提供程序大有好处。最重要的是,通过配置内容提供程序,您可以使其他应用安全地访问和修改您的应用数据

其他应用可通过你创建的内容提供程序查询或修改数据(如果内容提供程序允许), 提供一个标准在不同应用之间访问数据

Intent 分为两种类型:

显式 Intent

通过提供目标应用的软件包名称或完全限定的组件类名来指定可处理 Intent 的应用.通常, 您会在自己的应用中使用显式 Intent 来启动组件, 这是因为您知道要启动的 Activity 或服务的类名.例如, 您可能会启动您应用内的新 Activity 以响应用户操作, 或者启动服务以在后台下载文件.

隐式 Intent

不会指定特定的组件, 而是声明要执行的常规操作, 从而允许其他应用中的组件来处理.例如, 如需在地图上向用户显示位置, 则可以使用隐式 Intent, 请求另一具有此功能的应用在地图上显示指定的位置.

Intent 和 Intent过滤器

\[N_Android\]

布局/组件

约束布局 (ConstraintLayout)

约束布局ConstraintLayout 是一个ViewGroup, 可以在Api9 以上的Android系统使用它, 它的出现主要是为了解决布局嵌套过多的问题, 以灵活的方式定位和调整小部件.从 Android Studio 2.3 起, 官方的模板默认使用 ConstraintLayout.

属性 说明

layout_constraintLeft_toRightOf 放到相对于某个组件右边; 本组件靠左

假设控件B要放在控件A的右侧, 可以使用 layout_constraintLeft_toRightOf 属性.

xml 复制代码
<Button android:id="@+id/buttonA" ... />
<Button android:id="@+id/buttonB" ...
     app:layout_constraintLeft_toRightOf="@+id/buttonA" />

属性 说明

android:layout_marginStart 设置一个控件相对另一个控件的外边距:

使用 ConstraintLayout 构建自适应界面

线性布局 (Linear Layout)

LinearLayout 又称线性布局, 该布局应该是 Android 视图设计中最经常使用的布局; 该布局可以使放入其中的组件以水平方式或者垂直方式整齐排列, 通过 android:orientation 属性指定具体的排列方式, 通过 weight 属性设置每个组件在布局中所占的比重;

均等分布

如要创建线性布局,让每个子视图使用大小相同的屏幕空间,请将每个视图的 android:layout_height 设置为 "0dp"(针对垂直布局

不等分布

您也可创建线性布局,让子元素使用大小不同的屏幕空间:

如果有三个文本字段,其中两个声明权重为 1,另一个未赋予权重,那么没有权重的第三个文本字段就不会展开,而仅占据其内容所需的区域。另一方面,另外两个文本字段将以同等幅度展开,填充测量三个字段后仍剩余的空间。

如果有三个文本字段,其中两个字段声明权重为 1,而为第三个字段赋予权重 2(而非 0),那么现在相当于声明第三个字段比另外两个字段更为重要,因此,该字段将获得总剩余空间的一半,而其他两个字段均享余下的空间。

android:layout_weight: 指定该子元素在LinearLayout中所占的权重, 值越大权重越低!

developer.android 线性布局

表格布局 (TableLayout)

表格布局继承自LinearLayout, 通过TableRow 设置行 ,列数由TableRow中的子控件决定, 直接在TableLayout中添加子控件会占据整个一行.

常用属性:

属性 说明

android:shrinkColumns 设置可收缩的列, 内容过多就收缩显示到第二行

android:stretchColumns 设置可伸展的列, 将空白区域填充满整个列

android:collapseColumns 设置要隐藏的列

列的索引从0开始, shrinkColumns和stretchColumns可以同时设置.

子控件常用属性:

属性 说明

android:layout_column 第几列

android:layout_span 占据列数

宫格布局 (GridLayout)

作为android 4.0 后新增的一个布局,与前面介绍过的TableLayout(表格布局)其实有点大同小异;

GridLayout的目的是将多个 View / ViewGroup 按照网格的形式排列起来, 所以大多数的属性都是为了规范一个网格的样式;

跟LinearLayout(线性布局)一样,他可以设置容器中组件的对齐方式

容器中的组件可以跨多行也可以跨多列(相比TableLayout直接放组件,占一行相比较)

在Android 5.1(API Level 21)时引入了android:layout_columnWeight和android:layout_rowWeight来解决平分问题

属性

自身属性

属性 说明

android:alignmentMode 说明: 当设置alignMargins, 使视图的外边界之间进行校准.可以取以下值: alignBounds -- 对齐子视图边界/alignMargins -- 对齐子视距内容

android:columnCount GridLayout的最大列数

android:rowCount GridLayout的最大行数

android:columnOrderPreserved 当设置为true, 使列边界显示的顺序和列索引的顺序相同.默认是true

android:orientation GridLayout中子元素的布局方向.有以下取值: horizontal:水平布局/vertical -- 竖直布局

android:rowOrderPreserved 当设置为true, 使行边界显示的顺序和行索引的顺序相同.默认是true

android:useDefaultMargins 当设置ture, 当没有指定视图的布局参数时, 告诉GridLayout使用默认的边距.默认值是false

子元素属性

属性 说明

android:layout_column 显示该子控件的列 , 以0计

android:layout_columnSpan 该控件所占的列数

android:layout_row 显示该子控件的行 以0计

android:layout_rowSpan 该控件所占的行数, 例如android:layout_rowSpan="2",表示当前子控件占两行

android:layout_columnWeight 该控件的列权重, 与android:layout_weight类似, 例如有GridLayout上两列, 都设置android:layout_columnWeight = "1",则两列各占GridLayout宽度的 一半

android:layout_rowWeight 该控件的行权重, 原理同android:layout_columnWeight

当前 View 占据的空间

android:layout_rowSpan: 设置当前 View 占据几行的空间

android:layout_columnSpan: 设置当前 View 占据几列的空间

控件一些属性

layout_gravity: 设置一些对齐方式, 以及填满容器

ScrollView

有时候,我们在写布局的时,在最外层会套一个ScrollView,以防止内容超出屏幕的时候可以滚动。但如果这时候,内容不足以覆盖整个屏幕时,ScrollView 的android:layout_height="match_parent"属性是无效的,它始终都是wrap_content,这时可以使用android:fillViewport="true" 和 让它生效。

对齐属性

android:alignParentBottom 若是该值为true,则将该控件的底部和父控件的底部对齐

android:layout_alignParentLeft 若是该值为true,则将该控件的左边与父控件的左边对齐

android:layout_alignParentRight 若是该值为true,则将该控件的右边与父控件的右边对齐

android:layout_alignParentTop 若是该值为true,则将空间的顶部与父控件的顶部对齐

// 控件

android:layout_centerHorizontal 若是值为真,该控件将被至于水平方向的中央

android:layout_centerInParent 若是值为真,该控件将被至于父控件水平方向和垂直方向的中央

android:layout_centerVertical 若是值为真,该控件将被至于垂直方向的中央

android:gravity 用于设置View组件的对齐方式

android:layout_gravity 用于设置Container组件的对齐方式

自定义标题栏

在Android 3.0中除了我们重点讲解的Fragment外, Action Bar也是一个非常重要的交互元素, Action Bar取代了传统的tittle bar和menu, 在程序运行中一直置于顶部

运行时调用hide()方法也可以隐藏ActionBar, 调用show()方法来显示ActionBar().

ActionBar actionBar = getActionBar();

actionBar.hide();

java 复制代码
//自定义标题栏
ActionBar actionBar = getSupportActionBar();
actionBar.setCustomView(R.layout.layout_main_title);
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
actionBar.setDisplayShowCustomEnabled(true);
actionBar.setDisplayShowHomeEnabled(false);
actionBar.setDisplayShowTitleEnabled(false);

setContentView(R.layout.activity_main);

https://www.cnblogs.com/guanxinjing/p/9708613.html

资源使用

res/values

位于res/valus目录下, 根元素是标记, 在该元素中使用标记定义字符串.其中name属性来指定字符串的名称(name不可大写)

字符串 (String) 资源

xml 复制代码
<resources>
        <string  name="ss">.....</string>
</resources></span>

使用:

a 在MainActivity中使用:gerResources().getString(R.string.name)

b 在TextView中使用:

颜色 (color) 资源

xml 复制代码
<resources>
        <color  name="red">#FF0000</color>
</resources>

使用: a textview.setTextColor(getResources().getColor(R.color.red));

<TextView android:textcolor="@color/red"/>

尺寸 (dimen) 资源

xml 复制代码
<resources>
        <dimen  name="txt">20dp</dimen>
</resources>

使用:

a textview.setTextSize(getResources().getDimension(R.dimen.title));

b

布局 (layout) 资源

布局文件创建完成后, 可以在java代码或者XML中使用

使用:

复制代码
a: setContentView (R.layout.main) 
b: <include layout="@layout/ly"/>

res/drawable

\drawable-v24

\drawable

在Android开发中我们是用Drawable类来Drawable类型资源的.

Drawable资源一般存储在应用程序目录的\res\drawable目录下,当然依据分辨率的高低可以分别存储不同分辨率的资源到如下几个目录:

\res\drawable-hdpi

\res\drawable-ldpi

\res\drawable-mdpi

\res\drawable-xdpi

使用

在java

java 复制代码
getDrawable(R.drawable.filename)
getResources().getDrawable(R.drawable.filename,null)
imageView.getDrawable()
imageView.setImageResource(R.drawable.filename)
imageView.setImageDrawable(drawable)

在xml

xml 复制代码
@drawable/filename

Adapter (MVC模型)

https://www.runoob.com/w3cnote/android-tutorial-adapter.html

Adapter 概念

  • Model:通常可以理解为数据,负责执行程序的核心运算与判断逻辑,,通过view获得用户 输入的数据,然后根据从数据库查询相关的信息,最后进行运算和判断,再将得到的结果交给view来显示
  • view:用户的操作接口,说白了就是GUI,应该使用哪种接口组件,组件间的排列位置与顺序都需要设计
  • Controller:控制器,作为model与view之间的枢纽,负责控制程序的执行流程以及对象之间的一个互动

在MVC模型中 Adapter 这个Controller的部分: Model (数据) ---> Controller (以什么方式显示到)---> View(用户界面) 这就是简单MVC组件的简单理解!

Adapter 继承结构

  • BaseAdapter:抽象类,实际开发中我们会继承这个类并且重写相关方法,用得最多的一个Adapter!
  • ArrayAdapter:支持泛型操作,最简单的一个Adapter,只能展现一行文字~
  • SimpleAdapter:同样具有良好扩展性的一个Adapter,可以自定义多种效果!
  • SimpleCursorAdapter:用于显示简单文本类型的listView,一般在数据库那里会用到,不过有点过时, 不推荐使用!

实例

容器布局 (container)

map_layers_list.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
	android:orientation="vertical"  
	android:layout_width="match_parent"  
	android:layout_height="match_parent">  
	<ListView  
		android:id="@+id/listview"  
		android:layout_width="match_parent"  
		android:layout_height="wrap_content" />  
</LinearLayout>

var listview = findViewById<ListView>(R.id.listview)

ListView 则是将要应用 Adapter 的容器

内容布局 (item)

map_layers_list_item.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <CheckBox
            android:id="@+id/show"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="显示" />
        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="name" />
        <ImageButton
            android:id="@+id/but_down"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@android:drawable/stat_sys_download" />
        <ImageButton
            android:id="@+id/but_up"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@android:drawable/stat_sys_upload" />
</LinearLayout>

Adapter 关联(SimpleAdapter)

kotlin 复制代码
	//容器
	bottomSheetDialog!!.show();
	var listview = bottomSheetDialog!!.findViewById<ListView>(R.id.listview)
	/**
	数据结构
	[{"checked":true,"id":"9f149c8228e94db9a6dba36ccf0275a1","name":"坡型图"},{"checked":true,"id":"26e5c5581f044831ab2c8a7521fda660","name":"坡向图"}]
	*/
	val listMap = JacksonUtils.jsonToListMapBean(json);
	/*
	对应 数据键 对 ui id
	*/
	val from = arrayOf("checked", "name")
	val to = intArrayOf(R.id.show, R.id.name)
	/**
	listview 关联  Adapter
	*/
	//val adapter = SimpleAdapter(this, listMap,R.layout.map_layers_list_item, from, to);
	val adapter = object: SimpleAdapter(this, listMap,R.layout.map_layers_list_item, from, to){  
		override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {  
			val view = super.getView(position, convertView, parent)  
			//查找Adapter内的UI组件
			val button: ImageButton = view.findViewById(R.id.but_up)  
			//监听事件
			button.setOnClickListener {}
			// 注意 CheckBox  组件是复用的, 真XX恶心; 用 ClickListener 曲线救国
			val show : CheckBox = view.findViewById(R.id.show)  
			var data = listMap.get(position) as LinkedHashMap  
			show.isChecked = data["checked"] as Boolean  
			show.setOnClickListener{ _->  
				data["checked"] = !(data["checked"] as Boolean)  
				Log.i(TAG," getView 配置结果: "+ listMap)  
				notifyDataSetChanged()  
			}

			return view  
		}  
	
	}
	
	if (listview != null) {
		listview.adapter = adapter
	}

异步任务/非主线程更新UI

异步任务

java 复制代码
 AsyncTask syncTask = new AsyncTask(){
    @Override
    protected Object doInBackground(Object[] objects) {
        //TODO 耗时任务  返回结果
        return null;
    }
    @Override
    protected void onPostExecute(Object result) {
        //完成后接受 结果
    }
};
//开始异步, 可提交参数, 就是上面的 Object[] objects
syncTask.execute(arg1,arg2);

取消

值得注意的是, 调用cancel(true)函数需要注意以下几点:

  1. 当AsyncTask已经完成, 或则以及被取消, 亦或其他原因不能被取消, 调用cancel()会失败;

  2. 如果AsyncTask还没有开始执行, 调用cancel(true)函数后, AsyncTask不会再执行;

  3. 如果AsyncTask已经开始执行, 参数mayInterruptIfRunning决定是否立即stop该Task;

  4. 调用cancel()后, doInBackground()完成后, 不再调用onPostExecute(), 而是执行onCancelled();

非主线程更新UI

使用Handlerpost方法,传入一个Runnable接口, 里面run方法 就可以更新UI了

java 复制代码
android.os.Handler handler=new Handler();//在子线程创建 android.os.Handler handler=new Handler(Looper.getMainLooper());
handler.post(
    new Runnable(){
          @Override
        public void run(){
            //更新UI
           iv.setImageBitmap(baseBitmap);  
        }
    }
)

非阻塞弹窗

  • 非阻塞弹窗
java 复制代码
AlertDialog.Builder builder  = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("确认" ) ;
builder.setMessage("HTTP结果="+result.toString()+";\r\n"+"本地结果="+MainActivity.this.getHash(MainActivity.this) ) ;
builder.setPositiveButton("是" ,  null );
builder.show();

AlertDialog(简单对话框)

确认对话框

kotlin 复制代码
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("确认")
       .setMessage("确定要执行此操作吗?")
       .setPositiveButton("确认", new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int id) {
               // 用户点击确认时的操作
           }
       })
       .setNegativeButton("取消", new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int id) {
               // 用户点击取消时的操作
           }
       });
AlertDialog alertDialog = builder.create();
alertDialog.show();

列表对话框

显示一个列表供用户选择。

kotlin 复制代码
final CharSequence[] items = {"选项1", "选项2", "选项3"};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("选择一个选项")
       .setItems(items, new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int index) {
               // 用户选择列表项时的操作
               // index 是用户选择的索引
           }
       });
AlertDialog alertDialog = builder.create();
alertDialog.show();

自定义布局 (xml)

kotlin 复制代码
  // 创建弹窗
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MainActivity.this);
LayoutInflater inflater = MainActivity.this.getLayoutInflater();
View dialogView = inflater.inflate(R.layout.dialog_layout, null);
dialogBuilder.setView(dialogView);

EditText inputEditText = dialogView.findViewById(R.id.dialog_input);
Button okButton = dialogView.findViewById(R.id.dialog_button);

// 设置弹窗按钮点击事件
okButton.setOnClickListener(new View.OnClickListener() {
	@Override
	public void onClick(View v) {
		String userInput = inputEditText.getText().toString();
		// 在这里处理用户输入
		// 例如,可以将输入内容显示在界面上
		// 这里仅仅简单打印用户输入
		System.out.println("User input: " + userInput);
	
		// 关闭弹窗
		alertDialog.dismiss();
	}
});

// 创建并显示弹窗
AlertDialog alertDialog = dialogBuilder.create();
alertDialog.show();

自定义布局 (codeing)

kotlin 复制代码
val builder = AlertDialog.Builder(this)

// LinearLayout 布局容器 - 垂直
val layout = LinearLayout(this)
layout.orientation = LinearLayout.VERTICAL
layout.setPadding(20, 20, 20, 20) // 设置内边距
builder.setTitle("提示")
builder.setView(layout)
val dialog = builder.create()

// TextView
val textView = TextView(this)
val lng: String;
val lat: String
if (point == null){
	lng =  "%.6f".format(tapEvent.mapPoint?.x);
	lat =  "%.6f".format(tapEvent.mapPoint?.y);
	textView.text = "位置: ${lng}, ${lat}"
}else{
	lng = point.lng.toString();
	lat = point.lat.toString();
	textView.text = "样点: ${point.name}"
}
textView.setPadding(30, 20, 0, 20) // 文本与下元素之间的间距
textView.textAlignment = View.TEXT_ALIGNMENT_CENTER // 文本居中
layout.addView(textView)

// LinearLayout 布局容器 - 水平
val buttonLayout = LinearLayout(this)
buttonLayout.orientation = LinearLayout.HORIZONTAL
buttonLayout.weightSum = 2f // 分配权重,使按钮平均分布

// Button
val button1 = Button(this)
button1.text = "导航到此处"
button1.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) // 设置权重
button1.setOnClickListener {
	toNavi(lat,lng);
}
buttonLayout.addView(button1)
// Button
val button2 = Button(this)
button2.text = "查询图层数据"
button2.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) // 设置权重
button2.setOnClickListener {
	dialog.dismiss()
	lifecycleScope.launch(Dispatchers.Main) {//协程
		showLayerDetail(tapEvent)
	}
}
buttonLayout.addView(button2)
layout.addView(buttonLayout)
//
dialog.show()

一个 UI loading 动画库

kotlin 复制代码
dependencies {
   ...
  compile 'com.wang.avi:library:2.1.3'
  
}

github项目地址

layout_sample_view_load.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?android:attr/colorButtonNormal"
    android:orientation="vertical">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:gravity="center">
        <com.wang.avi.AVLoadingIndicatorView
            android:id="@+id/avi"
            style="@style/AVLoadingIndicatorView.Large"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:orientation="vertical">
    </LinearLayout>

</LinearLayout>

SampleViewActivity.java

java 复制代码
public class SampleViewActivity extends AppCompatActivity implements SampleCallback {
    private static final String TAG =  "SampleViewActivity";
    private AVLoadingIndicatorView avi;
    /**
     * 样品 信息 是否 从本地
     */
    private boolean fromLocal = false;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE); //去除这个Activity的标题栏
        setContentView(R.layout.layout_sample_view_load);
    ...
    // 耗时完成后调用 动态加载最终布局
    public void accept(final SampleBean bean) {
        LayoutInflater layout=this.getLayoutInflater();
        View view=layout.inflate(R.layout.layout_sample_view, null);
        setContentView(view);
        Button button = (Button)view.findViewById(R.id.but_tip);
    }