반응형

오늘은 간단하게 코틀린(Kotlin)에서 시계 만드는 방법을 알아보겠습니다. 대부분 개발 언어에서 시계를 만들기 위해서는 Thread를 사용해서 시간을 확인하는 로직을 많이 사용합니다. 하지만, Android에서는 Thread를 사용하지 않고 Widget만을 사용해서 시계를 구현할 수 있습니다.

특별한 코드 없이 Activity_main.xml에 TextClock를 선언하면 바로 시계가 동작합니다.

    <TextClock
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:format12Hour="hh:mm:ss a"
        android:format24Hour="hh:mm:ss"
        android:textSize="100sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

다양한 옵션이 있지만 기본적으로 시간을 표시하는 format12hour, format24hour 함수만 확인하면 됩니다.

실행 결과 아무 작업 없시도 시계가 완성되었습니다. 초단위로 움직이지만 App에 리소스를 많이 사용하지 않습니다. 초보자가 사용하기 정말 좋은 Widget라고 생각됩니다.

TextClock에 ID를 설정하고 main함수에서 포맷을 변경 할 수 있습니다.

setFormat12Hour(CharSequence) 함수를 사용해서 출력 포맷 변경이 가능합니다.

시간만 표시할 경우는 "hh:mm:ss a"를 사용하고 연월일을 표시할 경우 "yyyy-MM-dd hh:mm:ss a"를 사용합니다.

        val clock:TextClock = findViewById<TextClock>(R.id.mainclock)

        clock.setFormat12Hour("yyyy-MM-dd hh:mm:ss a")

setFormat12Hour에 CharSequence를 입력합니다.

출력 결과 연월일 시간을 모두 한 번에 확인할 수 있습니다.

TextClock는 다른 Widget과 동일하게 text 정보를 size, color 모두 변경 가능합니다.

TextClock 속성에서 textColor을 사용해서 텍스트 색상을 변경할 수 있습니다.

출력 결과 TextClock 정보 text 색상이 변경되었습니다.

TextClock Widget은 복잡한 시간 정보를 Thread 사용 없이 쉽게 사용할 수 있는 좋은 객체입니다.

Android에서 시계가 필요하다면 힘들게 만들지 마시고 TextClock를 사용해주세요.

감사합니다.

반응형
반응형

안드로이드는 다양한 레이아웃 파일을 생성해서 위젯을 자유롭게 변경할 수 있습니다.

오늘은 코틀린(Kotlin) 안드로이드에서 상태를 확인할 수 있는 프로그래스 바를 커스텀해서 원형으로 만들어 보겠습니다.

Custom Circular Progress bar를 만들기위해서 두 개의 레이아웃을 사용해보겠습니다.

원형 상태바(Circular Progress Bar)는 백그라운드를 사용해서 상태바(Progress Bar) 배경을 구현하고, 레이아웃을 로드해서 상태바(Progress Bar)를 디자인해보겠습니다.

먼저 drawable 하위 폴더에 progress_background.xml을 생성합니다.

생성된 xml에 shape를 사용해서 rectangle을 타원형으로 구성합니다.

bottomRightRadius, topRightRadius, topLeftRadius, BottomLeftRadius 속성을 사용해서 각 모서리를 타원형으로 구성합니다.

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#FF0089"/>
    <size
        android:width="5dp"
        android:height="10dp"/>
    <corners
        android:bottomRightRadius="40dp"
        android:topRightRadius="40dp"
        android:topLeftRadius="40dp"
        android:bottomLeftRadius="40dp"/>
</shape>

shape를 사용해면 다양한 모형을 그릴 수 있습니다.

이번에는 원형 상태바(Circular Progress Bar)를 만들기 위해서 상태바(Progress Bar) 레이아웃을 만들어야 합니다.

drawable 폴더 아래에 main_progressbar.xml을 생성합니다.

<?xml version="1.0" encoding="utf-8"?>
<rotate
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="270"
    android:toDegrees="270" >

    <shape
        android:innerRadiusRatio="2.5"
        android:shape="ring"
        android:thickness="5dp"
        android:useLevel="true">

        <gradient
            android:angle="0"
            android:endColor="@color/colorPrimary"
            android:centerColor="@color/colorPrimary"
            android:startColor="@color/colorPrimary"
            android:type="sweep"
            android:useLevel="false"/>

    </shape>

</rotate>

rotate를 사용해서 원형 상태바(Circular Progress Bar)를 디자인합니다.

shap 속성에서는 상태바(Progress Bar) 크기를 설정할 수 있습니다. gradinent는 상태바(Progress Bar) 내부 색상을 설정할 수 있습니다.

마지막으로 drawable에 생성된 progress_background.xml, main_progressbar.xml을 사용해서 Custom 원형 상태바(Circular Progress Bar)를 구성해보겠습니다.

activity_main.xml에 ProgressBar, Button을 등록합니다.

<Button
        android:id="@+id/btn_progress"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="progress"/>
    
   <ProgressBar
       android:id="@+id/progressbar"
       android:layout_width="match_parent"
       android:layout_height="400dp"
       android:background="@drawable/progress_background2"
       android:progressDrawable="@drawable/main_ptogressbar"
       android:indeterminateOnly="false"
       android:max="120"
       android:progress="0"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

Button은 원형 상태바(Circular Progress Bar)를 실행하기 위한 이벤트 버튼입니다.

상태바(ProgressBar) 속성 중에서 background에 progress_background.xml을 입력합니다.

progressDrawable 속성에는 원형 이미지 main_progressbar을 입력합니다.

이제 마지막으로 이벤트를 사용해서 원형 상태바(Circular Progress Bar)를 실행해야 합니다.

MainActivity로 이동해서 아래 코드를 입력합니다.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val buttonStart = findViewById<Button>(R.id.btn_progress)
        val CProgressBar = findViewById<ProgressBar>(R.id.progressbar)
        var iPos : Int = 10
        var iMaxValue : Int = 0

        iMaxValue = CProgressBar.max


        buttonStart.setOnClickListener{
            if( iMaxValue <= iPos)
            {
                CProgressBar.visibility =View.GONE
                Toast.makeText(this,"완료됨",Toast.LENGTH_SHORT).show()
                iPos = 0
                CProgressBar.setProgress(iPos)
            }
            else
            {
                if( 0 == iPos )
                {
                    CProgressBar.visibility = View.VISIBLE
                }
                iPos += 10
                CProgressBar.setProgress(iPos)
            }
        }

    }

findViewById 함수를 사용해서 버튼, 상태바를 선언합니다.

원형 상태바(Circular Progress Bar)는 setProgress 함수를 사용해서 이동할 수 있습니다.

상태바 함수 max를 사용해서 최댓값을 확인하고 상태바 증가 값이 최댓값과 같다면 초기화하는 구조입니다.

코트린 안드로이드 실행 후 상단 버튼을 클릭하면 원형 상태바(Circular Progress Bar)가 회전하면서 변경됩니다.

백그라운드(background)는 shape를 사용해서 라운드형 사각형을 확인할 수 있습니다.

상태바(ProgressBar)는 다양한 기능으로 사용되면서 자유롭게 커스텀할 수 있어 안드로이드 앱을 더욱더 효과적으로 표현할 수 있는 좋은 기술입니다.

감사합니다.

반응형
반응형

 

코틀린(Kotlin)을 사용한 파일 탐색기 만들기 마지막 시간으로 오늘은 메뉴를 추가해서 신규 파일 및 폴더를 생성해보겠습니다.

메뉴를 등록하기 위해서 먼저 메뉴 리소스를 생성해야 합니다.

리소스 파일로 이동해서 menu 폴더 생성 후 main_menu.xml 파일을 생성합니다.

<?xml version="1.0" encoding="utf-8"?>
<menu 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"
    tools:context=".main.SecuExplorer">

    <item
        android:id="@+id/menuCancel"
        android:icon="@drawable/ic_close_grey_24dp"
        android:title="Cancel"
        app:showAsAction="always" />

    <item
        android:id="@+id/menuPasteFile"
        android:icon="@drawable/ic_content_paste_grey_24dp"
        android:title="Paste"
        app:showAsAction="always" />

    <item
        android:id="@+id/subMenu"
        android:icon="@drawable/ic_folder_grey_24dp"
        android:title="Options"
        app:showAsAction="always"
        tools:ignore="AlwaysShowAction">
        <menu>
            <item
                android:id="@+id/menuNewFile"
                android:icon="@drawable/ic_insert_drive_file_grey_24dp"
                android:title="New File"
                app:showAsAction="never" />
            <item
                android:id="@+id/menuNewFolder"
                android:icon="@drawable/ic_create_new_folder_grey_24dp"
                android:title="New Folder"
                app:showAsAction="never" />
        </menu>
    </item>

</menu>

main_menu.xml 파일에 각 메뉴 아이콘 이미지 및 메뉴 정보를 입력합니다.

icon 메뉴는 xml 정보를 사용해서 vector로 구성되어 있습니다. 없을 경우 직접 xml을 생성해서 복사 붙여 넣기 해주시면 됩니다.

이제 메뉴를 연동하기 위해서 메인 Activity에 onCreateOptionMenu, onOptionsItemSelected 함수를 override 합니다.

앱을 실행하면 상단에 메뉴가 출력되는 것을 확인할 수 있습니다. 

이제 메뉴를 클릭하면 실행할 수 있는 함수를 onOptonsItemSelected 함수에 연동해줍니다.

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.main_menu, menu)
        return super.onCreateOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {

        when( item?.itemId){
            R.id.menuNewFile -> createNewFileInCurrentDirectory()
            R.id.menuNewFolder-> createNewFolderInCurrentDirectory()
        }

        return super.onOptionsItemSelected(item)
    }

    private fun createNewFileInCurrentDirectory(){

    }

    private fun createNewFolderInCurrentDirectory(){

    }

각 메뉴 ID에 파일 및 폴더 생성 함수를 연결하면 이벤트가 함수를 호출하게 됩니다.

이제 파일을 생성하기 위한 Dialog를 구현하겠습니다. layout 아래에 dialog_enter_name.xml를 생성합니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <TextView
        android:id="@+id/enterNameTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Enter a name"
        android:textSize="20sp" />

    <EditText
        android:id="@+id/nameEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        app:layout_constraintLeft_toLeftOf="parent"
        android:lines="1"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/enterNameTextView"
        android:inputType="text" />

    <Button
        android:id="@+id/createButton"
        style="@style/PrimaryButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="Create"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/nameEditText" />

</androidx.constraintlayout.widget.ConstraintLayout >

파일 생성에 필요한 Control를 constrainlayout에 배치합니다. 

BottomSheetDialog를 호출하기 위해서 main Activity로 이동해서 createNewFileInCurrentDirectory 함수에 아래 내용을 입력합니다.

private fun createNewFileInCurrentDirectory(){
        val bottomSheetDialog = BottomSheetDialog(this)
        val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null)
        view.createButton.setOnClickListener{
            val fileName = view.nameEditText.text.toString()
        }
        bottomSheetDialog.setContentView(view)
        bottomSheetDialog.show()
    }

앱을 실행해서 "New File" 메뉴를 클릭하면 하단에 File name을 입력할 수 있는 Dialog가 출력됩니다.

Dialog 출력 후 "CREATE" 버튼을 클릭하면 선택한 위치에 파일을 생성해야 합니다. 기존에 생성한 파일 관리 FileType.kt로 이동 후 createNewFile 함수를 입력합니다.

fun createNewFile(fileName : String, path: String, callback: (result:Boolean, message: String) -> Unit){
    val fileAlreadyExists = File(path).listFiles().map{ it.name}.contains(fileName)

    if( fileAlreadyExists){
        callback(false, "'{$fileName}' already exists")
    } else{
        val file = File(path, fileName)
        try{
            val result = file.createNewFile()
            if(result){
                callback(result, "File '${fileName}' created successfully.")
            } else{
                callback(result, "Unable to create file '${fileName}'.")
            }
        }catch( e: Exception){
            callback(false, "Unable to create file. Please try again.")
            e.printStackTrace()
        }
    }
}

createNewFile 함수는 callback 이벤트를 사용해서 파일 생성 여부를 판단 후 파일을 생성합니다.

main Activity로 이동해서 createNewFileInCurrentDirectory 함수로 이동해서 파일을 생성합니다.

  private fun createNewFileInCurrentDirectory(){
        val bottomSheetDialog = BottomSheetDialog(this)
        val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null)
        view.createButton.setOnClickListener{
            val fileName = view.nameEditText.text.toString()

            if (fileName.isNotEmpty()) {
                createNewFile(fileName, backStackManager.top.path) { _, message ->
                    bottomSheetDialog.dismiss()
                }
            }
        }
        bottomSheetDialog.setContentView(view)
        bottomSheetDialog.show()
    }

파일 및 폴더 생성 정보를 확인하기 위해서 전체 메시지를 보낼 수 있는 BroadcastReceiver 클래스를 생성합니다.

class FileChangeBroadcastReceiver(val path: String, val onChange: () -> Unit) : BroadcastReceiver() {

    companion object {
        const val EXTRA_PATH = "com.office.secuex.fileservice.path"
    }

    override fun onReceive(context: Context?, intent: Intent?) {
        val filePath = intent?.extras?.getString(EXTRA_PATH)
        if (filePath.equals(path)) {
            onChange.invoke()
        }
    }
}

FileChangeBroadcastReceiver 함수를 사용하기 위해서 기존에 생성한 FilesListFragment로 이동합니다.

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val filePath = arguments?.getString(ARG_PATH)
        if (filePath == null) {
            Toast.makeText(context, "Path should not be null!", Toast.LENGTH_SHORT).show()
            return
        }
        PATH = filePath

        mFileChangeBroadcastReceiver = FileChangeBroadcastReceiver(PATH) {
            updateDate()
        }
    }

    override fun onResume() {
        super.onResume()
        context?.registerReceiver(mFileChangeBroadcastReceiver, IntentFilter(getString(R.string.file_change_broadcast)))
    }

    override fun onPause() {
        super.onPause()
        context?.unregisterReceiver(mFileChangeBroadcastReceiver)
    }

앱 시퀀스에 따라서 이벤트를 확인할 수 있는 로직을 추가해서 파일 및 폴더를 생성할 경우 갱신되도록 구현합니다.

private fun createNewFileInCurrentDirectory() {
        val bottomSheetDialog = BottomSheetDialog(this)
        val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null)
        view.createButton.setOnClickListener {
            val fileName = view.nameEditText.text.toString()
            if (fileName.isNotEmpty()) {
                createNewFile(fileName, backStackManager.top.path) { _, message ->
                    bottomSheetDialog.dismiss()
                    Toast.makeText(this, message, Toast.LENGTH_LONG).show()
                    updateContentOfCurrentFragment()
                }
            }
        }
        bottomSheetDialog.setContentView(view)
        bottomSheetDialog.show()
    }

    private fun createNewFolderInCurrentDirectory(){
        val bottomSheetDialog = BottomSheetDialog(this)
        val view = LayoutInflater.from(this).inflate(R.layout.dialog_enter_name, null)
        view.createButton.setOnClickListener {
            val fileName = view.nameEditText.text.toString()
            if (fileName.isNotEmpty()) {
                createNewFolder(fileName, backStackManager.top.path) { _, message ->
                    bottomSheetDialog.dismiss()
                    //coordinatorLayout.createShortSnackbar(message)
                    Toast.makeText(this, message, Toast.LENGTH_LONG).show()
                    updateContentOfCurrentFragment()
                }
            }
        }
        bottomSheetDialog.setContentView(view)
        bottomSheetDialog.show()
    }



    private fun updateContentOfCurrentFragment() {
        val broadcastIntent = Intent()
        broadcastIntent.action = applicationContext.getString(R.string.file_change_broadcast)
        broadcastIntent.putExtra(FileChangeBroadcastReceiver.EXTRA_PATH, backStackManager.top.path)
        sendBroadcast(broadcastIntent)
    }

main Activity 클래스에 createNewFile, createNewFolder 함수에 각 파일을 생성할 수 있는 함수를 연동합니다.

함수 연동 후 메뉴를 클릭해서 이름을 입력하면 파일 및 폴더가 생성되면서 메시지가 출력됩니다.

파일 삭제

파일 탐색기에서 파일을 삭제하기 위해서는 간단하게 길게 클릭하는 이벤트를 사용해서 Dialog를 출력 후 처리하게 로직을 추가합니다.

파일 삭제 정보를 입력할 수 있는 FileOptionDialog 클래스를 생성합니다.

class FileOptionsDialog : BottomSheetDialogFragment() {

    var onDeleteClickListener: (() -> Unit)? = null
    var onCopyClickListener: (() -> Unit)? = null

    companion object {
        fun build(block: Builder.() -> Unit): FileOptionsDialog = Builder().apply(block).build()
    }

    class Builder {
        var path: String? = null

        fun build(): FileOptionsDialog {
            val fragment = FileOptionsDialog()
            val args = Bundle()
            fragment.arguments = args
            return fragment
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.dialog_file_options, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initViews()
    }

    private fun initViews() {
        deleteTextView.setOnClickListener {
            onDeleteClickListener?.invoke()
            dismiss()
        }

        copyTextView.setOnClickListener {
            onCopyClickListener?.invoke()
            dismiss()
        }
    }
}

OptionDialog 클래스에 컨트롤러를 연결하기 위해서 layout dialog_file_options.xml을 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:paddingBottom="24dp">

    <TextView
        android:id="@+id/fileOptionsTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="24dp"
        android:text="Options"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/deleteTextView"
        style="@style/FileDialogOption"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:drawableLeft="@drawable/ic_delete_grey_24dp"
        android:drawableStart="@drawable/ic_delete_grey_24dp"
        android:text="Delete"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/fileOptionsTextView" />

    <TextView
        android:id="@+id/copyTextView"
        style="@style/FileDialogOption"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/ic_content_copy_grey_24dp"
        android:drawableStart="@drawable/ic_content_copy_grey_24dp"
        android:text="Copy"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/deleteTextView" />

</androidx.constraintlayout.widget.ConstraintLayout>

main Activity로 이동 후 OnLongClick 함수에 optionDialog 클래스를 연동합니다.

 companion object {
        private const val OPTIONS_DIALOG_TAG: String = "com.com.office.secuex.main.options_dialog"
    }
override fun onLongClick(fileModel: FileModel) {
        val optionsDialog = FileOptionsDialog.build {}

        optionsDialog.onDeleteClickListener = {
            FileUtilsDeleteFile(fileModel.path)
            updateContentOfCurrentFragment()
        }

        optionsDialog.show(supportFragmentManager, OPTIONS_DIALOG_TAG)
    }

마지막으로 파일 삭제 함수를 FileType.kt에 입력합니다.

fun deleteFile(path: String) {
    val file = File(path)
    if (file.isDirectory) {
        file.deleteRecursively()
    } else {
        file.delete()
    }
}

deleteFile 함수는 이름 변경을 사용해서 직접 import 합니다.

import com.office.secuex.common.deleteFile as FileUtilsDeleteFile

파일 탐색기에서 파일을 선택하고 길게 클릭하면 하단에 optionDilaog가 출력됩니다.

"Delete" 메뉴를 클릭하면 폴더 및 파일이 삭제됩니다.

파일 탐색기는 다양한 기능이 포함되어 있는 앱으로 처음 코틀린(Kotlin) 안드로이드를 공부하기 매우 좋은 앱입니다. 

파일 탐색기 기본 소스를 사용해서 다양하게 변형된 기능 앱을 지금 부터 개발해보세요.

감사합니다.

 

Build a File Explorer in Kotlin - Part 5 - Creating/Deleting files and folders - TheTechnoCafe

Till now you have only read files. In this part of Kotlin File Explorer Series you will learn how to create and delete file and folders.

thetechnocafe.com

 

반응형
반응형

오늘은 코틀린(Kotlin)을 사용해서 파일 탐색기 만들기에 경로 확인 ToolBar을 추가해보겠습니다.

파일 탐색기를 사용해서 파일을 이동하면 현재 위치를 알 수 없는 단점을 보안하기 위해서 상단에 ToolBar를 사용해서 현재 경로를 저장할 수 있는 로직을 구현해보겠습니다.

먼저 ToolBar에 적용하기 위한 layout을 생성합니다.

layout 하단에 item_file_breadcrumb.xml 파일을 생성합니다.

Android 버전에 따라서 ConstraintLayout는 패키지 정보를 수정해야 합니다.

수정을 안할 경우 실행 시 App이 바로 종료되는 문제가 발생합니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
    android:layout_height="wrap_content"
    android:background="?selectableItemBackgroundBorderless"
    android:padding="8dp">

    <ImageView
        android:id="@+id/arrowTextView"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:src="@drawable/ic_folder_dark_24dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:tint="@color/grey"/>

    <TextView
        android:id="@+id/nameTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="4dp"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/arrowTextView"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Pictures" />

</androidx.constraintlayout.widget.ConstraintLayout>

ToolBar는 이미지를 출력하기 위한 ImageView 및 경로를 출력하기 위한 TextView로 구성해줍니다.

ImageView에 사용할 이미지를 Vector을 사용해서 추가합니다.

VectorDrawable는 정적 드로어블 객체로 path 및 group 객체로 구성할 수 있습니다.

https://developer.android.com/guide/topics/graphics/vector-drawable-resources?hl=ko 

 

벡터 드로어블 개요  |  Android 개발자  |  Android Developers

이 문서에서는 프레임워크 API 또는 지원 라이브러리를 통해 벡터 드로어블 리소스의 전반적인 사용을 설명합니다.

developer.android.com

drawable 하단에 ic_right_black_24dp.xml을 추가합니다.

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
</vector>

vector을 사용해서 path를 구성합니다.

색상을 추가하기 위해서 colors.xml에 grey를 추가합니다.

이번에는 모든 경로를 확인하기 위한 RecyclerView.Adapter를 확장한 BreadcrumbFileAdapter 클래스를 생성합니다.

class BreadcrumbFileAdapter : RecyclerView.Adapter<BreadcrumbFileAdapter.ViewHolder>() {

    var onItemClickListener: ((FileModel) -> Unit)? = null

    var files = listOf<FileModel>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_file_breadcrumb, parent, false)
        return ViewHolder(view)
    }

    override fun getItemCount() = files.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bindView(position)

    fun updateData(files: List<FileModel>) {
        this.files = files
        notifyDataSetChanged()
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {

        init {
            itemView.setOnClickListener(this)
        }

        override fun onClick(v: View?) {
            onItemClickListener?.invoke(files[adapterPosition])
        }

        fun bindView(position: Int) {
            val file = files[position]
            itemView.nameTextView.text = file.name
        }
    }
}

사용자가 선택한 파일 목록을 업데이트 하기 위해서 클래스 내부에 있는 RecyclerView.Adapter를 사용합니다.

이제 경로를 확인하기 위한 layout을 activity_main.xml에 추가합니다.

<com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:title="@string/app_name"
            app:titleTextColor="@color/grey">

        </androidx.appcompat.widget.Toolbar>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/breadcrumbRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        </androidx.recyclerview.widget.RecyclerView>

    </com.google.android.material.appbar.AppBarLayout>

Toolbar widget를 사용하기 위해서 AppBarLayout를 사용합니다.

최신 Android 버전에서는 패키지가 변경되어 패키지 수정이 필요합니다.

패키지 오류가 발생할 경우 아래 사이트를 확인해주세요.

https://believecom.tistory.com/750?category=1109462 

 

android How to solve ' java.lang.ClassNotFoundException: Didn't find class "com.google.android.material.appbar.AppBarLayout"'

Android에서 AppBarLayout을 추가하면 컴파일에는 문제가 없지만 실행 시점 오류가 발생합니다. 오류 내용은 'java.lang.ClassNotFoundException: Didn't find class "com.google.android.material.appbar.Ap..

believecom.tistory.com

정상적으로 소스를 추가 했다면 아래 화면을 확인할 수 있습니다.

상단에 텍스트가 추가된 ToolBar가 적용되어 있습니다.

파일 탐색기를 사용하면 다양하게 이동할 수 있기 때문에 모든 이벤트 내용을 저장해서 위치를 확인해야 합니다.

모든 내용을 저장하기 위해서 BackStackManager 클래스를 생성하겠습니다.

main 패키지 하단에 BackStackManager.kt를 추가해주세요.

class BackStackManager {
    private var files = mutableListOf<FileModel>()
    var onStackChangeListener: ((List<FileModel>) -> Unit)? = null

    val top: FileModel
        get() = files[files.size - 1]

    fun addToStack(fileModel: FileModel) {
        files.add(fileModel)
        onStackChangeListener?.invoke(files)
    }

    fun popFromStack() {
        if (files.isNotEmpty())
            files.removeAt(files.size - 1)
        onStackChangeListener?.invoke(files)
    }

    fun popFromStackTill(fileModel: FileModel) {
        files = files.subList(0, files.indexOf(fileModel) + 1)
        onStackChangeListener?.invoke(files)
    }
}

BackStackManager 클래스는 3개의 함수로 운영됩니다.

files 변수를 사용해서 파일 이동 경로를 저장합니다.

onStackChangeListener 이벤트를 사용해서 수진 정보를 확인합니다.

top 변수는 최상단 파일 정보를 리턴하게 적용합니다.

이제 BackStackManager을 적용하기 위해서 MainActivity로 이동합니다.

backStackManager 변수를 선언 합니다.

 private fun initViews() {
        setSupportActionBar(toolbar)

        breadcrumbRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        mBreadcrumbFileAdapter = BreadcrumbFileAdapter()
        breadcrumbRecyclerView.adapter = mBreadcrumbFileAdapter
        mBreadcrumbFileAdapter.onItemClickListener = {
            supportFragmentManager.popBackStack(it.path, 2);
            backStackManager.popFromStackTill(it)
        }
    }
    private fun initBackStack(){
        backStackManager.onStackChangeListener = {
            updateAdapterData(it)
        }
        backStackManager.addToStack(fileModel = FileModel(Environment.getExternalStorageDirectory().absolutePath, FileType.FOLDER, "/", 0.0))
    }
    
    private fun updateAdapterData(files:List<FileModel>){
        mBreadcrumbFileAdapter.updateData(files)
        if(files.isNotEmpty()){
            breadcrumbRecyclerView.smoothScrollToPosition(files.size - 1)
        }
    }

toolbar 및 backStack를 초기화하기 위한 initViews(), InitBackStack() 함수를 생성합니다.

InitViews() 함수는 처음에 생성한 BreadcrumbFileAdapter을 사용해서 layout View에 연동합니다.

initBackStack() 함수는 BackStackManager가 동작할 경우 업데이트를 실행합니다.

이제 마지막으로 폴더를 선택할 경우 backStackManager를 연동해서 파일 정보를 업데이트합니다.

AddFileFragment(), onBackPressed() 함수에  backStackManager 클래스 메서드를 호출합니다.

이제 파일 탐색기에서 폴더를 이동하면 상단에 폴더 경로를 한눈에 확인할 수 있습니다.

폴더 경로 정보는 매우 단순한 내용처럼 보이지만 파일 이동에 따른 중요한 요소입니다.

오늘은 파일 탐색기 4번째 시간으로 파일 경로를 추가했습니다.

감사합니다.

http://thetechnocafe.com/build-a-file-explorer-in-kotlin-part-4-adding-breadcrumbs/

 

Build a File Explorer in Kotlin – Part 4 – Adding Breadcrumbs - TheTechnoCafe

In this File Explorer tutorial you will add the functionality which enables the user to navigate back to any position in the backstack.

thetechnocafe.com

 

반응형
반응형

파일 탐색기를 사용하면 파일과 폴더 타입에 따라서 폴더 이동이 가능해야 합니다.

오늘은 코틀린(Kotlin)을 이용한 탐색기 세 번째 파트 파일 탐색기 이벤트 연동에 대해서 알아보겠습니다.

파일 탐색기 이벤트 연동은 파일, 폴더를 구분해서 재 탐색을 해야 합니다.

먼저 FilesListFragment에서 OnItemClickListener 이벤트 변수를 선언합니다.

아래쪽에 interface를 사용해서 onClick, onLogClick 함수를 선언합니다.

private lateinit var eCallback: OnItemClickListener

    interface OnItemClickListener{
        fun onClick(fileModel : FileModel)

        fun onLongClick(fileModel : FileModel)
    }

override onAttach를 선언하고 OnItemClickListener로 캐스팅하여 eCallback에 연결합니다.

onAttach는 실행 시점에 호출되기 때문에 오류를 방지하기 위해서 catch를 사용해서 오류를 정의합니다.

 override fun onAttach(context: Context?) {
        super.onAttach(context)

        try{
            eCallback = context as OnItemClickListener
        } catch( e: Exception){
            throw Exception("${context} FileListFragment onAttach")
        }
    }

FilesListFragment 클래스를 MainActivity에 상속합니다.

클래스명을 선언하면 OnItemClickListener 이벤트를 확인할 수 있습니다.

FilesListFragment 클래스는 interface로 onClick, onLongClick를 선언했기 때문에 상속을 받게 되면 interface를 선언해야 오류가 발생하지 않습니다.

onClick, onLongClick를 override하여 선언합니다.

onClick이벤트가 발생하면 container에 View를 실행하고 파일 정보를 넘겨줄 수 있는 AddFileFragment를 선언합니다.

	override fun onClick(fileModel: FileModel) {
       
    }

    override fun onLongClick(fileModel: FileModel) {
       
    }

    private fun AddFileFragment(fileModel: FileModel){
        val filesListFragment = FilesListFragment.build {
            path = fileModel.path
        }

        val fragmentTransaction = supportFragmentManager.beginTransaction()
        fragmentTransaction.replace(R.id.container, filesListFragment)
        fragmentTransaction.addToBackStack(fileModel.path)
        fragmentTransaction.commit()
    }

onClick 이벤트에 폴더일 경우 파일 경로를 저장하기 위해서 AddFileFragment를 연결합니다.

스택에 더 이상 확인할 내용이 없을 경우 종료할 수 있게 onBackPressed를 override 해줍니다.

override fun onBackPressed(){
        super.onBackPressed()
        if( supportFragmentManager.backStackEntryCount ==0){
            finish()
        }
    }

파일을 선택하면 파일 확장자에 연결된 앱을 실행하기 위해서 이벤트를 연동해야 합니다.

FileUtils.kt파일에 앱을 실행할 수 있는 함수 launchFileIntent를 선언합니다.

fun Context.launchFileIntent(fileModel: FileModel) {
    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = FileProvider.getUriForFile(this, packageName, File(fileModel.path))
    intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    startActivity(Intent.createChooser(intent, "Select Application"))
}

launchFileintent 함수는 fileModel 경로에 확인된 확장자에 따라서 설치된 앱을 실행합니다.

폴더가 아닐 경우 launchFileIntent 함수를 사용해서 앱 실행을 적용합니다.

override fun onClick(fileModel: FileModel) {
        if( fileModel.fileType == FileType.FOLDER){
            AddFileFragment(fileModel)
        }else{
            launchFileIntent(fileModel)
        }
    }

onClick 함수는 폴더일 경우 AddFileFragment를 재 호출하여 리스트를 생성합니다.

파일 경우 launchFileIntent 함수를 사용해서 앱을 실행하고 뒤로 가기를 클릭하면 다시 폴더 리스트를 호출합니다.

마지막으로 InitViews함수에서 초기화 시점에 mFilesAdapter에 클릭 이벤트를 연동합니다.

mFilesAdapter.onItemClickListener = {
            eCallback.onClick(it)
        }

        mFilesAdapter.onItemLongClickListener = {
            eCallback.onLongClick(it)
        }

onItemClickListener, onItemLongClickListener 함수에 eCallback 이벤트를 연동하면 리스트 클릭 시점에 eCallback가 호출되면서 이벤트가 연동됩니다.

컴파일해서 실행하면 리스트에서 하위 폴더가 있을 경우 리스트가 변경되는 것을 확인할 수 있습니다.

폴더 하단에 아무것도 없을 경우는 "there is nothing here" View를 확인할 수 있습니다.

파일을 클릭하면 연결된 앱 실행 여부를 확인하고 앱을 실행할 수 있습니다.

탐색기를 사용한 이벤트 연동은 어렵게 보이지만, 기본은 간단한 이벤트 연동이기 때문에 조금만 보면 바로 확인할 수 있습니다.

다음 시간에는 파일 경로를 확인할 수 있는 로직을 추가해보겠습니다.

감사합니다.

 

 

http://thetechnocafe.com/build-a-file-explorer-in-kotlin-part-3-navigating-through-file-system/

 

Build a File Explorer in Kotlin – Part 3 – Navigating through File System - TheTechnoCafe

Welcome to the 3rd part of the Kotlin File Explorer series. Good job reading files from a path in the previous tutorial. Right now our application takes...

thetechnocafe.com

 

반응형
반응형

스마트폰을 사용해서 다양한 파일을

관리하기 위해서 파일 탐색기를 많이 사용합니다.

파일 탐색기는 저장된 스토리지

정보를 모두 확인하기 위한 다양한 기술이

필요하기 때문에 프로그램 개발에서는

반드시 알아야 하는 프로그램입니다.

 

오늘은 코틀린(Kotlin)을 사용한 파일, 폴더를

관리할 수 있는 파일 탐색기(explorer)를

알아보겠습니다.

 

일반적인 파일 탐색기 보다는 좋은 예제 소스가 있어

참고 예제 소스 기준으로 몇 회로 나누어 알아보겠습니다.

참고 사이트는 아래쪽에 링크를 걸었습니다.

 

먼저 안드로이드(Android)에서 파일 탐색을

하기 위한 Provider에 대해서 알아보겠습니다.

 

안드로이드는 Acrivity, Service,

Broadcast Receiver, Content Provider

4가지 구성요소로 구분됩니다.

 

Content Provider는 앱이 접근할 수 있는

모든 영구 저장 위치에 저장 가능한 앱 데이터의

공유형 집합을 관리합니다.

파일 시스템, SQLite, 데이터 관리

파일 읽기, 쓰기, 수정하기, 삭제하기 기능 관리

데이터 엑세스 권한에 대한 제어 기능

즉 파일을 접근하기 위해서는 Provider를 먼저 생성해야 합니다.

코틀린(Kotlin) 신규 프로젝트를 생성합니다.

AndroidManifest.xml에 스토리지

읽기, 쓰기 권한을 설정합니다.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Provider을 사용하기 위해서 코틀린(Kotlin)

Class를 생성합니다.

Kotlin File/Class 메뉴를 선택 후

GenericFileProvider Class를 생성합니다.

package com.office.secuex
import androidx.core.content.FileProvider
class GenericFileProvider : FileProvider()

Provider path를 연결하기 위해서 package 이름을 추가하고

탐색기(Explorer)에 사용할 FileProvider을 상속받습니다.

Provider을 연결하기 위한

provider_paths.xml을 생성합니다.

<paths xmlns:android="http://schemas.android.com/apk/res-auto">
    <external-path
        name="external_files"
        path="."/>
</paths>

xml 정보를 입력하고 경로를 확인합니다.

provider을 연결하기 위해서

Manifest.xml 파일 하단에 provider 정보를

입력합니다.

        <provider
            android:authorities="com.office.secuex"
            android:name=".GenericFileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>

autorities 속성은 생성한 package 이름으로 변경

name 속성은 생성한 Provider 파일 명

resource 속성은 xml 경로입니다.

 

코틀린(Kotlin) Provider를 설정 후 빌드를

진행하면 정상적으로 오류 없이 빌드됩니다.

오류가 발생할 경우 대부분 provider 속성 오류입니다.

 

다음 시간에는 Provider을 사용해서

파일 리스트를 생성해보겠습니다.

감사합니다.

http://thetechnocafe.com/build-a-file-explorer-in-kotlin-part-1-introduction-and-set-up/

 

Build a File Explorer in Kotlin - Part 1 - Introduction and Set up - TheTechnoCafe

This tutorial series will guide you through building a simple Android File Explorer application in Kotlin. You will develop a file explorer application...

thetechnocafe.com

 

반응형
반응형

지난 시간에 카메라를 사용해서 이미지 뷰에 연결하는 방법을 공부했습니다.

카메라 캡처를 사용해서 이미지를 사용하면 해상도가 떨어지기 때문에 원본이 손실되는 문제가 발생했습니다.

이번 시간에는 원본 이미지 손실을 최소화할 수 있는 이미지 저장 방법에 대해서 알아보겠습니다.

지난 시간에 배운 코틀린을 이용한 카메라 이미지 저장 실행 화면입니다.

원본 이미지 해상도가 많이 떨어지는 것을 확인할 수 있습니다.

해상도를 유지하기 위해서 먼저 촬영된 이미지를 원본 그대로 갤러리에 저장 후 로드하는 형태로 변경해보겠습니다.

카메라 원보 이미지를 저장하기 위해서 먼저 이미지 경로 Uri를 생성합니다.

fun createImageUri(filename:String, mimeType:String):Uri?{
        var values = ContentValues()
        values.put(MediaStore.Images.Media.DISPLAY_NAME,filename)
        values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
        return contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
    }

ContentValues를 사용해서 이미지 Uri를 생성합니다.

contentResolver는 contentProvider과 비즈니스 로직의 중계자 역할을 담당합니다.

ContentValues는 contentResolver이 사용하는 데이터 정보라고 생각하면 됩니다.

ContentValues에 이미지 이름과 타입을 저장합니다.

카메라를 동작하기 위해서 dispatchTakePictureIntentEx 함수를 추가합니다.

private var photoURI : Uri? = null
    private val REQUEST_CREATE_EX = 3

    private fun dispatchTakePictureIntentEx()
    {
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        val takePictureIntent : Intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        val uri : Uri? =   createImageUri("JPEG_${timeStamp}_", "image/jpg")
        photoURI = uri
        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
        startActivityForResult(takePictureIntent, REQUEST_CREATE_EX)
    }

전역 경로 photoURI, 이벤트 ID REQUEST_CREATE_EX를 선언합니다.

이전 시간에 선언한 함수와 동일하 구성이기 때문에 Ex를 붙여서 이름을 변경했습니다.

timeStamp를 사용해서 이미지 이름을 시간에 따라서 생성합니다.

CreateimageUri 함수를 호출해서 이미지를 생성하고 카메라를 실행합니다.

fun loadBitmapFromMediaStoreBy(photoUri: Uri) : Bitmap?{
        var image: Bitmap? = null
        try{
            image = if(Build.VERSION.SDK_INT > 27){
                val source: ImageDecoder.Source =
                    ImageDecoder.createSource(this.contentResolver, photoUri)
                ImageDecoder.decodeBitmap(source)

            }else{
                MediaStore.Images.Media.getBitmap(this.contentResolver, photoUri)
            }
        }catch(e:IOException){
            e.printStackTrace()
        }
        return image
    }

생성된 Uri 경로에 이미지를 MediaStore를 사용해서 읽어옵니다.

 btnCamera.setOnClickListener{
                if(checkPermission()){
                    //dispatchTakePictureIntent()
                    dispatchTakePictureIntentEx()
                }
            else{
                    requestPermission()
                }
        }

기존에 연결되어 있던 버튼 이벤트에서 신규로 선언한 dispatchTakePictureIntentEx 함수를 실행합니다.

else if( requestCode == REQUEST_CREATE_EX)
{
    if( photoURI != null)
    {
           val bitmap = loadBitmapFromMediaStoreBy(photoURI!!)
           GImageView.setImageBitmap(bitmap)
           photoURI = null
    }
}

onActivityResult 함수에서 이벤트 ID REQUEST_CREATE_EX를 필터링 후

이미지를 로드하는 loadBitmapFromMediaStoreBy 함수를 호출해서 이미지 뷰에 연결합니다.

카메라 촬영 후 갤러리를 확인하면 카메라 이미지를 확인할 수 있습니다.

원본 이미지를 사용해서 이미지뷰에 연결하면 해상도가 동일한 것을 확인할 수 있습니다.

카메라 이미지 해상도를 원본과 동일하게하기 위해서 별도 저장 후 로드하는 방법을 사용하게 가장 좋은 방법입니다.

이미지 정보가 시간에 따라서 변경되기 때문에 이름을 고정하면 한 개의 사진만 업데이트됩니다.

코틀린을 사용하면 코드가 매우 간결하기 때문에 매우 편리합니다.

객체를 주고받을 때 NULL 존재하기 때문에 ?를 사용하는 부분만 확인하면 어렵지 않은 코드입니다.

감사합니다.

 

반응형
반응형

오늘은 코틀린(Kotlin)을 사용해서 안드로이드 카메라 이벤트 이미지를 이미지 뷰에 출력해보겠습니다.

안드로이드 카메라를 사용하기 위해서 먼저 AndroidMainfest.xml에 속성을 추가해주세요.

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" android:required="true" />

 "required" 속성이 true일 경우는 반드시 카메라 사용 옵션입니다.

경우에 따라서 설정을 변경하면 됩니다.

카메라를 실행하기 위해서 activity_main.xml에 button을 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnGallery"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Gallery"
            tools:layout_editor_absoluteX="62dp"
            tools:layout_editor_absoluteY="16dp" />

        <Button
            android:id="@+id/btnCamera"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Camera"
            tools:layout_editor_absoluteY="76dp" />
    </LinearLayout>


    <ImageView
        android:id="@+id/GImageView"
        android:layout_width="match_parent"
        android:layout_height="500dp"
        tools:layout_editor_absoluteX="16dp"
        tools:layout_editor_absoluteY="124dp" />

    </LinearLayout>
    

</androidx.constraintlayout.widget.ConstraintLayout>

xml 추가 후 빌드하면 버튼이 정렬되어 출력됩니다.

"CAMERA" 버튼을 클릭하면 카메라를 실행하고, 촬영된 이미지를 하단 이미지 뷰에 추가하겠습니다.

카메라를 사용하기 위해서는 먼저 권한을 승인받아야 합니다.

 private fun requestPermission(){
        ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE,CAMERA),1)

    }
    private fun checkPermission():Boolean{

        return (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this,
        Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)

    }
    @Override
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if( requestCode == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
            Toast.makeText(this, "권한 설정 OK", Toast.LENGTH_SHORT).show()
        }
        else
        {
            Toast.makeText(this, "권한 허용 안됨", Toast.LENGTH_SHORT).show()
        }
    }

ActivityCompat Class를 사용해서 카메라 사용 권한을 요청합니다.

checkPermission() 함수는 권한 여부를 확인할 수 있습니다.

onRequestPermissionsResult() 함수를 override 후 권한 승인에 따른 이벤트를 확인할 수 있습니다.

즉 권한이 없을 경우 앱 동작을 중단하거나, 메시지를 출력할 수 있습니다.

카메라를 실행하기 위해서 Intent를 실행합니다.

    private val REQUEST_IMAGE_CAPTURE = 2
    private fun dispatchTakePictureIntent() {
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            takePictureIntent.resolveActivity(packageManager)?.also {
                startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
            }
        }
    }

권한이 승인되면 MediaStore.ACTION_IMAGE_CAPTURE argument를 사용해서 Intent를 실행합니다.

이젠 버튼 이벤트를 연결해서 dispatchTakePictureIntent() 함수를 실행합니다.

btnCamera.setOnClickListener{
                if(checkPermission()){
                    dispatchTakePictureIntent()
                }
            else{
                    requestPermission()
                }
        }

button event에서 checkPermission() 함수를 실행 후 권한이 없다면 권한을 재 요청합니다.

권한 승인 되었다면 카메라를 실행합니다.

정상적으로 카메라 실행 화면을 확인할 수 있습니다.

카메라 촬영 버튼을 클릭하면 이미지가 저장되면서 이벤트가 발생합니다.

이전 시간에 배운 onActivityResult 이벤트를 사용해서 이미지를 저장합니다.

@Override
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if( resultCode == Activity.RESULT_OK) {
            if (requestCode == GALLERY) {
                var ImnageData: Uri? = data?.data
                Toast.makeText(this, ImnageData.toString(), Toast.LENGTH_SHORT).show()
                try {
                    val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, ImnageData)
                    GImageView.setImageBitmap(bitmap)
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
            else if( requestCode == REQUEST_IMAGE_CAPTURE)
            {
                val imageBitmap :Bitmap? = data?.extras?.get("data") as Bitmap
                GImageView.setImageBitmap(imageBitmap)
            }
        }
    }

requestCode가 REQUEST_IMAGE_CAPTURE일 경우 "data" 이미지를 이미지 뷰에 연결할 수 있습니다.

Intent를 사용한 뷰 이벤트는 대부분 onActivityResult를 사용해서 필터링이 가능합니다.

촬영 후 정상적으로 이미지 뷰에 촬영한 사진이 출력됩니다.

일반적인 카메라 이미지를 사용할 경우는 Intent를 사용하면 매우 편리합니다.

하지만, 카메라 기능을 제어할 수 없기 때문에 카메라 기능을 사용할 경우는 별도 뷰를 개발해야 합니다.

감사합니다.

https://developer.android.com/training/camera/photobasics?hl=ko

 

사진 촬영  |  Android 개발자  |  Android Developers

이 과정에서는 기존 카메라 애플리케이션을 사용하여 사진을 캡처하는 방법을 설명합니다. 클라이언트 앱을 실행하는 기기에서 촬영한 하늘 사진을 조합하여 세계 날씨 지도를 만드는 크라우

developer.android.com

 

반응형
반응형

오늘은 코틀린(Kotlin)을 사용해서 캘러리 뷰 이미지 클릭 이벤트에 대해서 알아보겠습니다.

먼저 layout을 변경하기 위해서 mainActivity.xml을 클릭합니다.

 <Button
        android:text="Gallery"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btnGallery"   />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:id="@+id/GImageView" />

갤러리를 호출하기 위한 Button과 선택한 이미지를 출력하기 위한 ImageView를 선언합니다.

ImageView 크기는 원하는 크기로 설정하면 됩니다.

빌드하면 GALLERY BUTTON만 확인할 수 있습니다.

이제 MainActivity에서 버튼 이벤트를 생성해서 갤러리 호출 이벤트 연결이 필요합니다.

    private val GALLERY = 1
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btnGallery.setOnClickListener{
            val intent: Intent = Intent(Intent.ACTION_GET_CONTENT)
            intent.setType("image/*")
            startActivityForResult(intent,GALLERY)
        }

    }

전역 변수로 GALLERY = 1로 정의해주세요.

Intent 갤러리 타입은 1이기 때문에 편하게 사용하기 위해서 정의하는 겁니다.

layout에서 선언한 button에 setOnClickListener 이벤트를 연결합니다.

갤러리는 Intent를 사용해서 호출합니다.

전체 이미지를 확인하기 위해서 "Image/*"로 정의합니다.

코드 작성 후 빌드하면 button 클릭 시 갤러리 화면을 확인할 수 있습니다.

이제 갤러리 이미지에서 이벤트를 연결하겠습니다.

 @Override
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if( resultCode == Activity.RESULT_OK){
            if( requestCode ==  GALLERY)
            {
                var ImnageData: Uri? = data?.data
                Toast.makeText(this,ImnageData.toString(), Toast.LENGTH_SHORT ).show()
                try {
                    val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, ImnageData)
                    GImageView.setImageBitmap(bitmap)
                }
                catch (e:Exception)
                {
                    e.printStackTrace()
                }
            }
        }
    }

onActivityResult 함수를 override 해줍니다.

onActivityResult 함수는 호출된 Activity 이벤트를 확인할 수 있습니다.

resultCode는 Activity 완료 이벤트를 확인할 수 있습니다.

requestCode는 호출 Code를 확인할 수 있습니다.

data에서는 현재 클릭한 Object URL 경로 확인이 가능합니다.

MediaStore 객체를 사용해서 URL 경로에 이미지를 Bitmap로 변경합니다.

마지막으로 Layout에 정의된 GImageView 객체에 setImageBitmap를 사용해서 BitMap을 출력합니다.

출력 결과 선택한 이미지를 ImageView에서 확인할 수 있습니다.

Intent를 사용해서 간단하게 갤러리 이미지를 확인할 수 있지만,

갤러리 뷰를 마음대로 조절할 수 없기 때문에 디바이스 종속적으로 사용만 가능합니다.

상기 기능은 갤러리에서 이미지를 선택할 경우 사용하면 매우 편리합니다.

감사합니다.

 

 

반응형
반응형

코틀린(Kotlin)도 다른 언어와 동일하게 함수를 사용할 수 있습니다.

코틀린에 가장 큰 함수 특징은 일급 객체로 사용이 가능합니다.

일급 객체는 일반적인 함수 형식과  엔티티로 적용하여 변수로 바로 사용이 가능하다.

함수 기본 형식은 (함수명) -> 반환 형식으로 되어 있습니다.

fun MakeString(mes : String): String{
    return mes + "  Create Message"
}

MakeString 함수는 mes String 매개변수를 받아서 String으로 반환합니다.

println(MakeString("함수 사용"))

println 함수를 사용해서 출력합니다.

출력 결과 입력된 "함수 사용" 뒤에 메시지가 추가되었습니다.

일반적인 함수는 함수를 만들어서 호출할 수 있습니다.

코틀린(Kotlin) 함수는 이런 함수를 변수 처럼 정의할 수 있습니다.

var MString:(String) -> String = fun(mes:String) = mes + " Create Message 2"

변수 MString에 함수를 직접 선언할 수 있습니다.

    println(MakeString("함수 사용"))

    println(MString("변수 함수 사용"))

MakeString 함수와 MString 변수를 출력 했습니다.

출력 결과 동일하게 입력 String에 추가 내용이 포함되어 있습니다.

함수에 사용되는 매개변수를 다양하게 사용이 가능합니다.

    var MString:(String) -> String = fun(mes:String) = mes + " Create Message 2"

    var a:(Int) -> Int = fun(i:Int) = i * 10

    println(a(7).toString())

    println(MakeString("함수 사용"))

    println(MString("변수 함수 사용"))

Int를 사용해서 추가 함수 a를 선언했습니다.

기존과 동일하게 println 함수를 사용해서 출력했습니다.

a 함수는 입력 변수에 10을 곱하는 형태로 7을 입력 시 70을 반환했습니다.

코틀린 함수는 매개변수 형식과 반환 형식을 자유롭게 사용할 수 있어 문법이 매우 편리합니다.

또한 외부 함수 정의가 아닌 내부에 함수를 정의를 사용할 수 있어 객체에 대한 처리를 완벽하게 수용할 수 있습니다.

내부 함수 정의 후 모호하다면 invoke() 메서드를 사용해서 모호함을 해결할 수 있다.

감사합니다.

반응형

+ Recent posts