728x90

일반적으로 서버 연동 개발을 위해 로컬 호스트 아이피(127.0.0.1)를 사용하여 로컬에서 가동중인 서버와의 통신을 시도한다.

 

하지만 이 로컬 호스트 아이피를 사용하여 아래와 같이 서버 api 호출을 요청하면 오류가 발생한다.

init{
    val gson = GsonBuilder().setLenient().create()
    val retrofit = Retrofit.Builder()
        .baseUrl("http://127.0.0.1:8080/") // 통신 오류 발생
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build()
    api = retrofit.create(ApiManager::class.java)
}

 

안드로이드에서 사용하는 에뮬레이터 상의 로컬호스트 주소와 내 컴퓨터의 로컬 호스트 주소가 서로 다른 것이 원인으로

다시말해 안드로이드에서 내 컴퓨터가 아닌 가상 운영체제인 에뮬레이터 안에서 작동중인 로컬 서버로의 접속을 시도하고 있기 때문에 오류가 발생하는 것이다.

따라서 안드로이드 에뮬레이터에서 내 컴퓨터의 서버를 가리키려면 기존에 사용하던 localhost 가 아닌 별도의 주소를 사용해야 하며 종류는 아래와 같다.

10.0.2.1 라우터 또는 게이트웨이 주소
10.0.2.2 호스트 루프백 인터페이스의 특수 별칭(개발 머신의 127.0.0.1)
10.0.2.3 첫 번째 DNS 서버
10.0.2.4/10.0.2.5/10.0.2.6 두 번째, 세 번째, 네 번째 DNS 서버(선택사항)
10.0.2.15 이더넷을 사용하여 연결된 경우 에뮬레이션된 기기 네트워크
10.0.2.16 Wi-Fi를 사용하여 연결된 경우 에뮬레이션된 기기 네트워크
127.0.0.1 에뮬레이션된 기기 루프백 인터페이스

 

만약 내 컴퓨터에서 작동중인 서버가 스프링 서버라면 아래와 같은 형식으로 기존 코드를 고쳐야한다.

init{
    val gson = GsonBuilder().setLenient().create()
    val retrofit = Retrofit.Builder()
        .baseUrl("http://10.0.2.2:8080/") // 개발 머신의 127.0.0.1에 해당
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build()
    api = retrofit.create(ApiManager::class.java)
}

 

추가로 안드로이드에서는 네트워크 통신시 허가된 아이피로의 통신만 허용하므로 res - xml - network_security_config.xml 에 해당 아이피를 추가해야 한다.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
	    <!-- 개발머신의 로컬 호스트 -->
        <domain includeSubdomains="true">10.0.2.2</domain>
    </domain-config>
</network-security-config>

 

 

더 자세한 정보은 안드로이드 개발자 공식문서를 참고하길 바란다.

 

https://developer.android.com/studio/run/emulator-networking?hl=ko

 

Android Emulator 네트워킹 설정  |  Android Studio  |  Android Developers

에뮬레이터는 앱에 복잡한 모델링 및 테스트 환경을 설정하는 데 사용할 수 있는 다목적 네트워킹 기능을 제공합니다.

developer.android.com

 

728x90

안드로이드 앱 개발을 하다보면 안드로이드 스튜디오에서 제공하는 폰트가 아닌 내가 원하는 폰트를 앱에 적용시키고 싶을때가 있다.

 

오늘은 안드로이드 어플리케이션에 내가 원하는 폰트를 적용시키는 방법에 대해 알아보자.

 

당연한 말이지만, 폰트를 적용시키기 위해선 폰트 파일이 필요하다. 아래는 무료 폰트를 다운받을 수 있는 사이트이다.

https://noonnu.cc/

 

눈누

상업용 무료한글폰트 사이트

noonnu.cc

이 밖에도 구글에 '무료 폰트'라고 검색하여 다양한 사이트를 찾아볼수 있다.

 

눈누 사이트에 접속하여 원하는 포트를 클릭한후 다운로드 페이지로 이동을 클릭한다.

 

 

해당 글씨체를 다운받을 수 있는 공식 사이트로 이동하여 ttf파일을 다운받는다.

 

글씨체 파일이 준비되었다면, 안드로이드 스튜디오에 글씨체를 적용시켜보자.

 

res - New - Directory를 눌러 font라는 이름의 새로운 디렉토리를 생성한다.

font - New - Font Resource File 을 클릭한다.

font라는 이름으로 xml파일을 생성한다.

 

방금전 다운받은 .ttf파일을 font 디렉토리에 위치시킨다.

ttf파일의 이름은 소문자로만 구성되어야하므로, 만약 다운받은 폰트에 대문자가 섞여있다면 적절히 수정하도록 하자.

font.xml 파일로 이동하여 아래와 같이 font를 추가하도록 한다. 내가 추가한 폰트의 이름은 yeongdeoksea 이므로

android:font = "@font/yeongdeoksea" 로 작성하였다.

fontWeight 속성은 글자의 굵기를 의미하며 기본값은 400으로 속성값은 반드시 양수이며 100의 배수로 설정해야 하고 100~900 사이의 숫자로 지정해야한다.

<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
    <font android:fontStyle="normal"
        android:fontWeight="400"
        android:font="@font/yeongdeoksea"/>
</font-family>

여기까지 하였다면 안드로이드 스튜디오에서 폰트를 사용할 준비를 마친 것이다.

이제 TextView에 폰트를 적용시켜보자.

 

폰트를 적용하고 싶은 TextView로 이동하여 아래 속성을 추가해준다.

android:fontFamil="@font/폰트이름"

<TextView
    android:fontFamily="@font/yeongdeoksea"
    ...
    ... />

 

결과를 확인해보면 성공적으로 폰트가 적용되었을 것이다.

728x90

Intent는 '전달만 하는 요청''리턴을 받는 요청' 으로 분류 할 수 있다.

 

먼저 '전달만 하는 요청'에 대해 살펴보자.

 

전달만 하는 요청은 startActivity()를 통해 다른 컴포넌트로 데이터를 전달 할 수 있다.

실습을 위해 MainActivity와 Activity2를 아래와 같이 구성한다.

 

[activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="154dp"
        android:layout_marginTop="148dp"
        android:layout_marginEnd="166dp"
        android:layout_marginBottom="535dp"
        android:text="1+2는?"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

[activity_2.xml]

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".Activity2">

  <TextView
      android:id="@+id/txt_answer"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="40dp"
      android:text="Answer"/>

  </LinearLayout>

 

실습의 목표는 MainActivity에서 버튼을 클릭하면 Activity2로 전환되며, Activity2로 1,2를 넘겨서 Activity2의 Answer부분을 변화시키는 것이다.

 

MainAcitivity에 아래와 같이 코드를 작성한다.

package com.example.androidstudy

import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) //화면을 그려주는 부분(해당 레이아웃으로 화면을그려주겠다)

        var change_activity : Button = findViewById(R.id.button)

        //Intent에 정보 담아서 보내기

        change_activity.setOnClickListener{
            //0. Intent 생성(요청 생성)
            val intent = Intent(this@MainActivity,Activity2::class.java)
            //1. Intent에 정보 담기 key:value방식(하나 이상의 값을 보낼 수 있음)
            intent.putExtra("number1",1) // "number1" : 1 형태로 값 담기
            intent.putExtra("number2",2) // "number2" : 2 형태로 값 담기
            startActivity(intent) //요청 보내기
        }
    }
}

버튼에 onClickListener를 달아서 버튼을 클릭했을때의 동작을 정의한다.

버튼을 클릭하면 MainActivity에서 Activity2로 요청하는 Intent객체를 생성하고, putExtra를 통해 인텐트 객체에 정보를 담는다.

정보는 {key:value} 형식으로 담기게 되므로, 키 값만 다르게 한다면 여러개의 값을 한번에 전달 할 수 있다.

 

위 코드의 경우 number1이라는 이름으로 정수 1과 number2라는 이름으로 정수 2를 삽입하였다.

이후 startActivity를 통해 요청 intent를 시스템에 요청한다.

 

위 코드는 apply를 사용하여 아래와 같이 깔끔하게 개선 할 수 있다.

apply를 사용하면 블럭으로 묶은 이하의 코드에서 this 키워드를 사용함으로서 intent에 대한 작업을 한눈에 알아보기 쉽게 해준다는 장점이 있으며, 위의 코드와 아래 코드는 문법적으로 완벽히 동일한 코드이다.

package com.example.androidstudy

import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) //화면을 그려주는 부분(해당 레이아웃으로 화면을그려주겠다)

        var change_activity : Button = findViewById(R.id.button)

        //Intent에 정보 담아서 보내기

        change_activity.setOnClickListener{
            //apply를 사용하는 경우
            val intent = Intent(this@MainActivity,Activity2::class.java)
            //apply -> this키워드를 사용할 수 있도록 해줌(intent에 대한 작업을 한눈에 알아보기 용이)
            intent.apply {
                this.putExtra("number1",1) // "number1" : 1 형태로 값 담기
                this.putExtra("number2",2) // "number2" : 2 형태로 값 담기
            }
            startActivity(intent) //요청 보내기
        }
    }
}

MainActivity에서 값을 전달하면 Activity2에서는 값을 전달 받아야 한다.

Activity2에 아래와 같이 작성한다.

package com.example.androidstudy

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class Activity2 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_2)

        //값 꺼내기 : number1, number2라는 키 값이 없을 수 있으므로 예외 처리 필요
        val number1 = intent.getIntExtra("number1",0)
        val number2 = intent.getIntExtra("number2",0)
        var answer : TextView = findViewById(R.id.txt_answer)
        answer.text =  " 1 + 2 = " + (number1+number2).toString()
    }
}

Intent에 값을 넣어줄 때는 타입을 지정하지 않았지만, 값을 뺄때는 얻고자하는 데이터의 타입을 명시하여 함수를 사용해야 한다.

키워드 number1과 number2로 넣어준 데이터는 모두 Integer타입이므로 getIntExtra()함수를 통해 값을 얻을 수 있으며, 해당 키워드에 대한 값이 없을 수 있기 때문에, 두번째 인자로 0을 넣어줌으로서 Default값을 0으로 지정한다.

 

즉 getIntExtra()는 아래와 같은 형식을 갖는다.

getIntExtra(name, defaultValue)

 

이제 에뮬레이터를 통해 결과를 확인해보자.

 

728x90

Intent는 직역하면 요구사항, 의도, 요청사항, 등으로 해석할 수 있으며 안드로이드 컴포넌트들 간에 데이터를 전달하거나, 특정 작업(예: 다른 액티비티 시작)을 실행하는데 사용되는 메시징 객체이다

 

Intent를 사용하는 대상, 즉 요청을 주고 받는 대상은 아래와 같다.

  • Activity와 Activity
  • Android System과 내 App (전화, 카메라, 등)
  • 다른 App과 내 App (배달앱에서 결제 요청 → 카카오 앱 실행) : 상호간의 합의가 필요

요청의 종류는 '전달만 하는 요청'과 '리턴을 받는 요청'으로 분류 할 수 있다.

 

Intent를 사용하기 위해선 먼저 Intent객체를 생성해야 하며, 상황에 맞는 생성자를 호출하면 된다.

 

다음은 Intent를 사용하여 Activity를 전환하는 예시이다.

 

[MainActivity.kt]

package com.example.androidstudy

import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) //화면을 그려주는 부분(해당 레이아웃으로 화면을그려주겠다)

        var change_activity : Button = findViewById(R.id.button)

        /*
        Intent : 의도, 요구사항, 의사전달, 요청
        누구가 누구에게 요청을 하는가?
        대상 : Activity 와 Activity, Android 시스템과 내 App(카메라 실행,등), 외부 App과 내 App

        요청의 종류 : 전달만 하는 요청, 리턴 받는 요청
         */

        change_activity.setOnClickListener{

            //0. Intent 생성(요청 생성)
            //1. 현재 앱의 Context와 이동할 엑티비티의 클래스
            //MainActivity에서 Activity2로 이동하겠다는 의미를 가진 Intent
            val intent = Intent(this@MainActivity,Activity2::class.java)
            startActivity(intent) //요청 보내기
        }
    }
}

 

[activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="154dp"
        android:layout_marginTop="148dp"
        android:layout_marginEnd="166dp"
        android:layout_marginBottom="535dp"
        android:text="인텐트"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

[MainActivity 화면]

 

[activity_2.xml]

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    tools:context=".Activity2">

  </androidx.constraintlayout.widget.ConstraintLayout>

[Activity2 화면]

버튼을 누르면 아래와 같이 잘 이동하는 것을 확인 할  수 있다.

 

 

해당 게시글은 패스트 캠퍼트 은창현 강사님의 ⟪Android 앱 개발 올인원 패키지⟫ 를 토대로 작성하였습니다.

728x90

엑티비티(Activity)앱이 UI 그리는 창을 제공하는 것이다쉽게 말해 앱의 화면을 말한다

우리가 앱을 사용할 때, 한 화면에서 다른 화면으로 넘어간다면 엑티비티가 바뀐것으로 생각 할 수 있다.

엑티비티는 생성되고 사라지는 동안 다음과 같은 생명 주기를 갖는다.

1. onCreate()

: activity가 만들어 질 때 단 한번만 호출된다.(앱이 정상적으로 작동한다는 가정하에)

: activity를 만들때 단 한번만 하면 되는 작업들은 여기에서 해준다.

 

2. onStart()

 

3. onResume()

: 다시 앱으로 돌아올때 반드시 한번 거치는 라이프 사이클

: 엑티비티가 다시 호출될 때 하면 작업들을 여기에서 해주면 된다

 

4. onPause()

: 화면의 일부가 가려 졌을 때, 앱의 일부분이 보이지 않을때(일부만 보이지 않는상태, 앱을 내린 상태) 호출됨

 

5. onStop()

: 앱이 아에 보이지 않을때(사용자의 눈에서 보이지 않을때, 백그라운드?) 호출됨

 

onPause()와 onStop()은 거의 구분되지 않고 동시에 호출된다.

 

 

 

 

 

해당 게시글은 패스트 캠퍼트 은창현 강사님의 ⟪Android 앱 개발 올인원 패키지⟫ 를 토대로 작성하였습니다.

728x90

안드로이드 어플리케이션 화면의 특정 부분을 이미지로 저장하는 방법에 대해 알아보자.

 

앱을 개발하다 보면 사용자 인터페이스의 특정 영역을 이미지로 저장하는 기능이 필요할 때가 있다.

예를 들어, 그림 그리기 앱에서 사용자가 그린 그림을 저장하거나, 특정 정보가 표시된 프래그먼트의 내용을 이미지 파일로 저장하는 경우이다. 해당 게시글에서는 바로 이러한 안드로이드 영역 캡처에 대해 서술할 것이다.

 

예시를 위해 아래와 같은 프래그먼트를 만들어 보았다.

 

 

우리는 여기서 First 버튼을 누르면 Dashboard 프래그먼트의 모든 뷰를 이미지로 저장할 것이고, Second버튼을 누르면 하늘색 영역만 저장할 것이다.

 

Dashboard 프래그먼트의 xml코드는 아래와 같다.

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".ui.dashboard.DashboardFragment"
    android:orientation="vertical"
    android:background="@color/purple_200">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:background="@color/teal_200"
        android:orientation="vertical"
        android:id="@+id/layout2">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Second"
            android:textSize="50dp"
            android:layout_gravity="center"/>

    </LinearLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="First"
        android:textSize="40dp"
        android:layout_gravity="center"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:orientation="vertical">

        <Button
            android:id="@+id/btn_first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="First" />

        <Button
            android:id="@+id/btn_second"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="Second" />

    </LinearLayout>


</LinearLayout>

 

화면을 이미지로 만드는 과정은 아래 2단계를 거쳐 진행된다.

 

1. 뷰를 비트맵으로 변환한다.

private fun viewToBitmap(view: View): Bitmap {
    val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    view.draw(canvas)
    return bitmap
}

 

2. 비트맵을 이미지로 저장한다. (MediaStore 활용)

private fun saveNoteImageToMediaStore(bitmap: Bitmap) {
    val displayName = "note_image_" + SimpleDateFormat(
        "yyyyMMdd_HHmmss",
        Locale.getDefault()
    ).format(Date()) + ".png"

    val imageCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    } else {
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }

    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
        put(MediaStore.Images.Media.MIME_TYPE, "image/png")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(
                MediaStore.Images.Media.RELATIVE_PATH,
                Environment.DIRECTORY_PICTURES + File.separator + "YourAppDirectoryName"
            )
        }
    }

    val contentResolver = requireContext().contentResolver
    var imageUri: Uri? = null

    try {
        imageUri = contentResolver.insert(imageCollection, contentValues)
        imageUri?.let {
            val outputStream: OutputStream? = contentResolver.openOutputStream(it)
            outputStream?.use { stream ->
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
                Toast.makeText(requireContext(), "이미지가 저장되었습니다.", Toast.LENGTH_SHORT).show()
            }
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

 

이 두 코드를 사용하여 각 버튼에 대한 이벤트를 정의한다.

이때 layout2는 Dashboard 프래그먼트의 하늘색 영역을 의미하며 layout2 라는 아이디를 xml에서 부여했다.

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

    val layout2 = binding.layout2 // layout2 LinearLayout 가져오기
    
	//First버튼이 눌릴 경우
    binding.btnFirst.apply {
        setOnClickListener{
            val bitmap = viewToBitmap(binding.root) // 프래그먼트의 뷰 전체를 Bitmap으로 변환
            saveNoteImageToMediaStore(bitmap) // Bitmap을 저장
        }
    }

	//Second버튼이 눌릴 경우
    binding.btnSecond.apply {
        setOnClickListener{
            val bitmap = viewToBitmap(layout2) // 프래그먼트의 뷰의 layout2 부분만 Bitmap으로 변환
            saveNoteImageToMediaStore(bitmap) // Bitmap을 저장
        }
    }

}

 

이제 버튼을 눌러 제대로 저장이 되는지 확인해보자.

버튼을 누르면 아래와 같이 토스트 메시지와 함께 이미지가 저장된다.

 

 

갤러리로 이동하여 확인하면 아래와 같이 2장의 이미지가 생성된 것을 확인 할 수 있다.

 

 

 

+ Recent posts