Implement the collapsible feature of conditional card
Add two control views for conditional card to achieve collapsibility. One of these control views is a footer that is appended to the last position when expanding all conditional cards. The other is a header that is used in the collapsed state of conditional cards. Bug: 119593268 Bug: 113451905 Test: visual, robotests Change-Id: I39e79b8925a9e3e4ce80c422bcd0ba889aba5f6a
This commit is contained in:
@@ -24,10 +24,14 @@ import androidx.annotation.VisibleForTesting;
|
|||||||
import com.android.settings.homepage.contextualcards.ContextualCard.CardType;
|
import com.android.settings.homepage.contextualcards.ContextualCard.CardType;
|
||||||
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardController;
|
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardController;
|
||||||
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer;
|
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer;
|
||||||
|
import com.android.settings.homepage.contextualcards.conditional
|
||||||
|
.ConditionHeaderContextualCardRenderer;
|
||||||
import com.android.settings.homepage.contextualcards.legacysuggestion
|
import com.android.settings.homepage.contextualcards.legacysuggestion
|
||||||
.LegacySuggestionContextualCardController;
|
.LegacySuggestionContextualCardController;
|
||||||
import com.android.settings.homepage.contextualcards.legacysuggestion
|
import com.android.settings.homepage.contextualcards.legacysuggestion
|
||||||
.LegacySuggestionContextualCardRenderer;
|
.LegacySuggestionContextualCardRenderer;
|
||||||
|
import com.android.settings.homepage.contextualcards.conditional
|
||||||
|
.ConditionFooterContextualCardRenderer;
|
||||||
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardController;
|
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardController;
|
||||||
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer;
|
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer;
|
||||||
|
|
||||||
@@ -83,6 +87,14 @@ public class ContextualCardLookupTable {
|
|||||||
SliceContextualCardRenderer.VIEW_TYPE,
|
SliceContextualCardRenderer.VIEW_TYPE,
|
||||||
SliceContextualCardController.class,
|
SliceContextualCardController.class,
|
||||||
SliceContextualCardRenderer.class));
|
SliceContextualCardRenderer.class));
|
||||||
|
add(new ControllerRendererMapping(CardType.CONDITIONAL_FOOTER,
|
||||||
|
ConditionFooterContextualCardRenderer.VIEW_TYPE,
|
||||||
|
ConditionContextualCardController.class,
|
||||||
|
ConditionFooterContextualCardRenderer.class));
|
||||||
|
add(new ControllerRendererMapping(CardType.CONDITIONAL_HEADER,
|
||||||
|
ConditionHeaderContextualCardRenderer.VIEW_TYPE,
|
||||||
|
ConditionContextualCardController.class,
|
||||||
|
ConditionHeaderContextualCardRenderer.class));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
public static Class<? extends ContextualCardController> getCardControllerClass(
|
public static Class<? extends ContextualCardController> getCardControllerClass(
|
||||||
|
@@ -129,5 +129,7 @@ public class ContextualCardsAdapter extends RecyclerView.Adapter<RecyclerView.Vi
|
|||||||
// Adding items to empty list, should animate.
|
// Adding items to empty list, should animate.
|
||||||
mRecyclerView.scheduleLayoutAnimation();
|
mRecyclerView.scheduleLayoutAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO(b/119465242): flickering conditional cards after collapsing/expanding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,10 +26,14 @@ import androidx.lifecycle.LifecycleOwner;
|
|||||||
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardController;
|
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardController;
|
||||||
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer;
|
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer;
|
||||||
|
import com.android.settings.homepage.contextualcards.conditional
|
||||||
|
.ConditionHeaderContextualCardRenderer;
|
||||||
import com.android.settings.homepage.contextualcards.legacysuggestion
|
import com.android.settings.homepage.contextualcards.legacysuggestion
|
||||||
.LegacySuggestionContextualCardController;
|
.LegacySuggestionContextualCardController;
|
||||||
import com.android.settings.homepage.contextualcards.legacysuggestion
|
import com.android.settings.homepage.contextualcards.legacysuggestion
|
||||||
.LegacySuggestionContextualCardRenderer;
|
.LegacySuggestionContextualCardRenderer;
|
||||||
|
import com.android.settings.homepage.contextualcards.conditional
|
||||||
|
.ConditionFooterContextualCardRenderer;
|
||||||
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardController;
|
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardController;
|
||||||
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer;
|
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer;
|
||||||
|
|
||||||
@@ -131,6 +135,12 @@ public class ControllerRendererPool {
|
|||||||
} else if (LegacySuggestionContextualCardRenderer.class == clz) {
|
} else if (LegacySuggestionContextualCardRenderer.class == clz) {
|
||||||
return new LegacySuggestionContextualCardRenderer(context,
|
return new LegacySuggestionContextualCardRenderer(context,
|
||||||
this /* controllerRendererPool */);
|
this /* controllerRendererPool */);
|
||||||
|
} else if (ConditionFooterContextualCardRenderer.class == clz) {
|
||||||
|
return new ConditionFooterContextualCardRenderer(context,
|
||||||
|
this /*controllerRendererPool*/);
|
||||||
|
} else if (ConditionHeaderContextualCardRenderer.class == clz) {
|
||||||
|
return new ConditionHeaderContextualCardRenderer(context,
|
||||||
|
this /*controllerRendererPool*/);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,8 @@ package com.android.settings.homepage.contextualcards.conditional;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.ArrayMap;
|
import android.util.ArrayMap;
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import com.android.settings.homepage.contextualcards.ContextualCard;
|
import com.android.settings.homepage.contextualcards.ContextualCard;
|
||||||
import com.android.settings.homepage.contextualcards.ContextualCardController;
|
import com.android.settings.homepage.contextualcards.ContextualCardController;
|
||||||
import com.android.settings.homepage.contextualcards.ContextualCardUpdateListener;
|
import com.android.settings.homepage.contextualcards.ContextualCardUpdateListener;
|
||||||
@@ -26,8 +28,11 @@ import com.android.settingslib.core.lifecycle.LifecycleObserver;
|
|||||||
import com.android.settingslib.core.lifecycle.events.OnStart;
|
import com.android.settingslib.core.lifecycle.events.OnStart;
|
||||||
import com.android.settingslib.core.lifecycle.events.OnStop;
|
import com.android.settingslib.core.lifecycle.events.OnStop;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This controller triggers the loading of conditional cards and monitors state changes to
|
* This controller triggers the loading of conditional cards and monitors state changes to
|
||||||
@@ -35,8 +40,12 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class ConditionContextualCardController implements ContextualCardController,
|
public class ConditionContextualCardController implements ContextualCardController,
|
||||||
ConditionListener, LifecycleObserver, OnStart, OnStop {
|
ConditionListener, LifecycleObserver, OnStart, OnStop {
|
||||||
|
public static final int EXPANDING_THRESHOLD = 2;
|
||||||
|
|
||||||
|
private static final double UNSUPPORTED_RANKING = -99999.0;
|
||||||
private static final String TAG = "ConditionCtxCardCtrl";
|
private static final String TAG = "ConditionCtxCardCtrl";
|
||||||
|
private static final String CONDITION_FOOTER = "condition_footer";
|
||||||
|
private static final String CONDITION_HEADER = "condition_header";
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final ConditionManager mConditionManager;
|
private final ConditionManager mConditionManager;
|
||||||
@@ -93,20 +102,102 @@ public class ConditionContextualCardController implements ContextualCardControll
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConditionsChanged() {
|
public void onConditionsChanged() {
|
||||||
|
if (mListener == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final List<ContextualCard> conditionCards = mConditionManager.getDisplayableCards();
|
final List<ContextualCard> conditionCards = mConditionManager.getDisplayableCards();
|
||||||
|
final Map<Integer, List<ContextualCard>> conditionalCards =
|
||||||
final boolean isOddNumber = conditionCards.size() % 2 == 1;
|
buildConditionalCardsWithFooterOrHeader(conditionCards);
|
||||||
if (isOddNumber) {
|
|
||||||
final int lastIndex = conditionCards.size() - 1;
|
|
||||||
final ConditionalContextualCard card = (ConditionalContextualCard) conditionCards
|
|
||||||
.get(lastIndex);
|
|
||||||
conditionCards.set(lastIndex, card.mutate().setIsHalfWidth(false).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mListener != null) {
|
|
||||||
final Map<Integer, List<ContextualCard>> conditionalCards = new ArrayMap<>();
|
|
||||||
conditionalCards.put(ContextualCard.CardType.CONDITIONAL, conditionCards);
|
|
||||||
mListener.onContextualCardUpdated(conditionalCards);
|
mListener.onContextualCardUpdated(conditionalCards);
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* According to conditional cards, build a map that includes conditional cards, header card and
|
||||||
|
* footer card.
|
||||||
|
*
|
||||||
|
* Rules:
|
||||||
|
* - The last one of conditional cards will be displayed as a full-width card if the size of
|
||||||
|
* conditional cards is odd number. The rest will be displayed as a half-width card.
|
||||||
|
* - By default conditional cards will be collapsed if there are more than TWO cards.
|
||||||
|
*
|
||||||
|
* For examples:
|
||||||
|
* - Only one conditional card: Returns a map that contains a full-width conditional card,
|
||||||
|
* no header card and no footer card.
|
||||||
|
* <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER,
|
||||||
|
* EMPTY_LIST)}</p>
|
||||||
|
* - Two conditional cards: Returns a map that contains two half-width conditional cards,
|
||||||
|
* no header card and no footer card.
|
||||||
|
* <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER,
|
||||||
|
* EMPTY_LIST)}</p>
|
||||||
|
* - Three conditional cards or above: By default, returns a map that contains no conditional
|
||||||
|
* card, one header card and no footer card. If conditional cards are expanded, will returns a
|
||||||
|
* map that contains three conditional cards, no header card and one footer card.
|
||||||
|
* If expanding conditional cards:
|
||||||
|
* <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, footerCards), (CONDITIONAL_HEADER,
|
||||||
|
* EMPTY_LIST)}</p>
|
||||||
|
* If collapsing conditional cards:
|
||||||
|
* <p>Map{(CONDITIONAL, EMPTY_LIST), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER,
|
||||||
|
* headerCards)}</p>
|
||||||
|
*
|
||||||
|
* @param conditionCards A list of conditional cards that are from {@link
|
||||||
|
* ConditionManager#getDisplayableCards}
|
||||||
|
* @return A map contained three types of lists
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
Map<Integer, List<ContextualCard>> buildConditionalCardsWithFooterOrHeader(
|
||||||
|
List<ContextualCard> conditionCards) {
|
||||||
|
final Map<Integer, List<ContextualCard>> conditionalCards = new ArrayMap<>();
|
||||||
|
conditionalCards.put(ContextualCard.CardType.CONDITIONAL,
|
||||||
|
getExpandedConditionalCards(conditionCards));
|
||||||
|
conditionalCards.put(ContextualCard.CardType.CONDITIONAL_FOOTER,
|
||||||
|
getConditionalFooterCard(conditionCards));
|
||||||
|
conditionalCards.put(ContextualCard.CardType.CONDITIONAL_HEADER,
|
||||||
|
getConditionalHeaderCard(conditionCards));
|
||||||
|
return conditionalCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ContextualCard> getExpandedConditionalCards(List<ContextualCard> conditionCards) {
|
||||||
|
if (conditionCards.isEmpty() || (conditionCards.size() > EXPANDING_THRESHOLD
|
||||||
|
&& !mIsExpanded)) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
final List<ContextualCard> expandedCards = conditionCards.stream().collect(
|
||||||
|
Collectors.toList());
|
||||||
|
final boolean isOddNumber = expandedCards.size() % 2 == 1;
|
||||||
|
if (isOddNumber) {
|
||||||
|
final int lastIndex = expandedCards.size() - 1;
|
||||||
|
final ConditionalContextualCard card =
|
||||||
|
(ConditionalContextualCard) expandedCards.get(lastIndex);
|
||||||
|
expandedCards.set(lastIndex, card.mutate().setIsHalfWidth(false).build());
|
||||||
|
}
|
||||||
|
return expandedCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ContextualCard> getConditionalFooterCard(List<ContextualCard> conditionCards) {
|
||||||
|
if (!conditionCards.isEmpty() && mIsExpanded
|
||||||
|
&& conditionCards.size() > EXPANDING_THRESHOLD) {
|
||||||
|
final List<ContextualCard> footerCards = new ArrayList<>();
|
||||||
|
footerCards.add(new ConditionFooterContextualCard.Builder()
|
||||||
|
.setName(CONDITION_FOOTER)
|
||||||
|
.setRankingScore(UNSUPPORTED_RANKING)
|
||||||
|
.build());
|
||||||
|
return footerCards;
|
||||||
|
}
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ContextualCard> getConditionalHeaderCard(List<ContextualCard> conditionCards) {
|
||||||
|
if (!conditionCards.isEmpty() && !mIsExpanded
|
||||||
|
&& conditionCards.size() > EXPANDING_THRESHOLD) {
|
||||||
|
final List<ContextualCard> headerCards = new ArrayList<>();
|
||||||
|
headerCards.add(new ConditionHeaderContextualCard.Builder()
|
||||||
|
.setConditionalCards(conditionCards)
|
||||||
|
.setName(CONDITION_HEADER)
|
||||||
|
.setRankingScore(UNSUPPORTED_RANKING)
|
||||||
|
.build());
|
||||||
|
return headerCards;
|
||||||
|
}
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,9 +16,12 @@
|
|||||||
|
|
||||||
package com.android.settings.homepage.contextualcards.conditional;
|
package com.android.settings.homepage.contextualcards.conditional;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.android.settings.homepage.contextualcards.ContextualCard;
|
import com.android.settings.homepage.contextualcards.ContextualCard;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data class representing a condition header {@link ContextualCard}.
|
* Data class representing a condition header {@link ContextualCard}.
|
||||||
@@ -44,6 +47,25 @@ public class ConditionHeaderContextualCard extends ContextualCard {
|
|||||||
return mConditionalCards;
|
return mConditionalCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(getName(), mConditionalCards);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(obj instanceof ConditionHeaderContextualCard)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final ConditionHeaderContextualCard that = (ConditionHeaderContextualCard) obj;
|
||||||
|
|
||||||
|
return TextUtils.equals(getName(), that.getName()) && mConditionalCards.equals(
|
||||||
|
that.mConditionalCards);
|
||||||
|
}
|
||||||
|
|
||||||
public static class Builder extends ContextualCard.Builder {
|
public static class Builder extends ContextualCard.Builder {
|
||||||
|
|
||||||
private List<ContextualCard> mConditionalCards;
|
private List<ContextualCard> mConditionalCards;
|
||||||
|
Reference in New Issue
Block a user