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