Android快速入门
距离上一次写Android代码已经过去了三四年了,基本上都忘得差不多了,最近在尝试使用kotlin开发一个App,发现查询很多资料都不太全,浪费了大量的时间,因此决定整理一篇在Android入门中经常遇见的需求和问题,希望能对刚接触Android的朋友有一点帮助。
本文包含大量的参考链接,可以先看一下文章的总结,如果有疑惑再点击参考链接,否则跳转太多可能会影响学习效率。
此外还包含大量的示例代码,大部分使用kotlin编写,可以参考 koltin基础教程
本文应该会陆续更新一段时间,直至把这段时间的Android学习任务搞完。
参考
- Android入门基础,文章有点老了
- Android四大组件
UI布局 
Android主流布局是xml,当然也可以直接使用代码创建view,此外目前的jetpack compose使用的声明式布局已经很流行了,未来可能是它和swift ui的天下,不过对于我们初学者,了解下xml布局也还是有点用的。
相比较之前iOS只能通过代码生成UIView的情形,安卓的xml布局已经是比较大的体验改善了。
三大基础布局 
参考:https://www.jianshu.com/p/422c55f1f08e
- 线性布局,LinearLayout是在父布局中把所有的子控件按线性排列,有按行和列两种排列方式
- 相对布局,RelativeLayout布局中,子控件采用相互参照的方式确定自身的位置。可以采用相对于父控件或者其他子控件的方式,使用方式较灵活
- frame布局,FrameLayout中子控件开始全部堆砌在父视图的左上角,通过设置子控件的属性gravity来调整位置。
如果之前先有web开发经验(比如我自己),可能第一思维是使用线性布局模仿文档流,但实际上相对布局可以减少标签嵌套,需要改变一下思维
ConstraintLayout 
参考
- https://developer.android.com/training/constraint-layout?hl=zh-cn ,官方文档,里面的视频演示还不错,基本上看一下就能了解大概了
- https://juejin.cn/post/6844903783294566414 ,图文教程使用约束布局创建一个登陆界面
可以使用约束在一个无嵌套的标签列表中编写复杂的UI布局,缺点是需要大概率需要借助可视化编辑器,否则手写会十分非常麻烦,同时需要记得在布局之前先把view的id给取好名称,不然后续再从各个约束依赖里面修改id,非常痛苦
动态创建view 
在大多数时候,页面都不会是纯静态的,需要我们根据数据动态添加view,比如渲染一个列表之类的
val badgeContainer = v.findViewById<FrameLayout>(R.id.badgeContainer) // 获取父节点
for (item in list) {
    val iv = ImageView(activity) // 传入context
    val size = 10
    val params = FrameLayout.LayoutParams(size, size) // 这里需要对应的父节点的LayoutParams
    iv.layoutParams = params
    iv.translationX = item.x
    iv.translationY = item.y
    iv.setImageResource(R.drawable.circle_shape)
    iv.adjustViewBounds = true
    badgeContainer.addView(iv)
}首先生成view,然后调用父节点的addView就可以了
当然系统内置的很多组件提供了传入动态数据的接口,如ListItemAdapter等。
组合view 
在某些需要将系统view近一步封装时,除了fragment,也可以使用组合view
下面演示一个自定义titleBar组件的UI
首先定义xml布局模板
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/titleBar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="horizontal">
    <TextView
        android:id="@+id/titleBarLeft"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="返回" />
    <TextView
        android:id="@+id/titleBarMid"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textAlignment="center"
        android:text="标题" />
    <TextView
        android:id="@+id/titleBarRight"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="右侧" />
</LinearLayout>如果我们期望传入一些类似于Prop的配置参数,需要先定义
在res/values目录下创建一个attrs_title_bar.xml的文件,下面定义了一个title_text
<resources>
    <declare-styleable name="TitleBar">
        <attr name="title_text" format="string" />
    </declare-styleable>
</resources>然后编写一个继承自根节点LinearLayout的view,完成初始化逻辑
- 初始化参数
- 初始化view
class TitleBar @JvmOverloads
constructor(
    private val ctx: Context,
    private val attributeSet: AttributeSet? = null,
    private val defStyleAttr: Int = 0
) : LinearLayout(ctx, attributeSet, defStyleAttr) {
    lateinit var titleView: TextView
    var title: String = "default title"
    init {
        initAttrs()
        initView()
    }
    private fun initAttrs() {
        val mTypedArray = context.obtainStyledAttributes(attributeSet, R.styleable.TitleBar)
        title = mTypedArray.getString(R.styleable.TitleBar_title_text).toString();
        mTypedArray.recycle()
    }
    private fun initView() {
        LayoutInflater.from(ctx).inflate(R.layout.view_titlebar, this, true);
        titleView = findViewById(R.id.titleBarMid)
        titleView.setText(title)
    }
}最后,就可以在其他布局中使用啦
<com.example.test.widget.TitleBar
        android:layout_width="match_parent"
        android:layout_height="40dp"
        app:title_text="hello"
        >自定义View 
参考:https://blog.csdn.net/aigestudio/article/details/41799811,爱哥的自定义控件系列,虽然时间有点久了,仍值得阅读。
自定义view,看起来就是直接控制底层的绘制API,把无法通过系统内置组件实现的UI给绘制出来,初学先不深究。
动画效果 
参考:https://blog.csdn.net/carson_ho/article/details/72827747
通过startAnimation方法
Button mButton = (Button) findViewById(R.id.button_head);
Animation translateAnimation = new TranslateAnimation(0, 500, 0, 0);
// 步骤2:创建平移动画的对象:平移动画对应的Animation子类为TranslateAnimation
// 参数分别是:
// 1. fromXDelta :视图在水平方向x 移动的起始值
// 2. toXDelta :视图在水平方向x 移动的结束值
// 3. fromYDelta :视图在竖直方向y 移动的起始值
// 4. toYDelta:视图在竖直方向y 移动的结束值
translateAnimation.setRepeatCount(-1);
translateAnimation.setDuration(3000);
// 固定属性的设置都是在其属性前加“set”,如setDuration()
mButton.startAnimation(translateAnimation);如果需要实现复杂动画,也可以考虑使用lottie的动画库,初学先不深究。
Activity相关 
activity生命周期 

class LifeActivity : AppCompatActivity() {
    private val Tag: String = "LifeActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_life)
    }
    override fun onStart() {
        super.onStart()
        Log.i(Tag, "onStart")
    }
    override fun onResume() {
        super.onResume()
        Log.i(Tag, "onResume")
    }
    override fun onPause() {
        super.onPause()
        Log.i(Tag, "onPause")
    }
    override fun onStop() {
        super.onStop()
        Log.i(Tag, "onStop")
    }
    override fun onDestroy() {
        super.onDestroy()
        Log.i(Tag, "onDestroy")
    }
}需要注意的一些细节
点击后退键返回上一页,调用onPause、onStop和onDestroy`,当前activity被销毁,相当于告诉系统:不需要这个activity了
点击home键,调用onPause和onStop,相当于告诉系统:我切出去看看其他的,稍后可能回来,因此不会调用onDestroy,但是需要注意系统在低内存时可能会销毁这些被停止的activity
设备旋转时,会销毁当前activty,然后重新创建一个,因此如果需要保存数据,则可以通过onSaveInstanceState将数据保存在bundle中,这样,新的activity在onCreate的时候可以拿到对应的bundle并恢复到对应状态
只有调用了onStop之后的activity才会被销毁,在此之前,会先调用onSaveInstanceState,通过系统将对应的activity暂存记录保存起来,下次activity再onCreate时可以使用暂存记录重新创建。
private val key1: String = "key1"
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putInt(key1, 1)
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_life)
    if(savedInstanceState!=null){
        val val1 = savedInstanceState.getInt(key1)
    }
}activity携带参数跳转 
通过intent跳转到新的activity,同时可以通过Bundle携带参数
// activity1
fun toPage(){
    val intent = Intent(this@Activity1, Activity2::class.java)
    val bundle = Bundle()
    // 携带相关的参数过去,上报数据的时候一起提交
    bundle.putString("driverName", inputDriverName.text.toString())
    bundle.putString("trainCode", spinnerTrainCode.selectedItem.toString())
    bundle.putBoolean("isPreviewTrain", previewToggle.isChecked)
    intent.putExtras(bundle)
    startActivity(intent)
}然后在目标Activity2中,可以通过intent属性获取到对应的数据
// activity2
// 获取初始化参数
driverName = intent.getStringExtra("driverName")
trainCode = intent.getStringExtra("trainCode")
isPreviewTrain = intent.getBooleanExtra("isPreviewTrain", true)使用fragment 
显然,每个页面都使用Activity进行构建是可以的,但是肯定存在多个Activity复用一些布局和逻辑的场景,这时候可以使用fragment
参考
- https://developer.android.com/guide/components/fragments?hl=zh-cn 官方文档
- https://www.ituring.com.cn/book/tupubarticle/16546 android编程权威指南
- https://developer.android.com/training/basics/fragments/communicating?hl=zh-cn , fragment和activity的通信
首先需要定义fragment
先来个布局,类似于一个小型的layout xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!--中间是其他布局-->
</RelativeLayout>然后定义Fragment子类,需要完成绑定布局,定义初始化参数等逻辑
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
class DemoFragment : Fragment() {
    private var param1: Boolean = true
    private var param2: String? = null
    private lateinit var imageTrain: ImageView
    private lateinit var badgeContainer: FrameLayout
    private lateinit var callback: OnTrainListener
    // 转换初始化参数
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getBoolean(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 绑定布局
        val v: View = inflater.inflate(R.layout.fragment_demo, container, false)
        return v
    }
    // 扩展一个newInstance方法,用于接收参数
    companion object {
        @JvmStatic
        fun newInstance(param1: Boolean, param2: String) =
            TrainFragment().apply {
                arguments = Bundle().apply {
                    putBoolean(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}fragment的生命周期与activity的方法类似,一个关键区别就在于,fragment的生命周期方法由托管activity而不是操作系统调用

然后就可以在activity中使用fragment了
首先在layout的xml中留一个占位标签
  <FrameLayout
        android:id="@+id/fragmentContainer"
        android:layout_width="match_parent"
        android:layout_height="300dp" />然后在onCreate中通过FragmentManager添加fragment,可以通过上面的newInstance传入初始化参数
private var fragment: Fragment?
private fun initFragment() {
    val fm: FragmentManager = supportFragmentManager
    fragment: Fragment? = fm.findFragmentById(R.id.trainContainer)
    if (fragment == null) {
        fragment = DemoFragment.newInstance(isPreviewTrain, "p2")
        fm.beginTransaction()
            .add(R.id.trainContainer, fragment)
            .commit()
    }
}通过构造参数可以完成activity向fragment的通信,如果fragment需要通知activity,可以通过约定接口来实现,大致实现为:通过在fragment中定义接口,在activity中实现接口,然后由fragment获取activity的实例执行即可
// fragment
class DemoFragment : Fragment() {
    // 暴露接口
    interface OnDemoListener {
        fun onBtnClick(params: String)
    }
    // 提供一个接口,用于获取activity实例
    fun setOnTrainListener(callback: OnTrainListener) {
        this.callback = callback
    }
    // 调用activity实例的方法
    fun btnClick(){
        this.callback.onBtnClick("test")
    }
}
// acitivty
class MainActivity : AppCompatActivity(), DemoFragment.OnDemoListener {
    // 添加fragment的时候将自己暴露给fragment
    override fun onAttachFragment(fragment: Fragment) {
        if (fragment is TrainFragment) {
            fragment.setOnTrainListener(this)
        }
    }
    // 实现OnDemoListener相关的接口
    override fun onBtnClick() {
        // 做点事情
    }
}消息通信机制 
在初学的时候经常遇见在非UI线程中操作UI然后导致App崩掉的问题,要一劳永逸地解决这个问题,需要先了解Android中的消息机制。Android有两种消息机制
- 组件间消息:Intent 机制
- 线程间消息:Message 机制
Intent:组件间通信 
Intent 是一个消息传递对象,主要用来从其他应用组件请求操作,在系统的各种交互都可以理解为操作,如打开文件选择题,打开摄像头拍照之类的,
下面展示一个通过intent选择文件的例子,实际上就是通过隐式intent打开了文件选择Activity
fun pickFile() {
    val intent = Intent(Intent.ACTION_GET_CONTENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.type = "*/*"
    this.startActivityForResult(intent, REQUEST_CODE)
}
// 获取文件的真实路径
override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (data == null) {
        // 用户未选择任何文件,直接返回
        return
    }
    val uri: Uri? = data.data // 获取用户选择文件的URI
    if(uri == null) {
        return
    }
    // 通过ContentProvider查询文件路径
    val resolver = this.contentResolver
    val cursor: Cursor? = resolver.query(uri, null, null, null, null)
    if (cursor == null) {
        // 未查询到,说明为普通文件,可直接通过URI获取文件路径
        val path: String? = uri.getPath()
        return
    }
    if (cursor.moveToFirst()) {
        // 多媒体文件,从数据库中获取文件的真实路径
        val path: String = cursor.getString(cursor.getColumnIndex("_data"))
        // todo 获取path之后,就可以拿去上传图片了
    }
    cursor.close()
}Message:线程通信 
参考
- https://mp.weixin.qq.com/s/bi42PKoLMy4uv9qEWSK-jA
- https://givemepass.blogspot.com/2015/10/thread-2_14.html
- https://blog.csdn.net/qian520ao/article/details/78262289 写的很好,自顶向下介绍了相关的概念
JS是单线程的,因此在前端开发中很少接触到线程的概念,而Android是多线程的,分为主线程(UI线程)和子线程,Handler 是用来切换线程的

消息机制的工作流程分三步进行:
1、Handler 调用 sendMessage 将 Message 入队到 MessageQueue 中;
2、Looper 类的 loop() 方法中无限循环,从 MessageQueue 中取出 Message ,再交回给 Handler ;
3、Handler 调用 dispatchMessage 分发处理消息。
具体的使用流程大致为
// 先在当前线程创建一个handler,同时注册消息处理方法
private val hanlder = Handler {  
 val b = when (it.what) {  
  1 -> true  
  else -> false  
 }  
 b  
}
// ... 当前线程有个messqgeQueue,循环从队列中取出消息通知handler
// 开启新线程,内部使用hander发送消息通知前面的线程
Thread{  
 val msg = Message()  
 msg.what = 1  
 hanlder.sendMessage(msg)  
}回头来看,子线程与UI线程的通信有下面几种方式,参考:https://blog.csdn.net/liugec/article/details/78731626
- Activity.runOnUIThread(Runnable)
- View.Post(Runnable)和View.PostDelayed(Runnabe,long)
- AsyncTask
- Handler.Post(Runnabe)和Handler.PostDelayed(Runnabe,long)
实际上activity的runOnUiThread或者view.post就是对于handler的封装
runOnUiThread,顾名思义,在UI线程执行
Thread{  
 // 子线程处理逻辑
 runOnUiThread{  
   // 更新UI线程
 }  
}.start()view.post,常用来在onCreate中获取view尺寸,也可以在子线程中调用用来更新UI
view.post()  // 会切换到主线程的mHandler此外,也可以自定义looper
- Looper.prepare,之后创建的Handler会跟这个自定义的looper绑定,
- Looper.loop()开始循环获取消息队列中的消息
- Looper.myLooper()?.quit()退出消息队列,Thread才会向后面执行
因此下面的代码输出为A、B1、B2、C、D
Thread {
    Log.e(TAG, "A")
    Looper.prepare()
    Handler().post{
        Log.e(TAG, "B1")
    }
    Handler().post{
        Log.e(TAG, "B2")
        Looper.myLooper()?.quit()
    }
    Looper.loop()
    Log.e(TAG, "C")
    runOnUiThread {
        Log.e(TAG, "D")
    }
}.start()Handler创建的时候会采用当前线程的Looper来构造消息循环系统,Looper在哪个线程创建,就跟哪个线程绑定,并且Handler是在他关联的Looper对应的线程中处理消息的。(敲黑板) 那么Handler内部如何获取到当前线程的Looper呢—–ThreadLocal。ThreadLocal可以在不同的线程中互不干扰的存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper。当然需要注意的是 ①线程是默认没有Looper的,如果需要使用Handler,就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread, ②ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。
网络相关 
okhttp网络请求 
参考:okhttp
首先添加依赖,最新的已经出到v4版本了,这里还是使用的v3.14版本
dependencies {
    implementation 'com.squareup.okhttp3:okhttp:3.14.4'
}然后封装相关的请求
object ApiUtil {
    private val host = "http://192.168.0.4:7001"
    // 基础的请求
    fun sendRequest(callback: Callback) {
        val client = OkHttpClient()
        val request = Request.Builder()
            .url("${host}/api/feedback")
            .build()
        client.newCall(request).enqueue(callback)
    }
    // 上传文件
    fun uploadFile(filePath: String, onSuccess: (String) -> Unit) {
        val url = "${host}/api/upload"
        val file = File(filePath) // 图片和视频都可以使用File进行上传
        val fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), file)
        val requestBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("file", file.name, fileBody)
            .build()
        val request = Request.Builder()
            .url(url)
            .post(requestBody)
            .build()
        val httpBuilder = OkHttpClient.Builder()
        val okHttpClient = httpBuilder
            .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
            .writeTimeout(120, java.util.concurrent.TimeUnit.SECONDS)
            .build()
        okHttpClient.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.i("shymean", "upload file error, ${e}")
            }
            override fun onResponse(call: Call, response: Response) {
                var json = response.body()?.string()
                // 通过gson转换成bean
                val res = Gson().fromJson(json, UploadResponse::class.java)
                if (res.data[0] != null) {
                    onSuccess(res.data[0].url)
                }
            }
        })
    }
    // 提交json
    fun submitReport(params: JSONObject, onSuccess: (String) -> Unit) {
        val okHttpClient = OkHttpClient()
        val requestBody: RequestBody =
            RequestBody.create(MediaType.parse("application/json"), params.toString())
        val request = Request.Builder()
            .url("${host}/api/feedback")
            .post(requestBody)
            .build()
        okHttpClient.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.i("shymean", "submitReport error, ${e}")
            }
            override fun onResponse(call: Call, response: Response) {
                val data = response.body()?.string()
                if (data != null) {
                    onSuccess(data)
                }
            }
        })
    }
}一般情况下需要对响应进行json序列化,可以使用诸如gson等工具
允许http访问 
在高版本Android上使用okhttp发送http请求会报错误
java.net.UnknownServiceException: CLEARTEXT communication ** not permitted by network security policy这是因为 Android P 以上版本将禁止 App 使用所有未加密的连接,如果需要强制开启http,可以参考如下步骤
在res目录下新建一个xml名称的目录,然后创建network_security_config.xml文件
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
 <base-config cleartextTrafficPermitted="true" />
</network-security-config>然后在APP的AndroidManifest.xml文件下的application标签增加以下属性
<application
 ...其他属性
 android:networkSecurityConfig="@xml/network_security_config"/>JSON序列化 
json转换成kotlin可以使用诸如json2kotlin之类的工具
首先准备需要转换的json模板,粘贴到输入框

点击生成按钮,即可获得kotlin代码
package com.laotang.train
import com.google.gson.annotations.SerializedName
data class UploadResult(
    @SerializedName("url") val url: String
)
data class UploadResponse(
    @SerializedName("code") val code: Int,
    @SerializedName("msg") val msg: String,
    @SerializedName("data") val data: List<UploadResult>
)然后就可以将json字符串序列化成对象了
var json = response.body()?.string() // 获取json字符串
// 通过gson转换成bean
val res = Gson().fromJson(json, UploadResponse::class.java)
if (res.data[0] != null) {
    onSuccess(res.data[0].url)
}常用工具 
gradle 
升级as之后可能需要更新gradle,使用AS默认下载有时候会很慢,可以从网上下载后直接导入
参考:https://blog.csdn.net/u011046452/article/details/107529346
主要步骤
- 从官网下载压缩包,如gradle-6.1.1.1-all.zip,gradle包官网地址
- 找到.gradle下面的未完成下载的目录/Users/bcz/.gradle/wrapper/dists/gradle-6.1.1-all,里面应该有个类似于hash之类的文件夹
- 将刚才下载的zip移动到这个hash名称文件夹,然后解压
- 重启as即可
adb 
adb学名安卓调试桥,是一个开发debug工具,有各种方便的操作
mac上述使用brew 安装
brew cask install android-platform-tools然后就可以使用adb install直接向当前连接的手机安装本地包了
如果安装时出现 Failure [INSTALL_FAILED_TEST_ONLY]错误,则需要加上-t参数
adb install -t xx.apk有些时候,emulator卡住了,甚至无法通过活动管理器关闭,可以使用adb进行操作
adb emu kill打包 
参考: https://blog.csdn.net/CC1991_/article/details/103285684
1、打开Android Studio,进入需要打包apk的项目工程; 2、找到Android Studio顶部菜单栏里面的Build选项,点击”Generate Signed Bundle/APK…”选项进入; 3、进入Generate Signed Bundle or APK选项,选择 jks文件路径,如果没有jks文件,可以直接在下面的Create new选项里面新建jks文件;如果已经新建有jks文件,就直接选择对应的jks文件即可。接着输入密钥密码、密钥别名、公钥密码,确认无误之后,点击Next; 4、进入选择生成apk导出的文件路径,然后选择apk的模式:release,勾选下面的V1 和 V2,二者缺一不可,选择无误之后,点击Finish按钮即可开始打包apk; 5、进过短暂的等待之后,在右下角会提示一个弹框,提示打包apk成功,那么根据第4不步选择的apk生成导出的文件夹就可以看到打包好的apk文件了。
小结 
作为一个前端,在学习iOS和Android开发的时候,才发现浏览器把底层功能都给封装了,web很难接触到底层编码,下面是在开发时遇见的一个关于chrome无法播放Android录像的问题
使用MediaRecorder 录制视频上传后,发现在本地播放器可以预览视频,但是在mac chrome等浏览器上面不行,Android手机浏览器可以预览,android微信浏览器也无法预览
查了一番,发现原来浏览器video标签是有编码要求的,其标准是用H.264方式编码视频的MP4文件,而我在录像的时候使用的是MediaRecorder.VideoEncoder.MPEG_4_SP编码,导致录制出来的视频编码不是h264

由于编码不正确,因此video标签无法展示对应视频,测试一下,将对应的视频用ffmpeg转码
ffmpeg -i input.mp4 -strict -2 output.mp4之后就可以在Chrome中正常访问了,因此需要在Android录制视频结束后调整编码。
从上面这个问题可以看出了解底层开发的必要性,在此之前,作为一个写了几年的前端,我甚至不知道video标签居然是有编码问题的!!
本文主要整理了入门Android开发初期时需要接触到的概念和常见问题,能把这些问题解决了,开始上手编写项目应该是没啥问题了。
本来还想去各大培训学校官网看看Android培训大纲,发现很多学校貌似都没有相关的岗位了,难道Android开发要消失了!!!不可能的哈哈,扩宽知识面是很重要的,原生开发学起来~
当入门了Android基础开发之后要学什么?逆向、底层、C++扩展,还有一堆东西呢
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
