2019년 3월 21일 목요일

안드로이드 QR 코드 리더 예제

- 오픈소스 라이브러리를 이용한 예제
- ZXing 공식 페이지
https://github.com/zxing/zxing




1. 기본코드
- 어플리케이션 build.gradle
implementation 'com.journeyapps:zxing-android-embedded:3.6.0'


- AndroidManifest.xml
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="android:screenOrientation" />

> tools:replace="android:screenOrientation"을 추가하지 않으면 android:screenOrientation="fullSensor" 에러가 발생한다.


- Activity
public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

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

        findViewById(R.id.action).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new IntentIntegrator(MainActivity.this).initiateScan();
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
        if (result != null) {
            if (result.getContents() != null) {
                String text = result.getContents();

                if (TextUtils.isEmpty(text) == false) {
                    TextView textView = findViewById(R.id.text);
                    textView.setText(text);
                }
                Log.d(TAG, "QR : " + result.getContents());
            }
            return;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }
}

> initiateScan(); 메소드를 호출하면 CaptureActivity가 호출되고 카메라 사용 퍼미션에 대한 안내가 표시된다.



2. 샘플코드
소스코드
APK








- QR





2019년 3월 18일 월요일

ShareActionProvider 예제 - androidx

- 기본코드
MenuItem ic_menu_share = menu.add("share").setIcon(android.R.drawable.ic_menu_share);
shareActionProvider = new ShareActionProvider(this);
ic_menu_share.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
MenuItemCompat.setActionProvider(ic_menu_share, shareActionProvider);

- createChooser 호출
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("http://www.google.com"));

shareActionProvider.setShareIntent(intent);
startActivity(Intent.createChooser(intent, "share"));


> createChooser를 호출하면 하단에 선택 메뉴가 표시.
> setShareIntent는 액션바 메뉴에 표시.






안드로이드 화면녹화 예제 - MediaProjection

- 안드로이드 API 21 이상
- WRITE_EXTERNAL_STORAGE, RECORD_AUDIO 권한 필요.


1. 기본코드
- MediaRecorder
MediaRecorder mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setOutputFile(videoFile);
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
mediaRecorder.setVideoSize(displayMetrics.widthPixels, displayMetrics.heightPixels);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setVideoEncodingBitRate(512 * 1000);
mediaRecorder.setVideoFrameRate(30);
try {
mediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}


- MediaProjection
final MediaRecorder screenRecorder = createRecorder();
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
MediaProjection.Callback callback = new MediaProjection.Callback() {
@Override
public void onStop() {
super.onStop();
if (screenRecorder != null) {
screenRecorder.stop();
screenRecorder.reset();
screenRecorder.release();
}
mediaProjection.unregisterCallback(this);
mediaProjection = null;
}
};
mediaProjection.registerCallback(callback, null);

DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
mediaProjection.createVirtualDisplay(
"sample",
displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
screenRecorder.getSurface(), null, null);

screenRecorder.start();


> createVirtualDisplay 과정에서 MediaRecorder의 Surface를 가져와 버추얼 디스플레이로 녹화를 한다.
> 녹화를 위해서 오디오와 저장소 사용 권한을 허용해야 한다.


- 전체코드
public class MainActivity extends AppCompatActivity {
    private static final String videoFile = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/MediaProjection.mp4";

    private static final int REQUEST_CODE_PERMISSIONS = 100;
    private static final int REQUEST_CODE_MediaProjection = 101;

    private MediaProjection mediaProjection;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 퍼미션 확인
        checkSelfPermission();
    }

    @Override
    protected void onDestroy() {
        // 녹화중이면 종료하기
        if (mediaProjection != null) {
            mediaProjection.stop();
        }
        super.onDestroy();
    }

    @Override
    protected void onActivityResult(int requestCode, final int resultCode, @Nullable final Intent data) {
        // 미디어 프로젝션 응답
        if (requestCode == REQUEST_CODE_MediaProjection && resultCode == RESULT_OK) {
            screenRecorder(resultCode, data);
            return;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    /**
     * 음성녹음, 저장소 퍼미션
     *
     * @return
     */
    public boolean checkSelfPermission() {
        String temp = "";
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            temp += Manifest.permission.RECORD_AUDIO + " ";
        }
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            temp += Manifest.permission.WRITE_EXTERNAL_STORAGE + " ";
        }

        if (TextUtils.isEmpty(temp) == false) {
            ActivityCompat.requestPermissions(this, temp.trim().split(" "), REQUEST_CODE_PERMISSIONS);
            return false;
        } else {
            initView();
            return true;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_PERMISSIONS: {
                int length = permissions.length;
                for (int i = 0; i < length; i++) {
                    if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                        // 퍼미션 동의가 1개라도 부족하면 화면을 초기화 하지 않음
                        return;
                    }
                    initView();
                }
                return;
            }
            default:
                return;
        }
    }

    /**
     * 뷰 초기화
     */
    private void initView() {
        findViewById(R.id.actionRec).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 미디어 프로젝션 요청
                startMediaProjection();
            }
        });
    }

    /**
     * 화면녹화
     *
     * @param resultCode
     * @param data
     */
    private void screenRecorder(int resultCode, @Nullable Intent data) {
        final MediaRecorder screenRecorder = createRecorder();
        MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
        mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
        MediaProjection.Callback callback = new MediaProjection.Callback() {
            @Override
            public void onStop() {
                super.onStop();
                if (screenRecorder != null) {
                    screenRecorder.stop();
                    screenRecorder.reset();
                    screenRecorder.release();
                }
                mediaProjection.unregisterCallback(this);
                mediaProjection = null;
            }
        };
        mediaProjection.registerCallback(callback, null);

        DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
        mediaProjection.createVirtualDisplay(
                "sample",
                displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                screenRecorder.getSurface(), null, null);

        final Button actionRec = findViewById(R.id.actionRec);
        actionRec.setText("STOP REC");
        actionRec.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                actionRec.setText("START REC");
                if (mediaProjection != null) {
                    mediaProjection.stop();

                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    intent.setDataAndType(Uri.parse(videoFile), "video/mp4");
                    startActivity(intent);
                }
            }
        });
        screenRecorder.start();
    }

    /**
     * 미디어 프로젝션 요청
     */
    private void startMediaProjection() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
            startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CODE_MediaProjection);
        }
    }

    /**
     * 미디어 레코더
     *
     * @return
     */
    private MediaRecorder createRecorder() {
        MediaRecorder mediaRecorder = new MediaRecorder();
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mediaRecorder.setOutputFile(videoFile);
        DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
        mediaRecorder.setVideoSize(displayMetrics.widthPixels, displayMetrics.heightPixels);
        mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
        mediaRecorder.setVideoEncodingBitRate(512 * 1000);
        mediaRecorder.setVideoFrameRate(30);
        try {
            mediaRecorder.prepare();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return mediaRecorder;
    }
}



2. 샘플코드
소스코드
APK









2019년 3월 14일 목요일

ExoPlayer 예제

- 구글 ExoPlayer
https://github.com/google/ExoPlayer
https://developer.android.com/guide/topics/media/exoplayer

- 기본 미디어 플레이와 HLS 미디어 플레이 예제


1. 예제코드
- 어플리케이션 build.gradle 설정
implementation 'com.google.android.exoplayer:exoplayer:2.8.4'


- 기본코드
public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    private static class EventListener extends Player.DefaultEventListener {
        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            String stateString;
            switch (playbackState) {
                case Player.STATE_IDLE:
                    stateString = "ExoPlayer.STATE_IDLE";
                    break;
                case Player.STATE_BUFFERING:
                    stateString = "ExoPlayer.STATE_BUFFERING";
                    break;
                case Player.STATE_READY:
                    stateString = "ExoPlayer.STATE_READY";
                    break;
                case Player.STATE_ENDED:
                    stateString = "ExoPlayer.STATE_ENDED";
                    break;
                default:
                    stateString = "UNKNOWN_STATE";
                    break;
            }
            Log.d(TAG, "changed state to " + stateString + ", playWhenReady: " + playWhenReady);
        }
    }

    private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
    private SimpleExoPlayer exoPlayer;
    private long playbackPosition;
    private int currentWindow;
    private boolean playWhenReady = true;

    private EventListener eventListener = new EventListener();

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

    @Override
    protected void onResume() {
        super.onResume();
        initPlayer();
    }

    @Override
    protected void onPause() {
        super.onPause();
        releasePlayer();
    }

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

    private void initPlayer() {
        String url = "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8";
        PlayerView playerView = findViewById(R.id.playerView);

        TrackSelection.Factory adaptiveTrackSelectionFactory =
                new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
        exoPlayer = ExoPlayerFactory.newSimpleInstance(new DefaultRenderersFactory(this), new DefaultTrackSelector(adaptiveTrackSelectionFactory), new DefaultLoadControl());
        exoPlayer.addListener(new EventListener());


        MediaSource mediaSource = url.endsWith(".m3u8") ? buildMediaSourceHLS(Uri.parse(url)) : buildMediaSourceVideo(Uri.parse(url));
        exoPlayer.prepare(mediaSource, true, false);
        exoPlayer.setPlayWhenReady(playWhenReady);
        exoPlayer.seekTo(currentWindow, playbackPosition);
        playerView.setPlayer(exoPlayer);
    }

    private void releasePlayer() {
        if (exoPlayer != null) {
            playbackPosition = exoPlayer.getCurrentPosition();
            currentWindow = exoPlayer.getCurrentWindowIndex();
            playWhenReady = exoPlayer.getPlayWhenReady();
            exoPlayer.removeListener(eventListener);
            exoPlayer.release();
            exoPlayer = null;
        }
    }

    private MediaSource buildMediaSourceHLS(Uri uri) {
        String userAgent = System.getProperty("http.agent");
        Log.d(TAG, "UserAgent : " + userAgent);
        DataSource.Factory manifestDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
        HlsMediaSource hlsMediaSource = new HlsMediaSource.Factory(manifestDataSourceFactory).createMediaSource(uri);

        return new ConcatenatingMediaSource(hlsMediaSource);
    }

    private MediaSource buildMediaSourceVideo(Uri uri) {
        String userAgent = System.getProperty("http.agent");
        Log.d(TAG, "UserAgent : " + userAgent);
        ExtractorMediaSource videoSource =
                new ExtractorMediaSource.Factory(
                        new DefaultHttpDataSourceFactory(TextUtils.isEmpty(userAgent) ? "DefaultHttpDataSourceFactory" : userAgent)).
                        createMediaSource(uri);

        return new ConcatenatingMediaSource(videoSource);
    }
}

> buildMediaSourceHLS 메소드는 HLS 미디어를 재생하기위한 메소드
> 예제에서는 PlayerView를 통해 ExoPlayer를 재생하는 예제.
> 안드로이드 MediaPlayer과 동일하게 구현 가능.
> 주요 특징은 별도 검색으로 확인해 볼 것.
> 사용된 HSL 미디어 소스
https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8


추가) 오디오와 영상이 분리된 경우
private MediaSource buildMediaSourceAdaptive(Uri video, Uri audio) {

ExtractorMediaSource videoSource =
new ExtractorMediaSource.Factory(
new DefaultHttpDataSourceFactory("exoplayer-codelab")).
createMediaSource(video);
ExtractorMediaSource audioSource =
new ExtractorMediaSource.Factory(
new DefaultHttpDataSourceFactory("exoplayer-codelab")).
createMediaSource(audio);
MergingMediaSource mergingMediaSource = new MergingMediaSource(videoSource, audioSource);
return new ConcatenatingMediaSource(mergingMediaSource);
}




2. 샘플코드
소스코드
APK


VideoView 기본 예제

- VideoView의 기본 예제
- 플레이, 정지, 구간 스킵의 UI 요소들은 MediaController를 적용하는 예제
- 커스텀 UI로 구현할 경우 MediaController의 코드를 분석한 뒤 구현하는 것을 권장.



1. 기본 소스
- 로드 및 재생
String path = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4";
VideoView videoView = findViewById(R.id.videoView);
videoView.setVideoPath(path);
videoView.setMediaController(new MediaController(videoView.getContext()));
videoView.start();

> 기본 MediaController를 적용하면 현재포지션과 재생과 구간 스킵은 간편함.
> 커스텀 UI를 구현할 때 MediaController 소스를 참고해서 구현하는 것을 권장함.


- 포지션 이동
VideoView videoView = findViewById(R.id.videoView);
videoView.resume();
if (currentPosition > 0)
videoView.seekTo(currentPosition);


- 일지정지
VideoView videoView = findViewById(R.id.videoView);
if (videoView.canPause()) {
currentPosition = videoView.getCurrentPosition();
videoView.pause();
}

> VideoView는 resume될 때 현재 진행된 포지션을 유지하지 못한다.
> 실시간 스트리밍을 구현하는 경우가 아니면 currentPosition을 알고 있어야 한다.


- 정지
VideoView videoView = findViewById(R.id.videoView);
videoView.stopPlayback();


- Activity 예제
public class MainActivity extends AppCompatActivity {

    private int currentPosition;

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

        String path = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4";
        VideoView videoView = findViewById(R.id.videoView);
        videoView.setVideoPath(path);
        videoView.setMediaController(new MediaController(videoView.getContext()));
        videoView.start();
    }

    @Override
    protected void onResume() {
        super.onResume();
        VideoView videoView = findViewById(R.id.videoView);
        videoView.resume();
        if (currentPosition > 0)
            videoView.seekTo(currentPosition);
    }

    @Override
    protected void onPause() {
        VideoView videoView = findViewById(R.id.videoView);
        if (videoView.canPause()) {
            currentPosition = videoView.getCurrentPosition();
            videoView.pause();
        }
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        VideoView videoView = findViewById(R.id.videoView);
        videoView.stopPlayback();
        super.onDestroy();
    }
}


> 사용된 MP4 소스 :
https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4






2. 샘플코드
소스코드
APK


2019년 3월 13일 수요일

안드로이드 데이터 바인딩 예제 - Android Jetpack

- 레퍼런스
https://developer.android.com/topic/libraries/data-binding?hl=ko



1. 데이터 바인딩
- gradle.properties 설정
android.databinding.enableV2=true


- xml 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="app.example.databindingapp.BuildConfig" />
        <variable
            name="listAdapter"
            type="app.example.databindingapp.ListItemAdapter" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/packageName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="16dp"
            android:text="@{BuildConfig.APPLICATION_ID}"
            android:textColor="@android:color/black"
            android:textSize="16dp" />


        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/packageName"
            android:adapter="@{listAdapter}" />
    </RelativeLayout>
</layout>


> layout 안에 data와 실제 레이아웃 구성으로 나눠짐
> import와 variable 구분 할 것.
> 자세한 문법은 구글 레퍼런스 쪽에서 확인
> 바인딩을 사용하는 View는 id를 지정할 것.


- Activity
ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

> setContentView 대신 DataBindingUtil의 setContentView를 사용한다.
> xml 레이아웃에서 데이터 바인딩을 정의하면 generatedJava 쪽에 코드가 자동 생성된다.
> 생성이 안되는 경우 Rebuild Project로 자동 생성을 시도하면 된다.



2. ListView를 데이터 바인딩으로 구성하기
- row_text_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="item"
            type="java.lang.String" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="16dp"
            android:text="@{item}"
            android:textColor="@android:color/black"
            android:textSize="16dp" />
    </RelativeLayout>
</layout>


- activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="android.view.View" />
        <import type="app.example.databindingapp.BuildConfig" />
        <variable
            name="listAdapter"
            type="app.example.databindingapp.ListItemAdapter" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/packageName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="16dp"
            android:text="@{BuildConfig.APPLICATION_ID}"
            android:textColor="@android:color/black"
            android:textSize="16dp" />

        <TextView
            android:id="@+id/versionName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:padding="16dp"
            android:text="@{BuildConfig.VERSION_NAME}"
            android:textColor="@android:color/black"
            android:textSize="16dp" />

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_above="@id/mode"
            android:layout_below="@id/packageName"
            android:adapter="@{listAdapter}" />

        <TextView
            android:id="@+id/mode"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:background="@android:color/black"
            android:gravity="center"
            android:padding="16dp"
            android:text="DEBUG APP"
            android:textColor="@android:color/white"
            android:textSize="16dp"
            android:visibility="@{BuildConfig.DEBUG ? View.VISIBLE : View.GONE}" />
    </RelativeLayout>
</layout>


- MainActivity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        ArrayList<String> list = new ArrayList<>();
        list.add("Hello");
        list.add("World");
        list.add("Android");
        ListItemAdapter listItemAdapter = new ListItemAdapter(getBaseContext(), list);
        activityMainBinding.setListAdapter(listItemAdapter);
    }
}

> 데이터 바인딩으로 처리하면 전체 코드는 매우 간결해진다.
> 데이터 바인딩을 적절하게 사용하면 편리하지만 단순히 코드의 양을 줄이기 위한 용도로 사용한다면 부작용이 발생한다.


3. 샘플코드
소스코드
APK





안드로이드 PIP 예제 - Picture In Picture

- PIP 지원
https://developer.android.com/guide/topics/ui/picture-in-picture?hl=ko
https://developer.android.com/about/versions/oreo/android-8.0?hl=ko#opip



1. 기본구성
- AndroidManifest.xml
<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">


- 미디어 플레이어 코드가 구현된 상태에서 Actvity 추가사항들.
PictureInPictureParams.Builder pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
Rational aspectRatio = new Rational(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
PictureInPictureParams build = pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(build);

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
Log.d(TAG, "onPictureInPictureModeChanged : " + isInPictureInPictureMode);
}


> PIP 동작 상태가 변경되면 onPictureInPictureModeChanged 메소드가 호출됩니다.
> enterPictureInPictureMode 메소드를 호출하면 PIP 모드로 동작합니다.


- Activity 전체코드
public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();
    private String mediaURL = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4";
    private MediaPlayer mediaPlayer;
    private boolean minimize;

    private final Object pictureInPictureParamsBuilder;
    {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
        } else {
            pictureInPictureParamsBuilder = null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportActionBar().hide();

        setContentView(R.layout.activity_main);


        SurfaceView surfaceView = findViewById(R.id.surfaceView);
        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                initMediaPlayer(holder);
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });

        View actionPIP = findViewById(R.id.actionPIP);
        actionPIP.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? View.VISIBLE : View.GONE);
        actionPIP.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                minimize();
            }
        });
    }

    private void initMediaPlayer(SurfaceHolder holder) {
        holder.getSurface();
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                mp.start();
            }
        });
        mediaPlayer.setSurface(holder.getSurface());

        try {
            mediaPlayer.setDataSource(getBaseContext(), Uri.parse(mediaURL));
            mediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void play() {
        if (mediaPlayer != null) {
            if (mediaPlayer.isPlaying() == false) {
                mediaPlayer.start();
            }
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        play();
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (minimize == false)
            if (mediaPlayer != null) {
                mediaPlayer.pause();
            }
    }

    @Override
    protected void onDestroy() {
        if (mediaPlayer != null) {
            mediaPlayer.release();
        }
        super.onDestroy();
    }


    /**
     * PIP 모드
     */
    private void minimize() {
        if (mediaPlayer == null) {
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (pictureInPictureParamsBuilder instanceof PictureInPictureParams.Builder) {
                Rational aspectRatio = new Rational(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
                PictureInPictureParams build = ((PictureInPictureParams.Builder) pictureInPictureParamsBuilder).setAspectRatio(aspectRatio).build();
                minimize = true;
                enterPictureInPictureMode(build);
            }
        }
    }


    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode);
        Log.d(TAG, "onPictureInPictureModeChanged : " + isInPictureInPictureMode);
        if (isInPictureInPictureMode) {
            play();
        } else {
            minimize = false;
        }
    }
}



3. 샘플코드
소스코드
APK

  


샘플 mp4 영상
https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4

구글 샘플코드
https://github.com/googlesamples/android-PictureInPicture/#readme