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

Commit 34acc650 authored by Catherine Liang's avatar Catherine Liang
Browse files

Create app icon floating sheet

Remove icon shape from grid floating sheet and move icon shape and dark
theme toggle into a combined floating sheet.

Flag: com.android.systemui.shared.new_customization_picker_ui
Bug: 402161932
Test: manually verified
Change-Id: Ie5b33014f7e9889651f4a70a8dff2440150d0a28
parent 47e854d5
Loading
Loading
Loading
Loading
+86 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  ~ Copyright (C) 2025 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:id="@+id/floating_sheet_content_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginHorizontal="@dimen/floating_sheet_horizontal_padding"
    android:paddingVertical="@dimen/floating_sheet_content_vertical_padding"
    android:background="@drawable/floating_sheet_content_background"
    android:orientation="vertical"
    android:clipToPadding="false"
    android:clipChildren="false">

    <FrameLayout
        android:id="@+id/app_shape_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="12dp"
        android:clipToPadding="false"
        android:clipChildren="false">

        <!--
        This is just an invisible placeholder put in place so that the parent keeps its height
        stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows the
        layout logic to keep the size of the preview container stable as well, which bodes well
        for setting up the SurfaceView for remote rendering without changing its size after the
        content is loaded into the RecyclerView.

        It's critical for any TextViews inside the included layout to have text.
        -->
        <include
            layout="@layout/shape_option2"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:visibility="invisible" />

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


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:gravity="center_vertical"
        android:layout_marginHorizontal="@dimen/floating_sheet_content_horizontal_padding"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/themed_icon_title"
            style="@style/SectionTitleTextStyle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/themed_icon_title" />

        <com.google.android.material.materialswitch.MaterialSwitch
            android:id="@+id/themed_icon_toggle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@null"
            android:focusable="false"
            android:minHeight="0dp"
            android:theme="@style/Theme.Material3.DynamicColors.DayNight" />
    </LinearLayout>
</LinearLayout>
 No newline at end of file
+19 −65
Original line number Diff line number Diff line
@@ -30,44 +30,6 @@
        android:clipToPadding="false"
        android:clipChildren="false">

        <FrameLayout
            android:id="@+id/app_shape_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clipToPadding="false"
            android:clipChildren="false">

            <!--
            This is just an invisible placeholder put in place so that the parent keeps its height
            stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows the
            layout logic to keep the size of the preview container stable as well, which bodes well
            for setting up the SurfaceView for remote rendering without changing its size after the
            content is loaded into the RecyclerView.

            It's critical for any TextViews inside the included layout to have text.
            -->
            <include
                layout="@layout/shape_option2"
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:visibility="invisible" />

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

        <FrameLayout
            android:id="@+id/app_grid_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clipToPadding="false"
            android:clipChildren="false">

        <!--
        This is just an invisible placeholder put in place so that the parent keeps its height
        stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows the
@@ -92,12 +54,4 @@
            android:clipChildren="false"
            android:layout_gravity="center_horizontal" />
    </FrameLayout>
    </FrameLayout>

    <com.android.wallpaper.picker.customization.ui.view.FloatingToolbar
        android:id="@+id/floating_toolbar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginVertical="@dimen/floating_sheet_tab_toolbar_vertical_margin" />
</LinearLayout>
 No newline at end of file
+4 −0
Original line number Diff line number Diff line
@@ -123,6 +123,10 @@
        the grid layout of the apps -->
    <string name="grid_layout">Layout</string>

    <!-- Title of a section of the customization picker where the user can customize app icon
     shape and theme for the home screen. [CHAR LIMIT=15] -->
    <string name="app_icons_title">Icons</string>

    <!-- Label for a button that allows the user to apply the currently selected Theme.
        [CHAR LIMIT=20] -->
    <string name="apply_theme_btn">Apply</string>
+1 −1
Original line number Diff line number Diff line
@@ -181,7 +181,7 @@ constructor(
        const val SHAPE_OPTIONS: String = "shape_options"
        const val GRID_OPTIONS: String = "list_options"
        const val SHAPE_GRID: String = "default_grid"
        const val SET_SHAPE: String = "set_shape"
        const val SET_SHAPE: String = "shape"
        const val COL_SHAPE_KEY: String = "shape_key"
        const val COL_GRID_KEY: String = "name"
        const val COL_GRID_NAME: String = "grid_name"
+170 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.customization.picker.common.ui.view.SingleRowListItemSpacing
import com.android.customization.picker.grid.ui.viewmodel.ShapeIconViewModel
import com.android.themepicker.R
import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.APP_ICONS
import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel
import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder
import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel
import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter2
import com.google.android.material.materialswitch.MaterialSwitch
import java.lang.ref.WeakReference
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch

object AppIconFloatingSheetBinder {

    fun bind(
        view: View,
        optionsViewModel: ThemePickerCustomizationOptionsViewModel,
        colorUpdateViewModel: ColorUpdateViewModel,
        lifecycleOwner: LifecycleOwner,
        backgroundDispatcher: CoroutineDispatcher,
    ) {
        val viewModel = optionsViewModel.appIconPickerViewModel
        val isFloatingSheetActive = { optionsViewModel.selectedOption.value == APP_ICONS }

        val floatingSheetContainer =
            view.requireViewById<ViewGroup>(R.id.floating_sheet_content_container)
        ColorUpdateBinder.bind(
            setColor = { color ->
                DrawableCompat.setTint(
                    DrawableCompat.wrap(floatingSheetContainer.background),
                    color,
                )
            },
            color = colorUpdateViewModel.colorSurfaceBright,
            shouldAnimate = isFloatingSheetActive,
            lifecycleOwner = lifecycleOwner,
        )

        val shapeOptionListAdapter =
            createShapeOptionItemAdapter(
                colorUpdateViewModel = colorUpdateViewModel,
                shouldAnimateColor = isFloatingSheetActive,
                lifecycleOwner = lifecycleOwner,
                backgroundDispatcher = backgroundDispatcher,
            )
        val shapeOptionList =
            view.requireViewById<RecyclerView>(R.id.shape_options).also {
                it.initShapeOptionList(view.context, shapeOptionListAdapter)
            }

        val themedIconsSwitch = view.requireViewById<MaterialSwitch>(R.id.themed_icon_toggle)

        lifecycleOwner.lifecycleScope.launch {
            lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    viewModel.shapeOptions.collect { options ->
                        shapeOptionListAdapter.setItems(options) {
                            val indexToFocus =
                                options.indexOfFirst { it.isSelected.value }.coerceAtLeast(0)
                            (shapeOptionList.layoutManager as LinearLayoutManager).scrollToPosition(
                                indexToFocus
                            )
                        }
                    }
                }

                launch {
                    viewModel.isThemedIconAvailable.collect { isAvailable ->
                        themedIconsSwitch.isEnabled = isAvailable
                    }
                }

                launch {
                    var binding: SwitchColorBinder.Binding? = null
                    viewModel.previewingIsThemeIconEnabled.collect {
                        themedIconsSwitch.isChecked = it
                        binding?.destroy()
                        binding =
                            SwitchColorBinder.bind(
                                switch = themedIconsSwitch,
                                isChecked = it,
                                colorUpdateViewModel = colorUpdateViewModel,
                                shouldAnimateColor = isFloatingSheetActive,
                                lifecycleOwner = lifecycleOwner,
                            )
                    }
                }

                launch {
                    viewModel.toggleThemedIcon.collect {
                        themedIconsSwitch.setOnCheckedChangeListener { _, _ ->
                            launch { it.invoke() }
                        }
                    }
                }
            }
        }
    }

    private fun createShapeOptionItemAdapter(
        colorUpdateViewModel: ColorUpdateViewModel,
        shouldAnimateColor: () -> Boolean,
        lifecycleOwner: LifecycleOwner,
        backgroundDispatcher: CoroutineDispatcher,
    ): OptionItemAdapter2<ShapeIconViewModel> =
        OptionItemAdapter2(
            layoutResourceId = R.layout.shape_option2,
            lifecycleOwner = lifecycleOwner,
            backgroundDispatcher = backgroundDispatcher,
            bindPayload = { view: View, shapeIcon: ShapeIconViewModel ->
                val imageView = view.findViewById(R.id.foreground) as? ImageView
                imageView?.let { ShapeIconViewBinder.bind(imageView, shapeIcon) }
                return@OptionItemAdapter2 null
            },
            colorUpdateViewModel = WeakReference(colorUpdateViewModel),
            shouldAnimateColor = shouldAnimateColor,
        )

    private fun RecyclerView.initShapeOptionList(
        context: Context,
        adapter: OptionItemAdapter2<ShapeIconViewModel>,
    ) {
        apply {
            this.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
            addItemDecoration(
                SingleRowListItemSpacing(
                    edgeItemSpacePx =
                        context.resources.getDimensionPixelSize(
                            R.dimen.floating_sheet_content_horizontal_padding
                        ),
                    itemHorizontalSpacePx =
                        context.resources.getDimensionPixelSize(
                            R.dimen.floating_sheet_list_item_horizontal_space
                        ),
                )
            )
            this.adapter = adapter
        }
    }
}
Loading