回 Android手機程式設計人才培訓班 課程時間表

專案六:我的多媒體播放器

專案簡介

可以掃描記憶卡內的影片跟音樂檔案,並播放。並同時支援手機跟平板的畫面。

平板畫面
手機畫面(ㄧ)
手機畫面(二)


專案需求

  • 使用File物件來取的音樂跟影片檔案清單。
  • 使用MediaPlayer物件播放影片及音樂。
  • Fragment應用。
  • Notification物件應用。
  • Service實戰。
  • 同時支援手機與平板的應用程式開發。


實作步驟

  1. 建立Master/Detail Flow專案。

  2. 專案預設執行後看不到ActionBar,所以先將java/{package}/ItemListActivity.java內的ItemListActivity的父類別改為ActionBarActivity

  3. 將影片放上傳到模擬器的/storage/sdcard/Movies目錄內,將音樂上傳到/storage/sdcard/Music目錄內。

  4. 在AndroidManifest.xml內加入權限:android.permission.READ_EXTERNAL_STORAGE

  5. 將res/layout/fragment_item_detail.xml檔案複製一份到res/layout-sw600dp目錄內。

  6. 將res/layout/fragment_item_detail.xml版面設計如下:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:background="#000"
        android:orientation="vertical"
        tools:context=".ItemDetailFragment">
    
        <TextView
            android:id="@+id/item_detail"
            style="?android:attr/textAppearanceLarge"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:textColor="#ff0"
            android:textIsSelectable="true"/>
    
        <VideoView android:id="@+id/videoView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_gravity="center"
            android:layout_weight="12"/>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="2"
            android:orientation="horizontal"
            android:shadowDx="3"
            android:shadowDy="3"
            android:shadowColor="#000"
            android:shadowRadius="1">
    
            <Button android:id="@+id/play"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="播放"/>
    
            <Button android:id="@+id/pause"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="暫停"/>
    
            <Button android:id="@+id/stop"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="停止"/>
        </LinearLayout>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="2"
            android:orientation="horizontal"
            android:shadowDx="3"
            android:shadowDy="3"
            android:shadowColor="#000"
            android:shadowRadius="1">
    
            <Button android:id="@+id/forward"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="快轉"/>
    
            <Button android:id="@+id/backward"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="倒轉"/>
    
            <!-- ToggleButton有On跟Off兩種狀態-->
            <ToggleButton android:id="@+id/repeat"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:background="#833"
                android:textColor="#ff0"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"/>
        </LinearLayout>
    </LinearLayout>
    
  7. 將res/layout-sw600dp/fragment_item_detail.xml版面設計如下:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:background="#000"
        android:orientation="vertical"
        tools:context=".ItemDetailFragment">
    
        <TextView
            android:id="@+id/item_detail"
            style="?android:attr/textAppearanceLarge"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:textColor="#ff0"
            android:textIsSelectable="true"/>
    
        <VideoView android:id="@+id/videoView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_gravity="center"
            android:layout_weight="12"/>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="2"
            android:orientation="horizontal"
            android:shadowDx="3"
            android:shadowDy="3"
            android:shadowColor="#000"
            android:shadowRadius="1">
    
            <Button android:id="@+id/play"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="播放"/>
    
            <Button android:id="@+id/pause"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="暫停"/>
    
            <Button android:id="@+id/stop"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="停止"/>
    
            <Button android:id="@+id/forward"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="快轉"/>
    
            <Button android:id="@+id/backward"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:textColor="#ff0"
                android:background="#833"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"
                android:text="倒轉"/>
    
            <!-- ToggleButton有On跟Off兩種狀態-->
            <ToggleButton android:id="@+id/repeat"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:background="#833"
                android:textColor="#ff0"
                android:shadowDx="3"
                android:shadowDy="3"
                android:shadowColor="#000"
                android:shadowRadius="1"/>
        </LinearLayout>
    </LinearLayout>
    
  8. 修改java/{package}/dummy/DummyContent.java原始碼檔案,加入將記憶卡內的影片和音樂檔案掃描出來:
    static
    {
        String musicPath = "/storage/sdcard/Music"; // 模擬器路徑
        File musicFolder = new File(musicPath);
    
        String videoPath = "/storage/sdcard/Movies"; // 模擬器路徑
        File videoFolder = new File(videoPath);
    
        // 建立MediaFileFilter物件來過濾檔案
        FilenameFilter mediafilefilter = new FilenameFilter()
        {
            private String[] filter = {".mp3",".ogg",".3gp",".mp4"};
    
            @Override
            public boolean accept(File dir, String filename)
            {
                // 依照filter字串陣列內的副檔名來判斷是不是要的檔案類型
                for(int i= 0 ; i < filter.length ; i++)
                {
                    if(filename.endsWith(filter[i]))
                    return true;
                }
    
                return false;
            }
        };
    
        // 取得影片目錄下的所有音樂檔案
        File[] listVideo = videoFolder.listFiles(mediafilefilter);
    
        // 將影片檔名跟路徑加入List中
        for(int i = 0 ; i < listVideo.length ; i++)
        {
            addItem(new DummyItem(listVideo[i].getPath(), listVideo[i].getName()));
        }
    
        // 取得音樂目錄下的所有音樂檔案(id=路徑, content=檔名)
        File[] listMusic = musicFolder.listFiles(mediafilefilter);
    
        // 將音樂檔名跟路徑加入List中(id=路徑, content=檔名)
        for(int i = 0 ; i < listMusic.length ; i++)
        {
            addItem(new DummyItem(listMusic[i].getPath(), listMusic[i].getName()));
        }
    }
    
  9. 修改java/{package}/ItemDetailFragment.java原始碼檔案,加入影片和音樂的播放功能:
    private VideoView videoView; // 用來播影片的物件
    private MediaPlayer mp;      // 用來播音樂的物件
    
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
    
        if (getArguments().containsKey(ARG_ITEM_ID))
        {
            // 使用者選擇的項目在這裡會被取出來成為DummyItem類別, id=路徑, content=檔名
            mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID));
        }
    }
    
    @Override
    public void onPause()
    {
        super.onPause();
        if(mp != null) mp.stop(); // 停止播放音樂
        if(videoView != null) videoView.stopPlayback(); // 停止播放影片
    }
    
    @Override
    public void onResume()
    {
        super.onResume();
        if(isMusic(mItem.content)) loadSong();  // 使用者選的是音樂的話,就初始化音樂
        if(isVideo(mItem.content)) loadMovie(); // 使用者選的是影片的話,就初始化影片
    }
    
    // 判斷檔名是不是音樂檔
    private boolean isMusic(String filename)
    {
        String[] filter = {".mp3"};
    
        for(int i = 0 ; i < filter.length ; i++)
        {
            if(filename.endsWith(filter[i]))
            return true;
        }
    
        return false;
    }
    
    // 判斷檔名是不是影片檔
    private boolean isVideo(String filename)
    {
        String[] filter = {".ogg",".3gp",".mp4"};
    
        for(int i = 0 ; i < filter.length ; i++)
        {
            if(filename.endsWith(filter[i]))
            return true;
        }
    
        return false;
    }
    
    private void loadMovie()
    {
        // 關聯VideoView
        videoView = (VideoView)getView().findViewById(R.id.videoView);
    
        // 加上MediaController來控制影片
        MediaController mc = new MediaController(getActivity());
        videoView.setMediaController(mc);
    
        // 設定要撥放的影片路徑
        videoView.setVideoPath(mItem.id);
    
        // 設定OnCompletionListener給VideoView
        videoView.setOnCompletionListener(onCompletionListener);
    
        // 開始撥放
        videoView.start();
    }
    
    // 影片播完完畢傾聽器
    MediaPlayer.OnCompletionListener onCompletionListener = new MediaPlayer.OnCompletionListener()
    {
        @Override
        public void onCompletion(MediaPlayer mp)
        {
        Toast.makeText(getActivity(), "影片播完了", Toast.LENGTH_SHORT).show();
        }
    };
    
    // 初始化要播放的音樂檔
    private void loadSong()
    {
        mp = new MediaPlayer(); // 初始化MediaPlayer
    
        try
        {
            mp.reset(); // 清除緩衝區資料
            mp.setDataSource(mItem.id); // 設定檔案路徑
            mp.prepare(); // 緩衝音樂
        }
        catch(IOException e)
        {
            Toast.makeText(getActivity(), "錯誤: " + e.getMessage(), Toast.LENGTH_SHORT).show();
        }
    
        // 從頭播放
        mp.seekTo(0);
    }
    
    // 處理按鈕事件
    View.OnClickListener onClickListener = new View.OnClickListener()
    {
        @Override
        public void onClick(View v)
        {
            // 如果不是音樂檔, 就不處理按鈕事件
            if(!isMusic(mItem.content)) return;
    
            switch(v.getId())
            {
            case R.id.play:
                mp.start(); // 播放
                showNotification(getActivity(), android.R.drawable.ic_media_play, "Play: " + mItem.content, ItemListActivity.class);
                break;
            case R.id.pause:
                mp.pause(); // 暫停
                showNotification(getActivity(), android.R.drawable.ic_media_pause, "Pause: " + mItem.content, ItemListActivity.class);
                break;
            case R.id.stop:
                mp.stop(); // 停止
                showNotification(getActivity(), android.R.drawable.ic_delete, "Stop: " + mItem.content, ItemListActivity.class);
                loadSong(); // 停止後,要重新播放,必須重新緩衝音樂
                break;
            case R.id.backward:
                // 往前跳5秒鐘
                if(mp.getCurrentPosition() - 5000 > 0)
                mp.seekTo(mp.getCurrentPosition() - 5000);
                break;
            case R.id.forward:
                // 往後跳5秒鐘
                if(mp.getCurrentPosition() + 5000 < mp.getDuration())
                mp.seekTo(mp.getCurrentPosition() + 5000);
                break;
            case R.id.repeat:
                ToggleButton tb = (ToggleButton)getView().findViewById(R.id.repeat);
                if(tb.isChecked())
                {
                    tb.setText("重複");
                    mp.setLooping(true); // 設定重複播放
                }
                else
                {
                    tb.setText("不重複");
                    mp.setLooping(false); // 設定不重複播放
                }
    
                break;
            }
        }
    };
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) 
    {
        View rootView = inflater.inflate(R.layout.fragment_item_detail, container, false);
    
        // Show the dummy content as text in a TextView.
        if (mItem != null)
        {
            ((TextView) rootView.findViewById(R.id.item_detail)).setText(mItem.id);
        }
    
        // 設定按鈕傾聽器
        rootView.findViewById(R.id.play).setOnClickListener(onClickListener);
        rootView.findViewById(R.id.pause).setOnClickListener(onClickListener);
        rootView.findViewById(R.id.stop).setOnClickListener(onClickListener);
        rootView.findViewById(R.id.backward).setOnClickListener(onClickListener);
        rootView.findViewById(R.id.forward).setOnClickListener(onClickListener);
        rootView.findViewById(R.id.repeat).setOnClickListener(onClickListener);
        ((ToggleButton)rootView.findViewById(R.id.repeat)).setText("不重複");
    
        return rootView;
    }
    
    // 將Notification包裝成一個方法
    // iconId: 要顯示的圖示
    // msg: 要顯示的文字訊息
    private void showNotification(Context context, int iconId, String msg, Class<?> cls)
    {
        // 在Notification Drawer點下去後要呼叫的Activity
        PendingIntent pit = PendingIntent.getActivity(
        context, // 目前所在的Activity
        0, // request code, 這裡不重要
        new Intent(context.getApplicationContext(), cls), // 想要備觸發的Intent
        PendingIntent.FLAG_UPDATE_CURRENT);
    
        // 建立Notification
        NotificationCompat.Builder b = new NotificationCompat.Builder(context);
    
        b.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS);
        b.setSmallIcon(iconId); // 設定Icon
        b.setTicker(msg);  // 設定系統列訊息
        b.setContentTitle("Title"); // 設定標題
        b.setContentText(msg);      // 訊息
        b.setContentIntent(pit);    // 設定在系統頁被點擊時要觸發的Intent
        b.setWhen(System.currentTimeMillis()); // 立即顯示
        b.setContentInfo("info");              // 設定內容
    
    
        Notification n = b.build();
    
        // 下面兩行如果是在Activity裡面要丟訊息到系統列須使用NotificationManager
        NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        nm.notify(1, n);
    
        // 在Service內要改成呼叫startForeground()方法
        // 參數1: Notification ID, 用來辨識或重新設定用
        // 參數2: Notification物件
        //startForeground(1, n);
    }
    
    注意:onCreate()方法和onCreateView()原來已經存在,必須先刪除原本的版本。
  10. 將程式執行在手機模擬器和平板模擬器中觀察結果。

如果你想讓影片畫面延伸到跟VideoView一樣大小,可以參考這裡
完整專案下載:MyMediaPlayerAS.zip(Android Studio專案)