Merge "Add entry animation to Settings Panels." into qt-dev

This commit is contained in:
Linda Tseng
2019-04-16 18:57:47 +00:00
committed by Android (Google) Code Review
9 changed files with 462 additions and 143 deletions

View File

@@ -16,13 +16,20 @@
package com.android.settings.panel;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.TextView;
@@ -30,8 +37,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.slice.Slice;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.SliceMetadata;
import androidx.slice.widget.SliceLiveData;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
@@ -40,10 +51,24 @@ import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.google.android.setupdesign.DividerItemDecoration;
import java.util.ArrayList;
import java.util.List;
public class PanelFragment extends Fragment {
private static final String TAG = "PanelFragment";
/**
* Duration of the animation entering or exiting the screen, in milliseconds.
*/
private static final int DURATION_ANIMATE_PANEL_MS = 250;
/**
* Duration of timeout waiting for Slice data to bind, in milliseconds.
*/
private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 250;
private View mLayoutView;
private TextView mTitleView;
private Button mSeeMoreButton;
private Button mDoneButton;
@@ -53,20 +78,40 @@ public class PanelFragment extends Fragment {
private MetricsFeatureProvider mMetricsProvider;
private String mPanelClosedKey;
private final List<LiveData<Slice>> mSliceLiveData = new ArrayList<>();
@VisibleForTesting
PanelSlicesAdapter mAdapter;
PanelSlicesLoaderCountdownLatch mPanelSlicesLoaderCountdownLatch;
private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener = () -> {
return false;
};
private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
animateIn();
if (mPanelSlices != null) {
mPanelSlices.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
};
private PanelSlicesAdapter mAdapter;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
final FragmentActivity activity = getActivity();
final View view = inflater.inflate(R.layout.panel_layout, container, false);
mPanelSlices = view.findViewById(R.id.panel_parent_layout);
mSeeMoreButton = view.findViewById(R.id.see_more);
mDoneButton = view.findViewById(R.id.done);
mTitleView = view.findViewById(R.id.panel_title);
mLayoutView = inflater.inflate(R.layout.panel_layout, container, false);
mPanelSlices = mLayoutView.findViewById(R.id.panel_parent_layout);
mSeeMoreButton = mLayoutView.findViewById(R.id.see_more);
mDoneButton = mLayoutView.findViewById(R.id.done);
mTitleView = mLayoutView.findViewById(R.id.panel_title);
final Bundle arguments = getArguments();
final String panelType =
@@ -82,6 +127,24 @@ public class PanelFragment extends Fragment {
.getPanel(activity, panelType, mediaPackageName);
mMetricsProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider();
mPanelSlices.setLayoutManager(new LinearLayoutManager((activity)));
// Add predraw listener to remove the animation and while we wait for Slices to load.
mLayoutView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
// Start loading Slices. When finished, the Panel will animate in.
loadAllSlices();
mTitleView.setText(mPanel.getTitle());
mSeeMoreButton.setOnClickListener(getSeeMoreListener());
mDoneButton.setOnClickListener(getCloseListener());
// If getSeeMoreIntent() is null, hide the mSeeMoreButton.
if (mPanel.getSeeMoreIntent() == null) {
mSeeMoreButton.setVisibility(View.GONE);
}
// Log panel opened.
mMetricsProvider.action(
0 /* attribution */,
@@ -90,27 +153,114 @@ public class PanelFragment extends Fragment {
callingPackageName,
0 /* value */);
mAdapter = new PanelSlicesAdapter(this, mPanel);
return mLayoutView;
}
mPanelSlices.setHasFixedSize(true);
mPanelSlices.setLayoutManager(new LinearLayoutManager((activity)));
mPanelSlices.setAdapter(mAdapter);
private void loadAllSlices() {
mSliceLiveData.clear();
final List<Uri> sliceUris = mPanel.getSlices();
mPanelSlicesLoaderCountdownLatch = new PanelSlicesLoaderCountdownLatch(sliceUris.size());
DividerItemDecoration itemDecoration = new DividerItemDecoration(getActivity());
itemDecoration.setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH);
mPanelSlices.addItemDecoration(itemDecoration);
for (Uri uri : sliceUris) {
final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getActivity(), uri);
mTitleView.setText(mPanel.getTitle());
// Add slice first to make it in order. Will remove it later if there's an error.
mSliceLiveData.add(sliceLiveData);
mSeeMoreButton.setOnClickListener(getSeeMoreListener());
mDoneButton.setOnClickListener(getCloseListener());
sliceLiveData.observe(getViewLifecycleOwner(), slice -> {
// If the Slice has already loaded, do nothing.
if (mPanelSlicesLoaderCountdownLatch.isSliceLoaded(uri)) {
return;
}
//If getSeeMoreIntent() is null, hide the mSeeMoreButton.
if (mPanel.getSeeMoreIntent() == null) {
mSeeMoreButton.setVisibility(View.GONE);
/**
* Watching for the {@link Slice} to load.
* <p>
* If the Slice comes back {@code null} or with the Error attribute, remove the
* Slice data from the list, and mark the Slice as loaded.
* <p>
* If the Slice has come back fully loaded, then mark the Slice as loaded. No
* other actions required since we already have the Slice data in the list.
* <p>
* If the Slice does not match the above condition, we will still want to mark
* it as loaded after 250ms timeout to avoid delay showing up the panel for
* too long. Since we are still having the Slice data in the list, the Slice
* will show up later once it is loaded.
*/
final SliceMetadata metadata = SliceMetadata.from(getActivity(), slice);
if (slice == null || metadata.isErrorSlice()) {
mSliceLiveData.remove(sliceLiveData);
mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri);
} else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) {
mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri);
} else {
Handler handler = new Handler();
handler.postDelayed(() -> {
mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri);
loadPanelWhenReady();
}, DURATION_SLICE_BINDING_TIMEOUT_MS);
}
loadPanelWhenReady();
});
}
}
return view;
/**
* When all of the Slices have loaded for the first time, then we can setup the
* {@link RecyclerView}.
* <p>
* When the Recyclerview has been laid out, we can begin the animation with the
* {@link mOnGlobalLayoutListener}, which calls {@link #animateIn()}.
*/
private void loadPanelWhenReady() {
if (mPanelSlicesLoaderCountdownLatch.isPanelReadyToLoad()) {
mAdapter = new PanelSlicesAdapter(
this, mSliceLiveData, mPanel.getMetricsCategory());
mPanelSlices.setAdapter(mAdapter);
mPanelSlices.getViewTreeObserver()
.addOnGlobalLayoutListener(mOnGlobalLayoutListener);
DividerItemDecoration itemDecoration = new DividerItemDecoration(getActivity());
itemDecoration
.setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH);
mPanelSlices.addItemDecoration(itemDecoration);
}
}
/**
* Animate a Panel onto the screen.
* <p>
* Takes the entire panel and animates in from behind the navigation bar.
* <p>
* Relies on the Panel being having a fixed height to begin the animation.
*/
private void animateIn() {
final View panelContent = mLayoutView.findViewById(R.id.panel_container);
final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, panelContent.getHeight(),
0.0f, new DecelerateInterpolator());
final ValueAnimator animator = new ValueAnimator();
animator.setFloatValues(0.0f, 1.0f);
animatorSet.play(animator);
animatorSet.start();
// Remove the predraw listeners on the Panel.
mLayoutView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
}
/**
* Build an {@link AnimatorSet} to bring the Panel, {@param parentView}in our out of the screen,
* based on the positional parameters {@param startY}, {@param endY} and at the rate set by the
* {@param interpolator}.
*/
@NonNull
private static AnimatorSet buildAnimatorSet(@NonNull View parentView, float startY, float endY,
@NonNull Interpolator interpolator) {
final View sheet = parentView.findViewById(R.id.panel_container);
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(DURATION_ANIMATE_PANEL_MS);
animatorSet.setInterpolator(interpolator);
animatorSet.playTogether(ObjectAnimator.ofFloat(sheet, View.TRANSLATION_Y, startY, endY));
return animatorSet;
}
@Override

View File

@@ -20,7 +20,6 @@ import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDIC
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -30,13 +29,13 @@ import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.Slice;
import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.google.android.setupdesign.DividerItemDecoration;
import java.util.ArrayList;
import java.util.List;
/**
@@ -45,14 +44,15 @@ import java.util.List;
public class PanelSlicesAdapter
extends RecyclerView.Adapter<PanelSlicesAdapter.SliceRowViewHolder> {
private final List<Uri> mSliceUris;
private final List<LiveData<Slice>> mSliceLiveData;
private final int mMetricsCategory;
private final PanelFragment mPanelFragment;
private final PanelContent mPanelContent;
public PanelSlicesAdapter(PanelFragment fragment, PanelContent panel) {
public PanelSlicesAdapter(
PanelFragment fragment, List<LiveData<Slice>> sliceLiveData, int metricsCategory) {
mPanelFragment = fragment;
mSliceUris = panel.getSlices();
mPanelContent = panel;
mSliceLiveData = new ArrayList<>(sliceLiveData);
mMetricsCategory = metricsCategory;
}
@NonNull
@@ -62,67 +62,60 @@ public class PanelSlicesAdapter
final LayoutInflater inflater = LayoutInflater.from(context);
final View view = inflater.inflate(R.layout.panel_slice_row, viewGroup, false);
return new SliceRowViewHolder(view, mPanelContent);
return new SliceRowViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SliceRowViewHolder sliceRowViewHolder, int position) {
sliceRowViewHolder.onBind(mPanelFragment, mSliceUris.get(position));
sliceRowViewHolder.onBind(mSliceLiveData.get(position));
}
@Override
public int getItemCount() {
return mSliceUris.size();
return mSliceLiveData.size();
}
@VisibleForTesting
List<Uri> getData() {
return mSliceUris;
List<LiveData<Slice>> getData() {
return mSliceLiveData;
}
/**
* ViewHolder for binding Slices to SliceViews.
*/
public static class SliceRowViewHolder extends RecyclerView.ViewHolder
public class SliceRowViewHolder extends RecyclerView.ViewHolder
implements DividerItemDecoration.DividedViewHolder {
private final PanelContent mPanelContent;
private boolean mDividerAllowedAbove = true;
@VisibleForTesting
LiveData<Slice> sliceLiveData;
@VisibleForTesting
final SliceView sliceView;
public SliceRowViewHolder(View view, PanelContent panelContent) {
public SliceRowViewHolder(View view) {
super(view);
sliceView = view.findViewById(R.id.slice_view);
sliceView.setMode(SliceView.MODE_LARGE);
sliceView.showTitleItems(true);
mPanelContent = panelContent;
}
public void onBind(PanelFragment fragment, Uri sliceUri) {
final Context context = sliceView.getContext();
sliceLiveData = SliceLiveData.fromUri(context, sliceUri);
sliceLiveData.observe(fragment.getViewLifecycleOwner(), sliceView);
public void onBind(LiveData<Slice> sliceLiveData) {
sliceLiveData.observe(mPanelFragment.getViewLifecycleOwner(), sliceView);
// Do not show the divider above media devices switcher slice per request
if (sliceUri.equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) {
final Slice slice = sliceLiveData.getValue();
if (slice != null && slice.getUri().equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) {
mDividerAllowedAbove = false;
}
// Log Panel interaction
sliceView.setOnSliceActionListener(
((eventInfo, sliceItem) -> {
FeatureFactory.getFactory(context)
FeatureFactory.getFactory(sliceView.getContext())
.getMetricsFeatureProvider()
.action(0 /* attribution */,
SettingsEnums.ACTION_PANEL_INTERACTION,
mPanelContent.getMetricsCategory(),
sliceUri.toString() /* log key */,
mMetricsCategory,
sliceLiveData.toString() /* log key */,
eventInfo.actionType /* value */);
})
);

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2019 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.settings.panel;
import android.net.Uri;
import androidx.slice.Slice;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
/**
* Helper class to isolate the work tracking all of the {@link Slice Slices} being loaded.
* <p>
* Uses a {@link CountDownLatch} and a {@link Set} of Slices to track how many
* Slices have been loaded. A Slice can only be counted as being loaded a single time, even
* when they get updated later.
* <p>
* To use the class, pass the number of expected Slices to load into the constructor. For
* every Slice that loads, call {@link #markSliceLoaded(Uri)} with the corresponding
* {@link Uri}. Then check if all of the Slices have loaded with
* {@link #isPanelReadyToLoad()}, which will return {@code true} the first time after all
* Slices have loaded.
*/
public class PanelSlicesLoaderCountdownLatch {
private final Set<Uri> mLoadedSlices;
private final CountDownLatch mCountDownLatch;
private boolean slicesReadyToLoad = false;
public PanelSlicesLoaderCountdownLatch(int countdownSize) {
mLoadedSlices = new HashSet<>();
mCountDownLatch = new CountDownLatch(countdownSize);
}
/**
* Checks if the {@param sliceUri} has been loaded: if not, then decrement the countdown
* latch, and if so, then do nothing.
*/
public void markSliceLoaded(Uri sliceUri) {
if (mLoadedSlices.contains(sliceUri)) {
return;
}
mLoadedSlices.add(sliceUri);
mCountDownLatch.countDown();
}
/**
* @return {@code true} if the Slice has already been loaded.
*/
public boolean isSliceLoaded(Uri uri) {
return mLoadedSlices.contains(uri);
}
/**
* @return {@code true} when all Slices have loaded, and the Panel has not yet been loaded.
*/
public boolean isPanelReadyToLoad() {
/**
* Use {@link slicesReadyToLoad} to track whether or not the Panel has been loaded. We
* only want to animate the Panel a single time.
*/
if ((mCountDownLatch.getCount() == 0) && !slicesReadyToLoad) {
slicesReadyToLoad = true;
return true;
}
return false;
}
}