diff options
author | 2024-10-10 22:51:43 +0000 | |
---|---|---|
committer | 2024-10-10 22:51:43 +0000 | |
commit | 33867d44db91247f2d5742f011574c651fe42e88 (patch) | |
tree | c70cd14761b84fe1dfa7f87b8138f2cf7e9cc7a0 /java/src | |
parent | 102bf02d3a21cdf402e6b1de6f3cb0794ca9c34f (diff) | |
parent | b134f387d15bf2b3184c38b4c0397265056bbd1c (diff) |
Merge "Add Launcher hover effect" into main
Diffstat (limited to 'java/src')
7 files changed, 185 insertions, 19 deletions
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index 016eb714..c2bb1f23 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -18,6 +18,7 @@ package com.android.intentresolver; import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; +import static com.android.intentresolver.Flags.targetHoverAndKeyboardFocusStates; import android.app.ActivityManager; import android.app.prediction.AppTarget; @@ -59,6 +60,8 @@ import com.android.intentresolver.widget.BadgeTextView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.google.common.collect.ImmutableList; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -365,7 +368,10 @@ public class ChooserListAdapter extends ResolverListAdapter { @Override View onCreateView(ViewGroup parent) { - return mInflater.inflate(R.layout.chooser_grid_item, parent, false); + int layout = targetHoverAndKeyboardFocusStates() + ? R.layout.chooser_grid_item_hover + : R.layout.chooser_grid_item; + return mInflater.inflate(layout, parent, false); } @Override @@ -522,8 +528,10 @@ public class ChooserListAdapter extends ResolverListAdapter { public void updateAlphabeticalList(Runnable onCompleted) { final DisplayResolveInfoAzInfoComparator comparator = new DisplayResolveInfoAzInfoComparator(mContext); - final List<DisplayResolveInfo> allTargets = new ArrayList<>(); - allTargets.addAll(getTargetsInCurrentDisplayList()); + ImmutableList<DisplayResolveInfo> displayList = getTargetsInCurrentDisplayList(); + final List<DisplayResolveInfo> allTargets = + new ArrayList<>(displayList.size() + mCallerTargets.size()); + allTargets.addAll(displayList); allTargets.addAll(mCallerTargets); new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { @@ -543,6 +551,24 @@ public class ChooserListAdapter extends ResolverListAdapter { // Consolidate multiple targets from same app. return allTargets .stream() + .map(appTarget -> { + if (targetHoverAndKeyboardFocusStates()) { + // Icon drawables are effectively cached per target info. + // Without cloning target infos, the same target info could be used + // for two different positions in the grid: once in the ranked + // targets row (from ResolverListAdapter#mDisplayList or + // #mCallerTargets, see #getItem()) and again in the all-app-target + // grid (copied from #mDisplayList and #mCallerTargets to + // #mSortedList). + // Using the same drawable for two list items would result in visual + // effects being applied to both simultaneously. + DisplayResolveInfo copy = appTarget.copy(); + copy.getDisplayIconHolder().setDisplayIcon(null); + return copy; + } else { + return appTarget; + } + }) .collect(Collectors.groupingBy(target -> target.getResolvedComponentName().getPackageName() + "#" + target.getDisplayLabel() diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java index 5e44c53e..f0674a27 100644 --- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java @@ -239,4 +239,11 @@ public class DisplayResolveInfo implements TargetInfo { public void setPinned(boolean pinned) { mPinned = pinned; } + + /** + * Creates a copy of the object. + */ + public DisplayResolveInfo copy() { + return new DisplayResolveInfo(this); + } } diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java index ba6c3c05..e5f40001 100644 --- a/java/src/com/android/intentresolver/chooser/TargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java @@ -65,7 +65,7 @@ public interface TargetInfo { * @param icon the icon to return on subsequent calls to {@link #getDisplayIcon()}. * Implementations may discard this request as a no-op if they don't support setting. */ - void setDisplayIcon(Drawable icon); + void setDisplayIcon(@Nullable Drawable icon); } /** A simple mutable-container implementation of {@link IconHolder}. */ @@ -78,7 +78,7 @@ public interface TargetInfo { return mDisplayIcon; } - public void setDisplayIcon(Drawable icon) { + public void setDisplayIcon(@Nullable Drawable icon) { mDisplayIcon = icon; } } diff --git a/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt index b0c26777..793b7621 100644 --- a/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt +++ b/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt @@ -23,6 +23,7 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.UserHandle import androidx.collection.LruCache +import com.android.intentresolver.Flags.targetHoverAndKeyboardFocusStates import com.android.intentresolver.chooser.DisplayResolveInfo import com.android.intentresolver.chooser.SelectableTargetInfo import java.util.function.Consumer @@ -46,11 +47,9 @@ class CachingTargetDataLoader( callback: Consumer<Drawable>, ): Drawable? { val cacheKey = info.toCacheKey() - return getCachedAppIcon(cacheKey, userHandle)?.let { BitmapDrawable(context.resources, it) } + return getCachedAppIcon(cacheKey, userHandle)?.toDrawable() ?: targetDataLoader.getOrLoadAppTargetIcon(info, userHandle) { drawable -> - (drawable as? BitmapDrawable)?.bitmap?.let { - getProfileIconCache(userHandle).put(cacheKey, it) - } + drawable.extractBitmap()?.let { getProfileIconCache(userHandle).put(cacheKey, it) } callback.accept(drawable) } } @@ -61,12 +60,10 @@ class CachingTargetDataLoader( callback: Consumer<Drawable>, ): Drawable? { val cacheKey = info.toCacheKey() - return cacheKey - ?.let { getCachedAppIcon(it, userHandle) } - ?.let { BitmapDrawable(context.resources, it) } + return cacheKey?.let { getCachedAppIcon(it, userHandle) }?.toDrawable() ?: targetDataLoader.getOrLoadDirectShareIcon(info, userHandle) { drawable -> if (cacheKey != null) { - (drawable as? BitmapDrawable)?.bitmap?.let { + drawable.extractBitmap()?.let { getProfileIconCache(userHandle).put(cacheKey, it) } } @@ -102,4 +99,20 @@ class CachingTargetDataLoader( append(directShareShortcutInfo?.id ?: "") } } + + private fun Bitmap.toDrawable(): Drawable { + return if (targetHoverAndKeyboardFocusStates()) { + HoverBitmapDrawable(this) + } else { + BitmapDrawable(context.resources, this) + } + } + + private fun Drawable.extractBitmap(): Bitmap? { + return when (this) { + is BitmapDrawable -> bitmap + is HoverBitmapDrawable -> bitmap + else -> null + } + } } diff --git a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt index 117c769d..e181f4f3 100644 --- a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt +++ b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt @@ -18,6 +18,7 @@ package com.android.intentresolver.icons import android.app.ActivityManager import android.content.Context +import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.AsyncTask @@ -27,6 +28,7 @@ import androidx.annotation.GuardedBy import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import com.android.intentresolver.Flags.targetHoverAndKeyboardFocusStates import com.android.intentresolver.R import com.android.intentresolver.TargetPresentationGetter import com.android.intentresolver.chooser.DisplayResolveInfo @@ -72,9 +74,7 @@ class DefaultTargetDataLoader( val taskId = nextTaskId.getAndIncrement() LoadIconTask(context, info, presentationFactory) { bitmap -> removeTask(taskId) - callback.accept( - bitmap?.let { BitmapDrawable(context.resources, it) } ?: loadIconPlaceholder() - ) + callback.accept(bitmap?.toDrawable() ?: loadIconPlaceholder()) } .also { addTask(taskId, it) } .executeOnExecutor(executor) @@ -93,9 +93,7 @@ class DefaultTargetDataLoader( presentationFactory, ) { bitmap -> removeTask(taskId) - callback.accept( - bitmap?.let { BitmapDrawable(context.resources, it) } ?: loadIconPlaceholder() - ) + callback.accept(bitmap?.toDrawable() ?: loadIconPlaceholder()) } .also { addTask(taskId, it) } .executeOnExecutor(executor) @@ -140,4 +138,12 @@ class DefaultTargetDataLoader( activeTasks.clear() } } + + private fun Bitmap.toDrawable(): Drawable { + return if (targetHoverAndKeyboardFocusStates()) { + HoverBitmapDrawable(this) + } else { + BitmapDrawable(context.resources, this) + } + } } diff --git a/java/src/com/android/intentresolver/icons/HoverBitmapDrawable.kt b/java/src/com/android/intentresolver/icons/HoverBitmapDrawable.kt new file mode 100644 index 00000000..4a21df92 --- /dev/null +++ b/java/src/com/android/intentresolver/icons/HoverBitmapDrawable.kt @@ -0,0 +1,41 @@ +/* + * Copyright 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 + * + * https://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.intentresolver.icons + +import android.graphics.Bitmap +import com.android.launcher3.icons.FastBitmapDrawable + +/** A [FastBitmapDrawable] extension that provides access to the bitmap. */ +class HoverBitmapDrawable(val bitmap: Bitmap) : FastBitmapDrawable(bitmap) { + + override fun newConstantState(): FastBitmapConstantState { + return HoverBitmapDrawableState(bitmap, iconColor) + } + + private class HoverBitmapDrawableState(private val bitmap: Bitmap, color: Int) : + FastBitmapConstantState(bitmap, color) { + override fun createDrawable(): FastBitmapDrawable { + return HoverBitmapDrawable(bitmap) + } + } + + companion object { + init { + setFlagHoverEnabled(true) + } + } +} diff --git a/java/src/com/android/intentresolver/widget/ChooserTargetItemView.kt b/java/src/com/android/intentresolver/widget/ChooserTargetItemView.kt new file mode 100644 index 00000000..28934495 --- /dev/null +++ b/java/src/com/android/intentresolver/widget/ChooserTargetItemView.kt @@ -0,0 +1,73 @@ +/* + * Copyright 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 + * + * https://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.intentresolver.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout + +class ChooserTargetItemView( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, +) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : this(context, attrs, defStyleAttr, 0) + + private var iconView: ImageView? = null + + override fun onViewAdded(child: View) { + super.onViewAdded(child) + if (child is ImageView) { + iconView = child + } + } + + override fun onViewRemoved(child: View?) { + super.onViewRemoved(child) + if (child === iconView) { + iconView = null + } + } + + override fun onHoverEvent(event: MotionEvent): Boolean { + val iconView = iconView ?: return false + if (!isEnabled) return true + when (event.action) { + MotionEvent.ACTION_HOVER_ENTER -> { + iconView.isHovered = true + } + MotionEvent.ACTION_HOVER_EXIT -> { + iconView.isHovered = false + } + } + return true + } + + override fun onInterceptHoverEvent(event: MotionEvent?) = true +} |