我感覺我學了一個假的Android...

2021-12-19 鴻洋

收錄於話題 #原創經典 34個內容

大家好,我是鴻洋。

上個周末是雙休,我決定來顛覆一下大家的認知。

在平時的Android開發中,如果一個新手遇到一個這樣的錯:

android.view.ViewRootImpl$CalledFromWrongThreadException: 
Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8066)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1297)
        at android.view.View.requestLayout(View.java:23147)

你作為一隻老鳥,嘴角露出一絲微笑:

「小兄弟,你這個是沒有在UI線程執行UI操作導致的錯誤,你搞個UI線程的handler.post一下就好了」。

但是...

我今天要說,真是是只有UI線程才能更新UI嗎?

你作為一隻老鳥,肯定立馬腦子裡閃過:

我知道你這文章寫啥了,又要在Activity#onCreate,去搞個線程執行TextView#setText,然後發現更新成功了,是不是?

這多年以前我就看過這樣的文章,ViewRootImpl還沒創建而已。

看你們這麼強,我這個文章沒法寫下去了...

但是我這個人專治各種不服好吧,我換個問題:

UI線程更新UI就不會出現上面的錯誤了嗎?

好了,開講。

下面是一個應屆小哥小奇寫需求的故事。

注意本文代碼為應屆小哥角度所寫,為了引出問題及原理,不要隨意參考,另外如果嘗試復現相關代碼,務必看好每一個字符,甚至xml裡面的屬性都很關鍵

需求很簡單,就是:

點擊一個按鈕;

Server會下發一個問題,客戶端Dialog展示;

在Dialog交互回答問題;

是不是很簡答。

小哥怒寫一波代碼:

package com.example.testviewrootimpl;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private Button mBtnQuestion;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mBtnQuestion = findViewById(R.id.btn_question);

        mBtnQuestion.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                requestAQuestion();
            }
        });
    }

    private void requestAQuestion() {
        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 模擬伺服器請求,返回問題
                String title = "鴻洋帥氣嗎?";
                showQuestionInDialog(title);
            }
        }.start();
    }

    private void showQuestionInDialog(String title) {

    }
}


很簡單吧,點擊按鈕,新啟動一個線程去模擬網絡請求,結果拿到後,把問題展示在Dialog。

下面開始寫Dialog的代碼:

public class QuestionDialog extends Dialog {

    private TextView mTvTitle;
    private Button mBtnYes;
    private Button mBtnNo;

    public QuestionDialog(@NonNull Context context) {
        super(context);

        setContentView(R.layout.dialog_question);

        mTvTitle = findViewById(R.id.tv_title);
        mBtnYes = findViewById(R.id.btn_yes);
        mBtnNo = findViewById(R.id.btn_no);

    }

    public void show(String title) {
        mTvTitle.setText(title);
        show();
    }
}

很簡答,就一個標題,兩個按鈕。

<RelativeLayout 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">


    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24dp"
        android:textStyle="bold"
        tools:text="鴻洋醜的一匹?鴻洋醜的一匹?鴻洋醜的一匹?鴻洋醜的一匹?" />

    <Button
        android:id="@+id/btn_yes"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_title"
        android:layout_marginTop="10dp"
        android:text="是的"></Button>

    <Button
        android:id="@+id/btn_no"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@id/btn_yes"
        android:layout_alignParentRight="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@id/btn_yes"
        android:text="不是"></Button>

</RelativeLayout>

然後我們在showQuestionInDialog讓它show出來。

private void showQuestionInDialog(String title) {
    QuestionDialog questionDialog = new QuestionDialog(this);
    questionDialog.show(title);
}

你們猜結果怎麼著...

崩潰了...

應屆生小齊迎來了第一次工作中的崩潰...

我們先停下來。

上面的代碼很簡單吧,那麼我想問各位為什麼會崩潰呢?憑各位多年的經驗。

猜想:

new Thread(){

    puublic void run(){
        show("...");
    }

}

public void show(String title) {
    mTvTitle.setText(title);
    show();
}


上面new Thread模擬數據,沒有切到UI線程就show Dialog了,而且執行了TextView#setText,肯定是在非UI線程更新UI導致的。

很有道理,絕不是一個人會這麼猜測吧。

下面我們看真正報錯的原因:

Process: com.example.testviewrootimpl, PID: 10544
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
    at android.os.Handler.<init>(Handler.java:207)
    at android.os.Handler.<init>(Handler.java:119)
    at android.app.Dialog.<init>(Dialog.java:133)
    at android.app.Dialog.<init>(Dialog.java:162)
    at com.example.testviewrootimpl.QuestionDialog.<init>(QuestionDialog.java:17)
    at com.example.testviewrootimpl.MainActivity.showQuestionInDialog(MainActivity.java:46)
    at com.example.testviewrootimpl.MainActivity.access$100(MainActivity.java:10)
    at com.example.testviewrootimpl.MainActivity$2.run(MainActivity.java:40)

Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()

雖然猜錯了,但是依舊有點熟悉的感覺,以前大家在子線程彈toast的時候是不是見過類似的錯誤。

作為一個老鳥,遇到這個問題,肯定是不在UI線程彈Dialog,但是應屆小哥處理問題的方式就不同了。

小哥,直接把報錯信息扔進Google,不,百度:

點開第一篇CSDN的博客:

然後迅速舉一反三,在剛才show Dialog的方法中增加:

private void showQuestionInDialog(String title) {
    Looper.prepare(); // 增加部分
    QuestionDialog questionDialog = new QuestionDialog(this);
    questionDialog.show(title);
    Looper.loop(); // 增加部分
}


解決問題就是這麼簡單,嘴角露出一絲對自己滿意的笑容。

再次運行App...

這裡大家再停一下。

憑各位多年的經驗,我想再問一句,這次還會崩潰嗎?

會嗎?

猜想:

這代碼治標不治本,還是沒有在UI線程執行相關代碼,還是會崩,而卻剛才的show裡面還有TextView#setText操作

有點道理。

看一下運行效果:

沒有崩潰...

是不是有一絲的鬱悶?

沒關係,作為擁有多年經驗的老鳥,總能立馬想到解釋的理由:

大家都知道在Activity#onCreate的時候,我們開個線程去執行Text#setText也不會崩潰,原因是ViewRootImpl那時候還沒初始化,所以這次沒崩潰也是這個原因。

對應源碼解釋是這樣的:

# Dialog源碼
public void show() {

    // 省略一堆代碼
    mWindowManager.addView(mDecor, l);
}


我們首次創建的Dialog,第一次調用show方法,內部確實會執行mWindowManager.addView,這個代碼會執行到:

# WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}


這個mGlobal對象是WindowManagerGlobal,我們看它的addView方法:

# WindowManagerGlobal 
public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow) {
    // 省略了一堆代碼
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        if (index >= 0) {
            removeViewLocked(index, true);
        }
        throw e;
    }
}

果然立馬有new ViewRootImpl的代碼,你看ViewRootImpl沒有創建,所以這和Activity那個是一個情況。

好像有那麼點道理哈...

我們繼續往下看。

應屆小哥要繼續做需求了。

接下來的需求很奇怪,就是當詢問"鴻洋帥氣嗎?"的時候,如果你點擊不是,那麼Dialog不消失,在問題的末尾再加一個?號,如此循環,永不關閉。

這難不倒我們的小哥:

mBtnNo.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View view) {

        String s = mTvTitle.getText().toString();
        mTvTitle.setText(s+"?");
    }
});

運行效果:

很完美。

如果我問,你覺得這個代碼有問題嗎?

你往上看了幾眼,就這兩行代碼有個雞兒問題,可能有空指針?

當然不是。

我稍微修改一下代碼:

mBtnNo.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        String s = mTvTitle.getText().toString();
        mTvTitle.setText(s+"?");


        boolean uiThread = Looper.myLooper() == Looper.getMainLooper();
        Toast.makeText(getContext(),"Ui thread = " + uiThread , Toast.LENGTH_LONG).show();
    }
});


每次點擊的時候,我彈了個Toast,輸出當前線程是不是UI線程。

看下效果:

發現問題了嗎?

出乎自己的意料嗎?

我們在非UI線程一直在更新TextView的text。

這個時候,你不能跟我扯什麼ViewRootImpl還沒有創建了吧?

別急...

還有更刺激的。


我再改一下代碼:

private Handler sUiHandler = new Handler(Looper.getMainLooper());

public QuestionDialog(@NonNull Context context) {
    super(context);

    setContentView(R.layout.dialog_question);


    mBtnNo.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            sUiHandler.post(new Runnable() {
                @Override
                public void run() {
                    String s = mTvTitle.getText().toString();
                    mTvTitle.setText(s+"?");
                }
            });
        }
    });

}

我搞了個UI線程的handler,然後post一下Runnable,確保我們的TextView#setText在UI線程執行,嚴謹而又優雅。

再停一下,以各位多年經驗,這次會崩潰嗎?

按照我寫博客的套路,這次肯定是演示崩潰呀,不然博客怎麼往下寫。

好像是這個道理...

我們跑一下效果:

點擊了幾下,沒崩...


作為擁有多年經驗的老鳥,總能立馬想到解釋的理由:

UI線程更新怎麼會崩潰呀(言語中有一絲不自信)。

是嗎?

我們多點擊幾次:

崩潰了...

但是剛才在沒有添加UiHandler.post之前可沒有崩潰喲。

這個結果,我都得把代碼露出來了,怕你們說我演你們...

好了,再停一停。

我又要問大家一個問題了,這次你猜是什麼崩潰?

是不是求我別搞你們了,直接揭秘吧。

com.example.testviewrootimpl E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.testviewrootimpl, PID: 18323
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8188)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1421)
        at android.view.View.requestLayout(View.java:24434)
        at android.view.View.requestLayout(View.java:24434)
        at android.view.View.requestLayout(View.java:24434)
        at android.view.View.requestLayout(View.java:24434)
        at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:380)
        at android.view.View.requestLayout(View.java:24434)
        at android.widget.TextView.checkForRelayout(TextView.java:9667)
        at android.widget.TextView.setText(TextView.java:6261)
        at android.widget.TextView.setText(TextView.java:6089)
        at android.widget.TextView.setText(TextView.java:6041)
        at com.example.testviewrootimpl.QuestionDialog$1$1.run(QuestionDialog.java:38)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7319)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:934)

那個熟悉的身影回來了:

Only the original thread that created a view hierarchy can touch its views.

但是!

但是!

這次可是在切換到UI線程拋出來的。

對應我開頭的靈魂拷問:

UI線程更新UI就不會出現上面的錯誤了嗎?

是不是在一股懵逼又刺激的感覺中無法自拔...

還有更刺激的事情...嗯,篇幅問題,本篇我們就到這了,更刺激的事情我們下次再寫。

別怕,沒完,我總得告訴你們為什麼吧。

其實這一切的根源都在於我們長久的一個錯誤的概念。

注意下面每一句話都很關鍵,請降低閱讀速度。

就是UI線程才能UI線程,這是不對的,為什麼這麼說呢?

Only the original thread that created a view hierarchy can touch its views.

這個異常是在ViewRootImpl裡面拋出的對吧,我們再次來審視一下這段代碼:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

其實就幾行代碼。

我們仔細看一下,他這個錯誤信息並不是:

Only the UI Thread ... 而是 Only the original thread。

對吧,如果真的想強制為Only the Ui Thread,上面的if語句應該寫成:

if(UI Thread != Thread.currentThread()){}

而不是mThread。

根本原因說完了。

我再帶大家看下源碼解析:

這個mThread是什麼?

是ViewRootImpl的成員變量,我們重點應該關注它什麼時候賦值的:


public ViewRootImpl(Context context, Display display) {
    mContext = context;

    mThread = Thread.currentThread();

}

在ViewRootImpl構造的時候賦值的,賦值的就是當前的Thread對象。

也就是說,你ViewRootImpl在哪個線程創建的,你後續的UI更新就需要在哪個線程執行,跟是不是UI線程毫無關係。

對應到上面的例子,我們中間也有段貼源碼的地方。

恰好說明了:

Dialog的ViewRootImpl,其實是在執行show()方法的時候創建的,而我們的Dialog的show放在子線程裡面,所以導致後續View更新,執行到ViewRootImpl#checkThread的時候,都在子線程才可以。

這就說明了,為什麼我們剛才切到UI線程去執行TextView#setText為啥崩了。

這裡有個思考題,注意我們上面演示的時候,切到UI線程執行setText沒有立馬崩潰,而是執行了好幾次之後才崩潰的,為什麼呢?自己想。

大家可能還有個一問題:

ViewRootImpl怎麼和View關聯起來的

其實我們看報錯堆棧很好找到相關代碼:

com.example.testviewrootimpl E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.testviewrootimpl, PID: 18323
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8188)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1421)
        at android.view.View.requestLayout(View.java:24434)

報錯的堆棧都是由View.requestLayout觸發到ViewRootImpl的。

我們直接看這個方法:

public void requestLayout() {

    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
}

注意裡面這個mParent變量,它的類型是ViewParent接口。

見名知意。

我要問你一個View的mParent是什麼,你肯定會回答是它的父View,也就是個ViewGroup。

對,沒錯。

public abstract class ViewGroup 
extends View 
implements ViewParent{}

ViewGroup確實實現了ViewParent接口。

但是還有個問題,一個界面的最最最上面那個ViewGroup它的mParent是誰?


對吧,總不能還是ViewGroup吧,那豈不是沒完沒了了。

所以,ViewParent還有另外一個實現類,叫做ViewRootImpl。

現在明白了吧。

按照ViewParent的體系,我們的界面結構是這樣的。

嗯,我還是寫坨代碼吧:

還是剛才Dialog,當我們點擊No的時候,我們列印下ViewParent體系:

mBtnNo.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        printViewParentHierarchy(mTvTitle, 0);

    }
});

private void printViewParentHierarchy(Object view, int level) {
    if (view == null) {
        return;
    }
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < level; i++) {
        sb.append("\t");
    }
    sb.append(view.getClass().getSimpleName());
    Log.d("lmj", sb.toString());

    if (view instanceof View) {
        printViewParentHierarchy(((View) view).getParent(), level + 1);
    }

}

很簡單,我們就列印mTbTitle,一直往上的ViewParent體系。

D/lmj: AppCompatTextView
D/lmj:  RelativeLayout
D/lmj:      FrameLayout
D/lmj:          FrameLayout
D/lmj:              DecorView
D/lmj:                  ViewRootImpl

看到沒,最底部的是誰。

是它,是它,就是它,我們的ViewRootImpl。

所以當你的TextView觸發requestLayout,會輾轉到ViewRootImpl的requestLayout,然後再到它的checkThread,而checkThread判斷的並非是UI線程和當前線程對比,而是mThread和當前線程對比。

到這裡,我可以結尾了吧。

最後,就一件事,如果有收穫,閱讀本文過程中腦子裡閃過臥槽的,可以點個再看,轉發一波,也算我花了這兩天是值得的。

讚賞就算了,我還是自己多接幾個廣告養自己吧。

下一篇我可能要寫:Google好像在秀我們,歡迎關注等文,具體時間未定,思路暫無。

再留個思考題:這篇文章我們以Dialog為案例,你還能想到別的案例嗎?

本文測試設備:Android 29模擬器。

本篇為一個悲傷的故事,始終沒有人按下是,留下來的只有更多的問號。

推薦閱讀:

掃一掃 關注我的公眾號

如果你想要跟大家分享你的文章,歡迎投稿~

┏(^0^)┛明天見!

相關焦點

  • Android | 爆肝兩天!我寫了一個支持圓角、描邊的UI庫
    我為什麼要寫這個庫在 Android 開發中,如果一個控制項想要實現上面類似的圓角和描邊效果,有如下兩種最為常用的方式。寫 xml 文件,設置圓角背景在 drawable 文件夾下,新建 xml 文件,通過 shape 標籤來設置圓角半徑,描邊寬度和顏色。
  • 他們說 Android Studio 3.0 有坑?我怎麼看到一個驚喜!
    距離 android studio 3.0 穩定版的發布已經有段時間。
  • Android 轉場動畫
    Android L 是Google於2014年升級的系統版本號, 在2015年國內廠商新機就開始推送Android L, 現在是2017年我覺得此時不用更待何時.在android.transition包下提供關於transitionAnimation的過渡框架, Transiton框架是在api19引入, 但是轉場動畫卻是在api21引入.
  • Android JetPack架構篇,一個實戰項目帶你學懂JetPack
    你會學到什麼學會如何使用架構組件庫和生命周期庫設計和構建應用程式。這裡有許多步驟去使用架構組件和推薦的框架。為一個欄位需要是public的或提供get方法。">6dp</item>   <item name="android:paddingLeft">8dp</item></style>添加一個layout/recyclerview_item.xml布局:<?
  • android studio布局嵌套_android studio相對布局和線性布局嵌套...
    利用android studio LinearLayout線性布局設計製作簡易的計算器詳細版【精選收藏】Android Studio簡介Android Studio 是谷歌推出的一個Android集成開發工具
  • 我可能回了一個假家,我可能有一個假媽,但是我想說……
    媽,你不是說你想我了嗎!?起晚被罵不做家務被罵房間太亂被罵出去玩被罵宅在家被罵玩電腦被罵玩手機也被罵我究竟是不是親生的少年看在你骨骼驚奇的份上馬上要過年了這有一份不被嫌棄的武林秘籍傳授於你●●●●●●雖然這幾天來覺得回了一個假家我可能有一個假媽但是看著媽媽在家裡忙碌的身影
  • 這遊戲讓我一度懷疑當年學了假的英語!!!
    氣得我想要重學英語昨天有差友在後臺威脅我
  • 我每次想到你的時候就會感覺很熱歌曲介紹
    我每次想到你的時候就會感覺很熱這首歌很好聽,聽這歌詞大家肯定也都有點感慨,想必大家都很想知道這到底是什麼歌。下面18183小編就為大家帶來了我每次想到你的時候就會感覺很熱歌曲介紹。下面18183小編就為大家帶來了我每次想到你的時候就會感覺很熱歌曲介紹。
  • 對於Android Button,你可能並不了解
    結果在昨天的公眾號留言中,有一位LitePal的熱衷粉絲朋友提前看到了項目主頁文檔的更新,並將最新版的LitePal集成到了自己的項目當中,卻幫我發現了一個驚天大Bug。這裡我要再次感謝這位朋友幫我及時發現了這個Bug,不然就這麼推送的話要造成非常嚴重的事故了。
  • Android 約束布局(ConstraintLayout)詳解
    創建布局接下來,我們創建一個布局,根布局就用ConstraintLayout:<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com
  • Android Notes|BottomNavigationView 愛上 Lottie
    作者:靜心Study,連結:https://juejin.im/post/6867895624025997320前言好容易解決個問題,感覺記錄一波。當日事當日畢,踐行雞老大,點滴積累,萬一某天優秀了呢?
  • Android藍牙自動配對攻略
    溫柔狠角色的博客地址:http://blog.csdn.net/qq_25827845其實藍牙我本人並沒有怎麼接觸過,不過該文對需要使用到藍牙的朋友應該有一定的幫助。藍牙自動配對,即搜索到其它藍牙設備之後直接進行配對,不需要彈出配對確認框或者密鑰輸入框。經過最近一段時間得研究,針對網上給出的案例。總結了一個親測好使的Demo。
  • Android安全防護之旅---幾行代碼讓Android應用變得更加安全
    所以我們還可以這麼做,網絡請求用底層的socket進行訪問操作,這樣就不會被抓包了,可惜的是這世界上還有一個叫做tcpdump+WireShark的工具,也是於事無補。所以我已經瘋了,因為想不到什麼方案了。最後請求數據中一定要記得帶上簽名加密信息,不要一味的放在請求參數中,我偷偷的告訴你很多破解者容易忽視請求頭信息。
  • Android Studio實現音樂播放器(2.0版本)
    如下圖所示:期間有很多同學問到我是否可以加上一首下一首功能,確實可以加,只要獲取到歌曲文件的下標position即可實現此功能,不難。但是因為寫完這個最初版本後就沒有當時那種心境再改了,我想大家都會有這種體會。
  • Android TV開發總結(一)構建一個TV app前要知道的事兒
    出於這個原因,Android系統不支持電視設備有以下特點:HardwareAndroid feature descriptorTouchscreenandroid.hardware.touchscreenTouchscreen emulatorandroid.hardware.faketouchTelephonyandroid.hardware.telephonyCameraandroid.hardware.cameraNear
  • Android:構建一個典型的音樂 App
    其實從技術的角度來看,和實現一個音樂 App 沒有多大差異。為了保證用戶有個良好的收聽體驗,有一些注意事項是我們需要處理的。概要下面列出構建一個典型的音樂 App 需要注意的點,然後我們將一一展開。<intent-filter>  <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter>典型的初始化工作如下:public class MediaPlaybackService extends
  • Android View 動畫
    ><layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"    android:animation="@anim/anim_file"    android:animationOrder="normal"    android:delay="0.5">
  • Android中LayoutAnimation的分析(一)
    "></android.support.v7.widget.RecyclerView>(4)在 res 目錄下新建一個 anim 文件夾,在 anim 目錄下新建一個 layoutAnimation 對應的 xml 文件 layout_animation_fall_down.xml :layout_animation_fall_down.xml
  • 一個Android菜雞的2020之旅
    輸出❝今年輸出的文章差不多16篇,雖然看得人不多,但寫博客已經成為我的一種習慣.總是要在學到東西之後,寫一篇才算完美.看了下我的CSDN,寫的第一篇文章是2016年,那排版,那文筆,不擺了. 雖然寫了幾年博客了,但文章還是很爛,可能還是太菜了吧.
  • Google為Android的「查找我的設備」添加了室內定位
    如果您丟失了Android手機,通常可以使用Google的「查找我的設備」服務來查看手機在地圖上的最後一個已知位置。隨著Google Maps越來越好,有意義的是使「查找我的設備」與最新的地圖技術保持最新。隨著最新的更新,谷歌也加入到了其丟失的設備服務室內測繪保障。