主頁 > 移動端開發 > Android媒體播放框架MediaSessionCompat介紹實踐

Android媒體播放框架MediaSessionCompat介紹實踐

2021-05-04 09:24:32 移動端開發

前言

最近維護專案的時候遇到了MediaSessionCompat框架的音樂播放器,簡單搜索記錄一下這套實作播放器的結構吧,

MediaSession框架簡介

我們先來看看如何設計一款音樂播放App的架構,傳統的做法是這樣的:

  • 注冊一個Service,用于異步獲取音樂庫資料、音樂控制等,在Service中我們可能還需要自定義一些狀態值和回呼介面用于流程控制
  • 通過廣播(其他方式如介面、Messenger都可以)實作Activity和Service之間的通信,使得用戶可以通過界面上的組件控制音樂的播放、暫停、拖動進度條等操作

如果我們的音樂播放器還需要支持通知欄快捷控制音樂播放的功能,那么又得新增一套廣播和相應的介面去回應通知欄按鈕的事件,

如果還需要支持多端(電視、手表、耳機等)控制同一個播放器,那么整個系統架構可能會變得非常復雜,我們要花費大量的時間和精力去設計、優化代碼的結構,那么有什么方法可以節省這些作業,提高我們的效率,然后還可以優雅地實作上述這些功能呢?

Google在Android 5.0中加入了MediaSession框架(在support-v4中同樣提供了相應的兼容包,相關的類以Compat結尾,Api基本相同),專門用來解決媒體播放時界面和Service通訊的問題,意在規范上述這些功能的流程,使用這個框架我們可以減少一些流程復雜的開發作業,例如使用各種廣播來控制播放器,而且其代碼可讀性、結構耦合度方面都控制得非常好,因此推薦大家嘗試下這個框架,下面我們就開始介紹MediaSession框架的核心成員和使用流程,

MediaSessionCompat位于android/support/v4/media/session包下,主要是用于替代Android L 之后推出的MessionSession,我們通過一張別人的圖來了解一下(這張圖說的是MediaSession,而不是MediaSessionCompat,但大致原理是一樣的):

常用成員類概述

MediaSession框架中有四個常用的成員類,它們是整個流程控制的核心

MediaBrowser

媒體瀏覽器,用來連接MediaBrowserService和訂閱資料,通過它的回呼介面我們可以獲取和Service的連接狀態以及獲取在Service中異步獲取的音樂庫資料,媒體瀏覽器一般創建于客戶端(可以理解為各個終端負責控制音樂播放的界面)中;

MediaBrowserService

瀏覽器服務,提供onGetRoot(控制客戶端媒體瀏覽器的連接請求,通過回傳值決定是否允許該客戶端連接服務)和onLoadChildren(媒體瀏覽器向Service發送資料訂閱時呼叫,一般在這執行異步獲取資料的操作,最后將資料發送至媒體瀏覽器的回呼介面中)這兩個抽象方法;

同時MediaBrowserService還作為承載媒體播放器(如MediaPlayer、ExoPlayer等)和MediaSession的容器

MediaSession

媒體會話,即受控端,通過設定MediaSessionCompat.Callback回呼來接收媒體控制器MediaController發送的指令,當收到指令時會觸發Callback中各個指令對應的回呼方法(回呼方法中會執行播放器相應的操作,如播放、暫停等),Session一般在Service.onCreate方法中創建,最后需呼叫setSessionToken方法設定用于和控制器配對的令牌并通知瀏覽器連接服務成功

MediaController

媒體控制器,在客戶端中開發者不僅可以使用控制器向Service中的受控端發送指令,還可以通過設定MediaControllerCompat.Callback回呼方法接收受控端的狀態,從而根據相應的狀態重繪界面UI,MediaController的創建需要受控端的配對令牌,因此需在瀏覽器成功連接服務的回呼執行創建的操作

通過上述的簡介中我們不難看出這四個成員之間有著非常明確的分工和作用范圍,使得整個代碼結構變得清晰易讀,

除此之外,MediaSession框架中還有一些同樣重要的類需要拿出來講,例如封裝了各種播放狀態的PlaybackState,和Map相似通過鍵值對保存媒體資訊的MediaMetadata,以及用于MediaBrowser和MediaBrowserService之間進行資料互動的MediaItem等等,下面我們通過實作一個簡單的demo來具體分析這套框架的作業流程,

使用MediaSession框架構建簡單的音樂播放器

例如我們的demo是這樣的(見下圖),只提供簡單的播放暫停操作,音樂資料源從raw資源檔案夾中獲取:

按照作業流程,我們就從獲取音樂庫資料開始吧,首先界面上方添加一個RecyclerView來展示獲取的音樂串列,我們在DemoActivity中完成一些RecyclerView的初始化操作

public class DemoActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private List<MediaBrowserCompat.MediaItem> list;
    private DemoAdapter demoAdapter;
    private LinearLayoutManager layoutManager;

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

        list = new ArrayList<>();
        layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        demoAdapter = new DemoAdapter(this,list);

        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setAdapter(demoAdapter);
    }
}

注意List元素的型別為MediaBrowserCompat.MediaItem,因為MediaBrowser從服務中獲取的每一首音樂都會封裝成MediaItem物件,接下來我們創建MediaBrowser,并執行連接服務端和訂閱資料的操作

public class DemoActivity extends AppCompatActivity {
    ...
    private MediaBrowserCompat mBrowser;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mBrowser = new MediaBrowserCompat(
                this,
                new ComponentName(this, MusicService.class),//系結服務端
                browserConnectionCallback,//設定連接回呼
                null
        );
    }

    @Override
    protected void onStart() {
        super.onStart();
        //Browser發送連接請求
        mBrowser.connect();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mBrowser.disconnect();
    }

    /**
     * 連接狀態的回呼介面,連接成功時會呼叫onConnected()方法
     */
    private MediaBrowserCompat.ConnectionCallback browserConnectionCallback=
            new MediaBrowserCompat.ConnectionCallback(){
                @Override
                public void onConnected() {
                    Log.e(TAG,"onConnected------");
                    //必須在確保連接成功的前提下執行訂閱的操作
                    if (mBrowser.isConnected()) {
                        //mediaId即為MediaBrowserService.onGetRoot的回傳值
                        //若Service允許客戶端連接,則回傳結果不為null,其值為資料內容層次結構的根ID
                        //若拒絕連接,則回傳null
                        String mediaId = mBrowser.getRoot();

                        //Browser通過訂閱的方式向Service請求資料,發起訂閱請求需要兩個引數,其一為mediaId
                        //而如果該mediaId已經被其他Browser實體訂閱,則需要在訂閱之前取消mediaId的訂閱者
                        //雖然訂閱一個 已被訂閱的mediaId 時會取代原Browser的訂閱回呼,但卻無法觸發onChildrenLoaded回呼

                        //ps:雖然基本的概念是這樣的,但是Google在官方demo中有這么一段注釋...
                        // This is temporary: A bug is being fixed that will make subscribe
                        // consistently call onChildrenLoaded initially, no matter if it is replacing an existing
                        // subscriber or not. Currently this only happens if the mediaID has no previous
                        // subscriber or if the media content changes on the service side, so we need to
                        // unsubscribe first.
                        //大概的意思就是現在這里還有BUG,即只要發送訂閱請求就會觸發onChildrenLoaded回呼
                        //所以無論怎樣我們發起訂閱請求之前都需要先取消訂閱
                        mBrowser.unsubscribe(mediaId);
                        //之前說到訂閱的方法還需要一個引數,即設定訂閱回呼SubscriptionCallback
                        //當Service獲取資料后會將資料發送回來,此時會觸發SubscriptionCallback.onChildrenLoaded回呼
                        mBrowser.subscribe(mediaId, BrowserSubscriptionCallback);
                    }
                }

                @Override
                public void onConnectionFailed() {
                    Log.e(TAG,"連接失敗!");
                }
            };
    /**
     * 向媒體服務器(MediaBrowserService)發起資料訂閱請求的回呼介面
     */
    private final MediaBrowserCompat.SubscriptionCallback browserSubscriptionCallback =
            new MediaBrowserCompat.SubscriptionCallback(){
                @Override
                public void onChildrenLoaded(@NonNull String parentId,
                                             @NonNull List<MediaBrowserCompat.MediaItem> children) {
                    Log.e(TAG,"onChildrenLoaded------");
                    //children 即為Service發送回來的媒體資料集合
                    for (MediaBrowserCompat.MediaItem item:children){
                        Log.e(TAG,item.getDescription().getTitle().toString());
                        list.add(item);
                    }
                    //在onChildrenLoaded可以執行重繪串列UI的操作
                    demoAdapter.notifyDataSetChanged();
                }
            };
}

通過上述的代碼和注釋大家應該清楚MediaBrowser連接服務到向其訂閱資料的流程了,簡單總結一下就是:

connect → onConnected → subscribe → onChildrenLoaded

那么Service端那邊在這段流程中又做了什么呢?首先我們得繼承MediaBrowserService(這里使用了support-v4包的類)創建MusicService類,MediaBrowserService繼承自Service,所以記得在AndroidManifest.xml中完成配置:

<service
    android:name=".demo.MusicService">
    <intent-filter>
        <action android:name="android.media.browse.MediaBrowserService" />
    </intent-filter>
</service>

我們需要在Service初始化的時候就完成MediaSession的構建,并為它設定相應的標志、狀態等,具體的代碼如下:

public class MusicService extends MediaBrowserServiceCompat {
    private MediaSessionCompat mSession;
    private PlaybackStateCompat mPlaybackState;

    @Override
    public void onCreate() {
        super.onCreate();
        mPlaybackState = new PlaybackStateCompat.Builder()
                .setState(PlaybackStateCompat.STATE_NONE,0,1.0f)
                .build();

        mSession = new MediaSessionCompat(this,"MusicService");
        mSession.setCallback(SessionCallback);//設定回呼
        mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
        mSession.setPlaybackState(mPlaybackState);

        //設定token后會觸發MediaBrowserCompat.ConnectionCallback的回呼方法
        //表示MediaBrowser與MediaBrowserService連接成功
        setSessionToken(mSession.getSessionToken());
    }
}

這里解釋下其中的一些細節,首先是呼叫MediaSession.setFlag為Session設定標志位,以便Session接收控制器的指令,然后是播放狀態的設定,需呼叫MediaSession.setPlaybackState,那么PlaybackState又是什么呢?之前我們簡單介紹過它是封裝了各種播放狀態的類,我們可以通過判斷當前播放狀態來控制各個成員的行為,而PlaybackState類為我們定義了各種狀態的規范,此外我們還需要設定SessionCallback回呼,當客戶端使用控制器發送指令時,就會觸發這些回呼方法,從而達到控制播放器的目的,

public class MusicService extends MediaBrowserServiceCompat {
    ...
    private MediaPlayer mMediaPlayer;

    @Override
    public void onCreate() {
        ...
        mMediaPlayer = new MediaPlayer();
        mMediaPlayer.setOnPreparedListener(PreparedListener);
        mMediaPlayer.setOnCompletionListener(CompletionListener);
    }

    /**
     * 回應控制器指令的回呼
     */
    private android.support.v4.media.session.MediaSessionCompat.Callback SessionCallback = new MediaSessionCompat.Callback(){
        /**
         * 回應MediaController.getTransportControls().play
         */
        @Override
        public void onPlay() {
            Log.e(TAG,"onPlay");
            if(mPlaybackState.getState() == PlaybackStateCompat.STATE_PAUSED){
                mMediaPlayer.start();
                mPlaybackState = new PlaybackStateCompat.Builder()
                        .setState(PlaybackStateCompat.STATE_PLAYING,0,1.0f)
                        .build();
                mSession.setPlaybackState(mPlaybackState);
            }
        }

        /**
         * 回應MediaController.getTransportControls().onPause
         */
        @Override
        public void onPause() {
            Log.e(TAG,"onPause");
            if(mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING){
                mMediaPlayer.pause();
                mPlaybackState = new PlaybackStateCompat.Builder()
                        .setState(PlaybackStateCompat.STATE_PAUSED,0,1.0f)
                        .build();
                mSession.setPlaybackState(mPlaybackState);
            }
        }

        /**
         * 回應MediaController.getTransportControls().playFromUri
         * @param uri
         * @param extras
         */
        @Override
        public void onPlayFromUri(Uri uri, Bundle extras) {
            Log.e(TAG,"onPlayFromUri");
            try {
                switch (mPlaybackState.getState()){
                    case PlaybackStateCompat.STATE_PLAYING:
                    case PlaybackStateCompat.STATE_PAUSED:
                    case PlaybackStateCompat.STATE_NONE:
                        mMediaPlayer.reset();
                        mMediaPlayer.setDataSource(MusicService.this,uri);
                        mMediaPlayer.prepare();//準備同步
                        mPlaybackState = new PlaybackStateCompat.Builder()
                                .setState(PlaybackStateCompat.STATE_CONNECTING,0,1.0f)
                                .build();
                        mSession.setPlaybackState(mPlaybackState);
                        //我們可以保存當前播放音樂的資訊,以便客戶端重繪UI
                        mSession.setMetadata(new MediaMetadataCompat.Builder()
                                .putString(MediaMetadataCompat.METADATA_KEY_TITLE,extras.getString("title"))
                                .build()
                        );
                        break;
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }

        @Override
        public void onPlayFromSearch(String query, Bundle extras) {
        }
    };

    /**
     * 監聽MediaPlayer.prepare()
     */
    private MediaPlayer.OnPreparedListener PreparedListener = new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mediaPlayer) {
            mMediaPlayer.start();
            mPlaybackState = new PlaybackStateCompat.Builder()
                    .setState(PlaybackStateCompat.STATE_PLAYING,0,1.0f)
                    .build();
            mSession.setPlaybackState(mPlaybackState);
        }
    } ;

    /**
     * 監聽播放結束的事件
     */
    private MediaPlayer.OnCompletionListener CompletionListener = new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mediaPlayer) {
            mPlaybackState = new PlaybackStateCompat.Builder()
                    .setState(PlaybackStateCompat.STATE_NONE,0,1.0f)
                    .build();
            mSession.setPlaybackState(mPlaybackState);
            mMediaPlayer.reset();
        }
    };
}

MediaSessionCompat.Callback中還有許多回呼方法,大家可以按需覆寫重寫即可:

構建好MediaSession后記得呼叫setSessionToken保存Session的配對令牌,同時呼叫此方法也會回呼MediaBrowser.ConnectionCallback的onConnected方法,告知客戶端Browser與BrowserService連接成功了,我們也就完成了MediaSession的創建和初始化

之前我們還講到Browser與BrowserService的訂閱關系,在MediaBrowserService中我們需要重寫onGetRoot和onLoadChildren方法,其作用之前已經講過就不多贅述了,

public class MusicService extends MediaBrowserServiceCompat {
    @Nullable
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
        Log.e(TAG,"onGetRoot-----------");
        return new BrowserRoot(MEDIA_ID_ROOT, null);
    }

    @Override
    public void onl oadChildren(@NonNull String parentId, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        Log.e(TAG,"onLoadChildren--------");
        //將資訊從當前執行緒中移除,允許后續呼叫sendResult方法
        result.detach();

        //我們模擬獲取資料的程序,真實情況應該是異步從網路或本地讀取資料
        MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
                .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ""+R.raw.jinglebells)
                .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "圣誕歌")
                .build();
        ArrayList<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
        mediaItems.add(createMediaItem(metadata));

        //向Browser發送資料
        result.sendResult(mediaItems);
    }

    private MediaBrowserCompat.MediaItem createMediaItem(MediaMetadataCompat metadata){
        return new MediaBrowserCompat.MediaItem(
                metadata.getDescription(),
                MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
        );
    }
}

最后我們回到客戶端這邊,四大成員還剩下控制器MediaController沒講,MediaController的創建依賴于Session的配對令牌,當Browser和BrowserService連接成功我們就可以通過Browser拿到這個令牌了,控制器創建后,我們就可以通過MediaController.getTransportControls的方法發送播放指令,同時也可以注冊MediaControllerCompat.Callback回呼接收播放狀態,用以重繪界面UI:

public class DemoActivity extends AppCompatActivity {
    ...
    private Button btnPlay;
    private TextView textTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        btnPlay = (Button) findViewById(R.id.btn_play);
        textTitle = (TextView) findViewById(R.id.text_title);
    }

    public void clickEvent(View view) {
        switch (view.getId()) {
            case R.id.btn_play:
                if(mController!=null){
                    handlerPlayEvent();
                }
                break;
        }
    }

    /**
     * 處理播放按鈕事件
     */
    private void handlerPlayEvent(){
        switch (mController.getPlaybackState().getState()){
            case PlaybackStateCompat.STATE_PLAYING:
                mController.getTransportControls().pause();
                break;
            case PlaybackStateCompat.STATE_PAUSED:
                mController.getTransportControls().play();
                break;
            default:
                mController.getTransportControls().playFromSearch("", null);
                break;
        }
    }

    /**
     * 連接狀態的回呼介面,連接成功時會呼叫onConnected()方法
     */
    private MediaBrowserCompat.ConnectionCallback browserConnectionCallback =
            new MediaBrowserCompat.ConnectionCallback(){
                @Override
                public void onConnected() {
                    Log.e(TAG,"onConnected------");
                    if (mBrowser.isConnected()) {
                        ...
                        try{
                            mController = new MediaControllerCompat(DemoActivity.this,mBrowser.getSessionToken());
                            //注冊回呼
                            mController.registerCallback(ControllerCallback);
                        }catch (RemoteException e){
                            e.printStackTrace();
                        }
                    }
                }

                @Override
                public void onConnectionFailed() {
                    Log.e(TAG,"連接失敗!");
                }
            };

    /**
     * 媒體控制器控制播放程序中的回呼介面,可以用來根據播放狀態更新UI
     */
    private final MediaControllerCompat.Callback ControllerCallback =
            new MediaControllerCompat.Callback() {
                /***
                 * 音樂播放狀態改變的回呼
                 * @param state
                 */
                @Override
                public void onPlaybackStateChanged(PlaybackStateCompat state) {
                    switch (state.getState()){
                        case PlaybackStateCompat.STATE_NONE://無任何狀態
                            textTitle.setText("");
                            btnPlay.setText("開始");
                            break;
                        case PlaybackStateCompat.STATE_PAUSED:
                            btnPlay.setText("開始");
                            break;
                        case PlaybackStateCompat.STATE_PLAYING:
                            btnPlay.setText("暫停");
                            break;
                    }
                }

                /**
                 * 播放音樂改變的回呼
                 * @param metadata
                 */
                @Override
                public void onMetadataChanged(MediaMetadataCompat metadata) {
                    textTitle.setText(metadata.getDescription().getTitle());
                }
            };

    private Uri rawToUri(int id){
        String uriStr = "android.resource://" + getPackageName() + "/" + id;
        return Uri.parse(uriStr);
    }
}

MediaSession框架的基本用法我們已經分析完了,對于各自的播放器可能結構稍有不同,需要結合實際情況再分析,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/282644.html

標籤:其他

上一篇:自定義PopupWindow實作下拉選擇框并進行選擇資料傳遞

下一篇:E. Generate a String(dp)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more