티스토리 뷰

728x90

 

디자인 가이드라인

1. 한 번 보고 파악하기 쉽게 설계

2. 적절한 타이밍에 나오도록 설계

3. 손쉬운 탭이 가능하도록 설계

4. 시간 절약이 가능하도록 설계

 

스타일 가이드라인

1. Typography

2. Writing

3. Backgrounds

 

유저 인터페이스

1. 테마

 <style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
   <item name="android:windowBackground">@android:color/black</item>
   <item name="android:colorEdgeEffect">@android:color/white</item>
   <item name="android:textColor">@android:color/white</item>
 </style>

권장 테마 - [ 엑션바 제거, 검정색 배경, 흰색 글씨, 가장자리 효과 색상 ]

2. 레이아웃

레이아웃의 경우 두 가지 유형을 제시하고 있습니다.

 1) Main layout

더보기
<?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:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

  <FrameLayout
      android:id="@+id/body_layout"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:layout_margin="@dimen/glass_card_margin"
      app:layout_constraintBottom_toTopOf="@id/footer"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">

    <!-- Put your widgets inside this FrameLayout. -->

  </FrameLayout>

  <!-- The footer view will grow to fit as much content as possible while the
         timestamp view keeps its width. If the footer text is too long, it
         will be ellipsized with a 40dp margin between it and the timestamp. -->

  <TextView
      android:id="@+id/footer"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_marginStart="@dimen/glass_card_margin"
      android:layout_marginEnd="@dimen/glass_card_margin"
      android:layout_marginBottom="@dimen/glass_card_margin"
      android:ellipsize="end"
      android:singleLine="true"
      android:textAppearance="?android:attr/textAppearanceSmall"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toStartOf="@id/timestamp"
      app:layout_constraintStart_toStartOf="parent" />

  <TextView
      android:id="@+id/timestamp"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_marginEnd="@dimen/glass_card_margin"
      android:layout_marginBottom="@dimen/glass_card_margin"
      android:ellipsize="end"
      android:singleLine="true"
      android:textAlignment="viewEnd"
      android:textAppearance="?android:attr/textAppearanceSmall"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

2) Left column layout

더보기
<?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="match_parent">

  <FrameLayout
      android:id="@+id/left_column"
      android:layout_width="0dp"
      android:layout_height="match_parent"
      android:background="#303030"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintWidth_percent=".333">

    <!-- Put widgets for the left column inside this FrameLayout. -->

  </FrameLayout>

  <FrameLayout
      android:id="@+id/right_column"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:layout_marginTop="@dimen/glass_card_two_column_margin"
      android:layout_marginStart="@dimen/glass_card_two_column_margin"
      android:layout_marginBottom="@dimen/glass_card_two_column_margin"
      android:layout_marginEnd="@dimen/glass_card_margin"
      app:layout_constraintBottom_toTopOf="@id/footer"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@id/left_column"
      app:layout_constraintTop_toTopOf="parent">

    <!-- Put widgets for the right column inside this FrameLayout. -->

  </FrameLayout>

  <!-- The footer view will grow to fit as much content as possible while the
         timestamp view keeps its width. If the footer text is too long, it
         will be ellipsized with a 40dp margin between it and the timestamp. -->

  <TextView
      android:id="@+id/footer"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_marginStart="@dimen/glass_card_margin"
      android:layout_marginEnd="@dimen/glass_card_margin"
      android:layout_marginBottom="@dimen/glass_card_margin"
      android:ellipsize="end"
      android:singleLine="true"
      android:textAppearance="?android:attr/textAppearanceSmall"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toStartOf="@id/timestamp"
      app:layout_constraintStart_toEndOf="@id/left_column" />

  <TextView
      android:id="@+id/timestamp"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_marginEnd="@dimen/glass_card_margin"
      android:layout_marginBottom="@dimen/glass_card_margin"
      android:ellipsize="end"
      android:singleLine="true"
      android:textAlignment="viewEnd"
      android:textAppearance="?android:attr/textAppearanceSmall"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

문서에서 dimens의 값도 제공해주고 있습니다

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- The recommended margin for the top, left, and right edges of a card. -->
  <dimen name="glass_card_margin">40dp</dimen>

  <!-- The recommended margin between the bottom of the card and the footer. -->
  <dimen name="glass_card_footer_margin">50dp</dimen>

  <!-- The recommended margin for the left column of the two-column card. -->
  <dimen name="glass_card_two_column_margin">30dp</dimen>

</resources>

3. 메뉴

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    val menuResource = intent
        .getIntExtra(EXTRA_MENU_KEY, EXTRA_MENU_ITEM_DEFAULT_VALUE)
    if (menuResource != EXTRA_MENU_ITEM_DEFAULT_VALUE) {
        menuInflater.inflate(menuResource, menu)
        for (i in 0 until menu.size()) {
            val menuItem = menu.getItem(i)
            menuItems.add(
                GlassMenuItem(
                    menuItem.itemId, menuItem.icon,
                    menuItem.title.toString()
                )
            )
            adapter.notifyDataSetChanged()
        }
    }
    return super.onCreateOptionsMenu(menu)
}

  1) res > menu 폴더 안에 메뉴들을 정의합니다.
  2) onCreateOptionsMenu를 통해 메뉴들을 불러옵니다.
  3) 불러온 메뉴를 RecyclerView의 adapter에 담아주고 갱신해줍니다.
  4) 제스처에 따라서 setResult에 Intent에 값을 담아 전달합니다.
  5) onActivityResult를 통해 원하는 행동을 취합니다.

4. Swipeable pages

View Pager를 활용하여 스와프가 가능하게 제작합니다.

Input & Sensors

1. 터치 제스처

class GlassGestureDetector(context: Context, private val onGestureListener: OnGestureListener) :
    GestureDetector.OnGestureListener {

    private val gestureDetector = GestureDetector(context, this)

    enum class Gesture {
        TAP,
        SWIPE_FORWARD,
        SWIPE_BACKWARD,
        SWIPE_UP,
        SWIPE_DOWN
    }

    interface OnGestureListener {
        fun onGesture(gesture: Gesture): Boolean
    }

    fun onTouchEvent(motionEvent: MotionEvent): Boolean {
        return gestureDetector.onTouchEvent(motionEvent)
    }

    override fun onDown(e: MotionEvent): Boolean {
        return false
    }

    override fun onShowPress(e: MotionEvent) {}

    override fun onSingleTapUp(e: MotionEvent): Boolean {
        return onGestureListener.onGesture(Gesture.TAP)
    }

    override fun onScroll(
        e1: MotionEvent,
        e2: MotionEvent,
        distanceX: Float,
        distanceY: Float
    ): Boolean {
        return false
    }

    override fun onLongPress(e: MotionEvent) {}

    /**
     * Swipe detection depends on the:
     * - movement tan value,
     * - movement distance,
     * - movement velocity.
     *
     * To prevent unintentional SWIPE_DOWN and SWIPE_UP gestures, they are detected if movement
     * angle is only between 60 and 120 degrees.
     * Any other detected swipes, will be considered as SWIPE_FORWARD and SWIPE_BACKWARD, depends
     * on deltaX value sign.
     *
     * ______________________________________________________________
     * |                     \        UP         /                    |
     * |                       \               /                      |
     * |                         60         120                       |
     * |                           \       /                          |
     * |                             \   /                            |
     * |  BACKWARD  <-------  0  ------------  180  ------>  FORWARD  |
     * |                             /   \                            |
     * |                           /       \                          |
     * |                         60         120                       |
     * |                       /               \                      |
     * |                     /       DOWN        \                    |
     * --------------------------------------------------------------
     */
    override fun onFling(
        e1: MotionEvent,
        e2: MotionEvent,
        velocityX: Float,
        velocityY: Float
    ): Boolean {
        val deltaX = e2.x - e1.x
        val deltaY = e2.y - e1.y
        val tan =
            if (deltaX != 0f) abs(deltaY / deltaX).toDouble() else java.lang.Double.MAX_VALUE

        return if (tan > TAN_60_DEGREES) {
            if (abs(deltaY) < SWIPE_DISTANCE_THRESHOLD_PX || Math.abs(velocityY) < SWIPE_VELOCITY_THRESHOLD_PX) {
                false
            } else if (deltaY < 0) {
                onGestureListener.onGesture(Gesture.SWIPE_UP)
            } else {
                onGestureListener.onGesture(Gesture.SWIPE_DOWN)
            }
        } else {
            if (Math.abs(deltaX) < SWIPE_DISTANCE_THRESHOLD_PX || Math.abs(velocityX) < SWIPE_VELOCITY_THRESHOLD_PX) {
                false
            } else if (deltaX < 0) {
                onGestureListener.onGesture(Gesture.SWIPE_FORWARD)
            } else {
                onGestureListener.onGesture(Gesture.SWIPE_BACKWARD)
            }
        }
    }

    companion object {

        private const val SWIPE_DISTANCE_THRESHOLD_PX = 100
        private const val SWIPE_VELOCITY_THRESHOLD_PX = 100
        private val TAN_60_DEGREES = tan(Math.toRadians(60.0))
    }
}

제스처 관련해서 구글에서 제공하는 "GlassGestureDetector"입니다.
Enum Class를 보면 알 수 있듯이 [TAP, SWIPE_FORWARD, SWIPE_BACKWARD, SWIPE_UP, SWIPE_DOWN]의 제스처를 제공해줍니다.

제스처 별 권장 역활은 다음과 같습니다.
TAP  => 확인 또는 입력
SWIPE_FORWARD, SWIPE_BACKWARD => 카드와 화면 탐색
SWIPE DOWN => 뒤로가기 또는 종료

제스처를 사용하기 위해 Manifest에 다음과 같이 meta-data를 추가해 줍니다.

<application>
<!-- Copy below declaration into your manifest file -->
<meta-data
  android:name="com.google.android.glass.TouchEnabledApplication"
  android:value="true" />
</application>

엑티비티에서 GlassGestureDetector.OnGestureListener를 상속받은뒤 Override 함수를 통해 제스처의 활동을 정의해 줍니다.

class MainAcvitiy : AppCompatActivity(), GlassGestureDetector.OnGestureListener {

    private lateinit var glassGestureDetector: GlassGestureDetector

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

        glassGestureDetector = GlassGestureDetector(this, this)
    }

    override fun onGesture(gesture: GlassGestureDetector.Gesture): Boolean {
        when (gesture) {
            TAP ->
                // Response for TAP gesture
                return true
            SWIPE_FORWARD ->
                // Response for SWIPE_FORWARD gesture
                return true
            SWIPE_BACKWARD ->
                // Response for SWIPE_BACKWARD gesture
                return true
            else -> return false
        }
    }

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        return if (glassGestureDetector.onTouchEvent(ev)) {
            true
        } else super.dispatchTouchEvent(ev)
    }
}

2. 오디오 입력
Glass Enterprise Edition 2는 기본 오디오 소스를 지원하는 표준 AOSP 기반 장치입니다.
다음 오디오 소스에는 고급 신호 처리가 구현되어 있습니다.
[VOICE_COMMUNICATION, VOICE_RECOGNITION]

3. 음성 입력, 녹음, 명령

private const val SPEECH_REQUEST = 109

private fun displaySpeechRecognizer() {
    val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
    startActivityForResult(intent, SPEECH_REQUEST)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == SPEECH_REQUEST && resultCode == RESULT_OK) {
        val results: List<String>? =
            data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
        val spokenText = results?.get(0)
        // Do something with spokenText.
    }
    super.onActivityResult(requestCode, resultCode, data)
}

음성인식의 경우 영어로만 가능하다고 합니다.
키워드를 넣고 싶으면 다음과 같이 하면 된다고 합니다.

val keywords = arrayOf("Example", "Biasing", "Keywords")

val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
intent.putExtra("recognition-phrases", keywords)

startActivityForResult(intent, SPEECH_REQUEST)

이 외에 녹음, 명령에 대한 안내도 있지만
이 부분은 RecognizerIntent를 활용한 구글 음성 API 활용인데 
아직 사용해본 경험이 없어서 다음에 한 번 사용해 봐야겠습니다.

4. 카메라
카메라의 경우 CameraX 또는 Camera2 API 사용을 권장하고 있습니다.
카메라 버튼의 경우 물리적 버튼이라고 하는데 실제 장비를 봐야 알 수 있을 것 같습니다.
KeyEvent의 KEYCODE_CAMERA로 버튼을 인식할 수 있다고 합니다.

5. 센서
사용 가능한 센서 리스트

사용 할 수 없는 센서 리스트

센서의 경우 실제 테스트 하면서 확인해봐야 할 것같아서 사용 가능/불가능 리스트만 보고 넘어갑니다.

6. 위치
구글 글라스의 경우 GPS 모듈이 장착되어 있지 않고 근처의 WI-FI나 블루투스를 통해 위치를 파악합니다.

val devicePolicyManager = context
    .getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
if (devicePolicyManager.isDeviceOwnerApp(context.getPackageName())) {
    val componentName = ComponentName(context, MyDeviceAdmin::class.java)
    devicePolicyManager.setSecureSetting(
        componentName,
        Settings.Secure.LOCATION_MODE,
        Settings.Secure.LOCATION_MODE_SENSORS_ONLY.toString()
    )
}

 

샘플코드
카드 샘플, 카메라 2 샘플, QR코드 스캔 샘플, 음성 인식 샘플, 노트 샘플, 음성 명령 인식 샘플, 제스처 인식 샘플, WebRTC 샘플 

이렇게 샘플 코드 들을 제공해 주고 있습니다.

아직 구글 글라스 장비가 없어서 실제로 테스트는 진행하지 못하였습니다.

대부분의 코드는 일반 안드로이드 개발하는 것과 다른 점은 없는 걸로 보였습니다.

일반적인 안드로이드 개발과는 달리 "GlassGestureDetector"가 사용되는데 

정의된 클래스 코드를 제공해 주고 있고 특별한 경우가 아니면 이 부분에 대해선 커스텀을 따로 하지 않을 거라고 판단됩니다.

단순한 기능 위주의 샘플이라 그런지 엑티비티는 1개에서 2~3개 까지만 있고 나머지 화면은 프래그먼트 전환이나 뷰 페이저, 리사이클러 뷰 등을 통해 화면을 표시하였습니다.

샘플코드에서 사용 된 기술을 다음과 같습니다.

ViewPager, RecyclerView, RecognizerIntent, Room, Live Data, ViewModel, Camera2 ApI, zxing 라이브러리 등

이번 코드 분석하면서 힘들었던 부분은

Room 이나 Live Data 등과 같이 아직 많이 사용해보지 않은 기술이나

Camera2, ViewModel과 같이 아직 사용해 본적이 없는 기술이 코드에 포함되어 있어서

이러한 부분들은 따로 학습을 해야겠다고 느꼈습니다.

 

 

 

위의 모든 자료 출처는 구글글래스 공심 홈페이지입니다.
https://developers.google.com/glass-enterprise/guides/design-guidelines

 

Design guidelines  |  Glass Enterprise Edition 2  |  Google Developers

Users typically have multiple devices that store and display information for specific uses. Don't try to replace smartphone, tablet, or laptop features with Glass. It works best with information that's simple, relevant, and current. Instead, use Glass to s

developers.google.com

 

728x90

'메타버스 스터디' 카테고리의 다른 글

ARCore 정리  (0) 2021.08.06
AR Core 4주차  (0) 2021.07.16
ARCore 3주차  (0) 2021.07.11
ARCore 2주차  (0) 2021.07.04
ARCore  (0) 2021.06.27
댓글