Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit adc24df5 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Quick affordance list (2/3)" into main

parents c1adcb5f 60049f3d
Loading
Loading
Loading
Loading
+31 −6
Original line number Diff line number Diff line
@@ -14,11 +14,36 @@
  ~ limitations under the License.
  -->

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    android:layout_height="wrap_content"
    android:paddingHorizontal="16dp"
    android:paddingBottom="16dp"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/picker_fragment_background">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/quick_affordance_horizontal_list"
            android:layout_width="match_parent"
        android:layout_height="wrap_content" />
            android:layout_height="wrap_content"
            android:clipChildren="false"
            android:clipToPadding="false"/>
    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingVertical="8dp">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@id/slot_tabs"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:clipToPadding="false" />
    </FrameLayout>
</LinearLayout>
 No newline at end of file
+63 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  ~ Copyright (C) 2024 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="64dp"
    android:layout_height="wrap_content"
    android:divider="@drawable/vertical_divider_8dp"
    android:clipChildren="false"
    android:showDividers="middle">

    <FrameLayout
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:background="@drawable/option_item_background"
        android:clipChildren="false">

        <ImageView
            android:id="@id/selection_border"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/option_item_border"
            android:alpha="0"
            android:importantForAccessibility="no" />

        <ImageView
            android:id="@id/background"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/option_item_background"
            android:importantForAccessibility="no" />

        <ImageView
            android:id="@id/foreground"
            android:layout_width="@dimen/keyguard_quick_affordance_icon_size"
            android:layout_height="@dimen/keyguard_quick_affordance_icon_size"
            android:layout_gravity="center"
            android:tint="@color/system_on_surface" />
    </FrameLayout>

    <TextView
        android:id="@id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:textColor="@color/system_on_surface"
        android:lines="2"
        android:hyphenationFrequency="normal"
        android:ellipsize="end" />
</LinearLayout>
 No newline at end of file
+83 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.customization.picker.common.ui.view

import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView

/** Item spacing used by the RecyclerView. */
class KeyguardQuickAffordanceItemSpacing() : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val itemIndex = parent.getChildAdapterPosition(view)
        val columnIndex = itemIndex / 2
        val isRtl = parent.layoutManager?.layoutDirection == View.LAYOUT_DIRECTION_RTL
        val density = parent.context.resources.displayMetrics.density

        val itemCount = parent.adapter?.itemCount ?: 0
        val columnCount = (itemCount + 1) / 2
        when {
            columnCount == 1 -> {
                outRect.left = EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
                outRect.right = EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
            }
            columnIndex > 0 && columnIndex < columnCount - 1 -> {
                outRect.left = COMMON_HORIZONTAL_SPACING_DP.toPx(density)
                outRect.right = COMMON_HORIZONTAL_SPACING_DP.toPx(density)
            }
            columnIndex == 0 -> {
                outRect.left =
                    if (!isRtl) EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
                    else COMMON_HORIZONTAL_SPACING_DP.toPx(density)
                outRect.right =
                    if (isRtl) EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
                    else COMMON_HORIZONTAL_SPACING_DP.toPx(density)
            }
            columnIndex == columnCount - 1 -> {
                outRect.right =
                    if (!isRtl) EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
                    else COMMON_HORIZONTAL_SPACING_DP.toPx(density)
                outRect.left =
                    if (isRtl) EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
                    else COMMON_HORIZONTAL_SPACING_DP.toPx(density)
            }
        }

        if (itemIndex % 2 == 0) {
            outRect.top = FIRST_ROW_TOP_SPACING_DP.toPx(density)
            outRect.bottom = FIRST_ROW_BOTTOM_SPACING_DP.toPx(density)
        } else {
            outRect.bottom = SECOND_ROW_BOTTOM_SPACING_DP.toPx(density)
        }
    }

    private fun Int.toPx(density: Float): Int {
        return (this * density).toInt()
    }

    companion object {
        const val EDGE_ITEM_HORIZONTAL_SPACING_DP = 20
        const val COMMON_HORIZONTAL_SPACING_DP = 9
        const val FIRST_ROW_TOP_SPACING_DP = 20
        const val FIRST_ROW_BOTTOM_SPACING_DP = 8
        const val SECOND_ROW_BOTTOM_SPACING_DP = 24
    }
}
+187 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wallpaper.customization.ui.binder

import android.app.Dialog
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.widget.ImageView
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.customization.picker.common.ui.view.ItemSpacing
import com.android.customization.picker.common.ui.view.KeyguardQuickAffordanceItemSpacing
import com.android.customization.picker.quickaffordance.ui.adapter.SlotTabAdapter
import com.android.themepicker.R
import com.android.wallpaper.customization.ui.viewmodel.KeyguardQuickAffordancePickerViewModel2
import com.android.wallpaper.picker.common.dialog.ui.viewbinder.DialogViewBinder
import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel
import com.android.wallpaper.picker.common.icon.ui.viewbinder.IconViewBinder
import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

@OptIn(ExperimentalCoroutinesApi::class)
object ShortcutBottomSheetBinder {

    fun bind(
        view: View,
        viewModel: KeyguardQuickAffordancePickerViewModel2,
        lifecycleOwner: LifecycleOwner,
    ) {
        val quickAffordanceAdapter =
            OptionItemAdapter(
                layoutResourceId = R.layout.quick_affordance_list_item,
                lifecycleOwner = lifecycleOwner,
                bindIcon = { foregroundView: View, gridIcon: Icon ->
                    val imageView = foregroundView as? ImageView
                    imageView?.let { IconViewBinder.bind(imageView, gridIcon) }
                },
            )
        val quickAffordanceList =
            view.requireViewById<RecyclerView>(R.id.quick_affordance_horizontal_list).apply {
                adapter = quickAffordanceAdapter
                layoutManager =
                    GridLayoutManager(
                        view.context.applicationContext,
                        2,
                        GridLayoutManager.HORIZONTAL,
                        false
                    )
                addItemDecoration(KeyguardQuickAffordanceItemSpacing())
            }
        val slotTabAdapter = SlotTabAdapter()
        val slotTabView: RecyclerView =
            view.requireViewById<RecyclerView>(R.id.slot_tabs).apply {
                adapter = slotTabAdapter
                layoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
                addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP))
            }
        // Setting a custom accessibility delegate so that the default content descriptions
        // for items in a list aren't announced (for left & right shortcuts). We populate
        // the content description for these shortcuts later on with the right (expected)
        // values.
        val slotTabViewDelegate: AccessibilityDelegateCompat =
            object : AccessibilityDelegateCompat() {
                override fun onRequestSendAccessibilityEvent(
                    host: ViewGroup,
                    child: View,
                    event: AccessibilityEvent
                ): Boolean {
                    if (event.eventType != AccessibilityEvent.TYPE_VIEW_FOCUSED) {
                        child.contentDescription = null
                    }
                    return super.onRequestSendAccessibilityEvent(host, child, event)
                }
            }
        ViewCompat.setAccessibilityDelegate(slotTabView, slotTabViewDelegate)

        var dialog: Dialog? = null

        lifecycleOwner.lifecycleScope.launch {
            lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    viewModel.slots
                        .map { slotById -> slotById.values }
                        .collect { slots -> slotTabAdapter.setItems(slots.toList()) }
                }

                launch {
                    viewModel.quickAffordances.collect { affordances ->
                        quickAffordanceAdapter.setItems(affordances)
                    }
                }

                launch {
                    viewModel.quickAffordances
                        .flatMapLatest { affordances ->
                            combine(affordances.map { affordance -> affordance.isSelected }) {
                                selectedFlags ->
                                selectedFlags.indexOfFirst { it }
                            }
                        }
                        .collectIndexed { index, selectedPosition ->
                            // Scroll the view to show the first selected affordance.
                            if (selectedPosition != -1) {
                                // We use "post" because we need to give the adapter item a pass to
                                // update the view.
                                quickAffordanceList.post {
                                    if (index == 0) {
                                        // don't animate on initial collection
                                        quickAffordanceList.scrollToPosition(selectedPosition)
                                    } else {
                                        quickAffordanceList.smoothScrollToPosition(selectedPosition)
                                    }
                                }
                            }
                        }
                }

                launch {
                    viewModel.dialog.distinctUntilChanged().collect { dialogRequest ->
                        dialog?.dismiss()
                        dialog =
                            if (dialogRequest != null) {
                                showDialog(
                                    context = view.context,
                                    request = dialogRequest,
                                    onDismissed = viewModel::onDialogDismissed
                                )
                            } else {
                                null
                            }
                    }
                }

                launch {
                    viewModel.activityStartRequests.collect { intent ->
                        if (intent != null) {
                            view.context.startActivity(intent)
                            viewModel.onActivityStarted()
                        }
                    }
                }
            }
        }
    }

    private fun showDialog(
        context: Context,
        request: DialogViewModel,
        onDismissed: () -> Unit,
    ): Dialog {
        return DialogViewBinder.show(
            context = context,
            viewModel = request,
            onDismissed = onDismissed,
        )
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -29,8 +29,10 @@ import com.android.wallpaper.picker.customization.ui.util.CustomizationOptionUti
import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationOptionsViewModel
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch

@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class ThemePickerCustomizationOptionsBinder
@Inject
@@ -77,5 +79,11 @@ constructor(private val defaultCustomizationOptionsBinder: DefaultCustomizationO
                }
            }
        }

        ShortcutBottomSheetBinder.bind(
            view,
            viewModel.keyguardQuickAffordancePickerViewModel2,
            lifecycleOwner,
        )
    }
}
Loading