Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settings.widget;
     18 
     19 import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
     20 
     21 import android.animation.Animator;
     22 import android.animation.AnimatorListenerAdapter;
     23 import android.animation.ArgbEvaluator;
     24 import android.animation.ValueAnimator;
     25 import android.content.Context;
     26 import android.graphics.Color;
     27 import android.os.Bundle;
     28 import android.support.annotation.VisibleForTesting;
     29 import android.support.v7.preference.PreferenceGroup;
     30 import android.support.v7.preference.PreferenceGroupAdapter;
     31 import android.support.v7.preference.PreferenceScreen;
     32 import android.support.v7.preference.PreferenceViewHolder;
     33 import android.support.v7.widget.RecyclerView;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 import android.util.TypedValue;
     37 import android.view.View;
     38 
     39 import com.android.settings.R;
     40 import com.android.settings.SettingsPreferenceFragment;
     41 
     42 public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
     43 
     44     private static final String TAG = "HighlightableAdapter";
     45     @VisibleForTesting
     46     static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
     47     private static final long HIGHLIGHT_DURATION = 15000L;
     48     private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L;
     49     private static final long HIGHLIGHT_FADE_IN_DURATION = 200L;
     50 
     51     @VisibleForTesting
     52     final int mHighlightColor;
     53     @VisibleForTesting
     54     boolean mFadeInAnimated;
     55 
     56     private final int mNormalBackgroundRes;
     57     private final String mHighlightKey;
     58     private boolean mHighlightRequested;
     59     private int mHighlightPosition = RecyclerView.NO_POSITION;
     60 
     61 
     62     /**
     63      * Tries to override initial expanded child count.
     64      * <p/>
     65      * Initial expanded child count will be ignored if:
     66      * 1. fragment contains request to highlight a particular row.
     67      * 2. count value is invalid.
     68      */
     69     public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) {
     70         if (host == null) {
     71             return;
     72         }
     73         final PreferenceScreen screen = host.getPreferenceScreen();
     74         if (screen == null) {
     75             return;
     76         }
     77         final Bundle arguments = host.getArguments();
     78         if (arguments != null) {
     79             final String highlightKey = arguments.getString(EXTRA_FRAGMENT_ARG_KEY);
     80             if (!TextUtils.isEmpty(highlightKey)) {
     81                 // Has highlight row - expand everything
     82                 screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
     83                 return;
     84             }
     85         }
     86 
     87         final int initialCount = host.getInitialExpandedChildCount();
     88         if (initialCount <= 0) {
     89             return;
     90         }
     91         screen.setInitialExpandedChildrenCount(initialCount);
     92     }
     93 
     94     public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key,
     95             boolean highlightRequested) {
     96         super(preferenceGroup);
     97         mHighlightKey = key;
     98         mHighlightRequested = highlightRequested;
     99         final Context context = preferenceGroup.getContext();
    100         final TypedValue outValue = new TypedValue();
    101         context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
    102                 outValue, true /* resolveRefs */);
    103         mNormalBackgroundRes = outValue.resourceId;
    104         mHighlightColor = context.getColor(R.color.preference_highligh_color);
    105     }
    106 
    107     @Override
    108     public void onBindViewHolder(PreferenceViewHolder holder, int position) {
    109         super.onBindViewHolder(holder, position);
    110         updateBackground(holder, position);
    111     }
    112 
    113     @VisibleForTesting
    114     void updateBackground(PreferenceViewHolder holder, int position) {
    115         View v = holder.itemView;
    116         if (position == mHighlightPosition) {
    117             // This position should be highlighted. If it's highlighted before - skip animation.
    118             addHighlightBackground(v, !mFadeInAnimated);
    119         } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
    120             // View with highlight is reused for a view that should not have highlight
    121             removeHighlightBackground(v, false /* animate */);
    122         }
    123     }
    124 
    125     public void requestHighlight(View root, RecyclerView recyclerView) {
    126         if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) {
    127             return;
    128         }
    129         root.postDelayed(() -> {
    130             final int position = getPreferenceAdapterPosition(mHighlightKey);
    131             if (position < 0) {
    132                 return;
    133             }
    134             mHighlightRequested = true;
    135             recyclerView.smoothScrollToPosition(position);
    136             mHighlightPosition = position;
    137             notifyItemChanged(position);
    138         }, DELAY_HIGHLIGHT_DURATION_MILLIS);
    139     }
    140 
    141     public boolean isHighlightRequested() {
    142         return mHighlightRequested;
    143     }
    144 
    145     @VisibleForTesting
    146     void requestRemoveHighlightDelayed(View v) {
    147         v.postDelayed(() -> {
    148             mHighlightPosition = RecyclerView.NO_POSITION;
    149             removeHighlightBackground(v, true /* animate */);
    150         }, HIGHLIGHT_DURATION);
    151     }
    152 
    153     private void addHighlightBackground(View v, boolean animate) {
    154         v.setTag(R.id.preference_highlighted, true);
    155         if (!animate) {
    156             v.setBackgroundColor(mHighlightColor);
    157             Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background");
    158             requestRemoveHighlightDelayed(v);
    159             return;
    160         }
    161         mFadeInAnimated = true;
    162         final int colorFrom = Color.WHITE;
    163         final int colorTo = mHighlightColor;
    164         final ValueAnimator fadeInLoop = ValueAnimator.ofObject(
    165                 new ArgbEvaluator(), colorFrom, colorTo);
    166         fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION);
    167         fadeInLoop.addUpdateListener(
    168                 animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
    169         fadeInLoop.setRepeatMode(ValueAnimator.REVERSE);
    170         fadeInLoop.setRepeatCount(4);
    171         fadeInLoop.start();
    172         Log.d(TAG, "AddHighlight: starting fade in animation");
    173         requestRemoveHighlightDelayed(v);
    174     }
    175 
    176     private void removeHighlightBackground(View v, boolean animate) {
    177         if (!animate) {
    178             v.setTag(R.id.preference_highlighted, false);
    179             v.setBackgroundResource(mNormalBackgroundRes);
    180             Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background");
    181             return;
    182         }
    183 
    184         if (!Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
    185             // Not highlighted, no-op
    186             Log.d(TAG, "RemoveHighlight: Not highlighted - skipping");
    187             return;
    188         }
    189         int colorFrom = mHighlightColor;
    190         int colorTo = Color.WHITE;
    191 
    192         v.setTag(R.id.preference_highlighted, false);
    193         final ValueAnimator colorAnimation = ValueAnimator.ofObject(
    194                 new ArgbEvaluator(), colorFrom, colorTo);
    195         colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION);
    196         colorAnimation.addUpdateListener(
    197                 animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
    198         colorAnimation.addListener(new AnimatorListenerAdapter() {
    199             @Override
    200             public void onAnimationEnd(Animator animation) {
    201                 // Animation complete - the background is now white. Change to mNormalBackgroundRes
    202                 // so it is white and has ripple on touch.
    203                 v.setBackgroundResource(mNormalBackgroundRes);
    204             }
    205         });
    206         colorAnimation.start();
    207         Log.d(TAG, "Starting fade out animation");
    208     }
    209 }
    210