/* * Copyright (C) 2017 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.launcher3.widget; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Pair; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.animation.AnimationUtils; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.Insettable; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener; import com.android.launcher3.R; import com.android.launcher3.views.RecyclerViewFastScroller; import com.android.launcher3.views.TopRoundedCornerView; /** * Popup for showing the full list of available widgets */ public class WidgetsFullSheet extends BaseWidgetSheet implements Insettable, ProviderChangedListener { private static final long DEFAULT_OPEN_DURATION = 267; private static final long FADE_IN_DURATION = 150; private static final float VERTICAL_START_POSITION = 0.3f; private final Rect mInsets = new Rect(); private final WidgetsListAdapter mAdapter; private WidgetsRecyclerView mRecyclerView; public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); LauncherAppState apps = LauncherAppState.getInstance(context); mAdapter = new WidgetsListAdapter(context, LayoutInflater.from(context), apps.getWidgetCache(), apps.getIconCache(), this, this); } public WidgetsFullSheet(Context context, AttributeSet attrs) { this(context, attrs, 0); } @Override protected void onFinishInflate() { super.onFinishInflate(); mContent = findViewById(R.id.container); mRecyclerView = findViewById(R.id.widgets_list_view); mRecyclerView.setAdapter(mAdapter); mAdapter.setApplyBitmapDeferred(true, mRecyclerView); TopRoundedCornerView springLayout = (TopRoundedCornerView) mContent; springLayout.addSpringView(R.id.widgets_list_view); mRecyclerView.setEdgeEffectFactory(springLayout.createEdgeEffectFactory()); onWidgetsBound(); } @Override protected Pair getAccessibilityTarget() { return Pair.create(mRecyclerView, getContext().getString( mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed)); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mLauncher.getAppWidgetHost().addProviderChangeListener(this); notifyWidgetProvidersChanged(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mLauncher.getAppWidgetHost().removeProviderChangeListener(this); } @Override public void setInsets(Rect insets) { mInsets.set(insets); mRecyclerView.setPadding( mRecyclerView.getPaddingLeft(), mRecyclerView.getPaddingTop(), mRecyclerView.getPaddingRight(), insets.bottom); if (insets.bottom > 0) { setupNavBarColor(); } else { clearNavBarColor(); } ((TopRoundedCornerView) mContent).setNavBarScrimHeight(mInsets.bottom); requestLayout(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthUsed; if (mInsets.bottom > 0) { widthUsed = 0; } else { Rect padding = mLauncher.getDeviceProfile().workspacePadding; widthUsed = Math.max(padding.left + padding.right, 2 * (mInsets.left + mInsets.right)); } int heightUsed = mInsets.top + mLauncher.getDeviceProfile().edgeMarginPx; measureChildWithMargins(mContent, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int width = r - l; int height = b - t; // Content is laid out as center bottom aligned int contentWidth = mContent.getMeasuredWidth(); int contentLeft = (width - contentWidth) / 2; mContent.layout(contentLeft, height - mContent.getMeasuredHeight(), contentLeft + contentWidth, height); setTranslationShift(mTranslationShift); } @Override public void notifyWidgetProvidersChanged() { mLauncher.refreshAndBindWidgetsForPackageUser(null); } @Override public void onWidgetsBound() { mAdapter.setWidgets(mLauncher.getPopupDataProvider().getAllWidgets()); } private void open(boolean animate) { if (animate) { if (getPopupContainer().getInsets().bottom > 0) { mContent.setAlpha(0); setTranslationShift(VERTICAL_START_POSITION); } mOpenCloseAnimator.setValues( PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); mOpenCloseAnimator .setDuration(DEFAULT_OPEN_DURATION) .setInterpolator(AnimationUtils.loadInterpolator( getContext(), android.R.interpolator.linear_out_slow_in)); mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mRecyclerView.setLayoutFrozen(false); mAdapter.setApplyBitmapDeferred(false, mRecyclerView); mOpenCloseAnimator.removeListener(this); } }); post(() -> { mRecyclerView.setLayoutFrozen(true); mOpenCloseAnimator.start(); mContent.animate().alpha(1).setDuration(FADE_IN_DURATION); }); } else { setTranslationShift(TRANSLATION_SHIFT_OPENED); mAdapter.setApplyBitmapDeferred(false, mRecyclerView); post(this::announceAccessibilityChanges); } } @Override protected void handleClose(boolean animate) { handleClose(animate, DEFAULT_OPEN_DURATION); } @Override protected boolean isOfType(int type) { return (type & TYPE_WIDGETS_FULL_SHEET) != 0; } @Override public boolean onControllerInterceptTouchEvent(MotionEvent ev) { // Disable swipe down when recycler view is scrolling if (ev.getAction() == MotionEvent.ACTION_DOWN) { mNoIntercept = false; RecyclerViewFastScroller scroller = mRecyclerView.getScrollbar(); if (scroller.getThumbOffsetY() >= 0 && getPopupContainer().isEventOverView(scroller, ev)) { mNoIntercept = true; } else if (getPopupContainer().isEventOverView(mContent, ev)) { mNoIntercept = !mRecyclerView.shouldContainerScroll(ev, getPopupContainer()); } } return super.onControllerInterceptTouchEvent(ev); } public static WidgetsFullSheet show(Launcher launcher, boolean animate) { WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater() .inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false); sheet.attachToContainer(); sheet.mIsOpen = true; sheet.open(animate); return sheet; } @VisibleForTesting public static WidgetsRecyclerView getWidgetsView(Launcher launcher) { return launcher.findViewById(R.id.widgets_list_view); } @Override protected int getElementsRowCount() { return mAdapter.getItemCount(); } @Nullable @Override public Animator createHintCloseAnim(float distanceToMove) { AnimatorSet anim = new AnimatorSet(); anim.play(ObjectAnimator.ofFloat(mRecyclerView, TRANSLATION_Y, -distanceToMove)); anim.play(ObjectAnimator.ofFloat(mRecyclerView, ALPHA, 0.5f)); return anim; } }