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.text.TextUtils; 29 import android.util.Log; 30 import android.util.TypedValue; 31 import android.view.View; 32 33 import androidx.annotation.VisibleForTesting; 34 import androidx.preference.PreferenceGroup; 35 import androidx.preference.PreferenceGroupAdapter; 36 import androidx.preference.PreferenceScreen; 37 import androidx.preference.PreferenceViewHolder; 38 import androidx.recyclerview.widget.RecyclerView; 39 40 import com.android.settings.R; 41 import com.android.settings.SettingsPreferenceFragment; 42 43 public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter { 44 45 private static final String TAG = "HighlightableAdapter"; 46 @VisibleForTesting 47 static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L; 48 private static final long HIGHLIGHT_DURATION = 15000L; 49 private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L; 50 private static final long HIGHLIGHT_FADE_IN_DURATION = 200L; 51 52 @VisibleForTesting 53 final int mHighlightColor; 54 @VisibleForTesting 55 boolean mFadeInAnimated; 56 57 private final int mNormalBackgroundRes; 58 private final String mHighlightKey; 59 private boolean mHighlightRequested; 60 private int mHighlightPosition = RecyclerView.NO_POSITION; 61 62 63 /** 64 * Tries to override initial expanded child count. 65 * <p/> 66 * Initial expanded child count will be ignored if: 67 * 1. fragment contains request to highlight a particular row. 68 * 2. count value is invalid. 69 */ adjustInitialExpandedChildCount(SettingsPreferenceFragment host)70 public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) { 71 if (host == null) { 72 return; 73 } 74 final PreferenceScreen screen = host.getPreferenceScreen(); 75 if (screen == null) { 76 return; 77 } 78 final Bundle arguments = host.getArguments(); 79 if (arguments != null) { 80 final String highlightKey = arguments.getString(EXTRA_FRAGMENT_ARG_KEY); 81 if (!TextUtils.isEmpty(highlightKey)) { 82 // Has highlight row - expand everything 83 screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE); 84 return; 85 } 86 } 87 88 final int initialCount = host.getInitialExpandedChildCount(); 89 if (initialCount <= 0) { 90 return; 91 } 92 screen.setInitialExpandedChildrenCount(initialCount); 93 } 94 HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key, boolean highlightRequested)95 public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key, 96 boolean highlightRequested) { 97 super(preferenceGroup); 98 mHighlightKey = key; 99 mHighlightRequested = highlightRequested; 100 final Context context = preferenceGroup.getContext(); 101 final TypedValue outValue = new TypedValue(); 102 context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, 103 outValue, true /* resolveRefs */); 104 mNormalBackgroundRes = outValue.resourceId; 105 mHighlightColor = context.getColor(R.color.preference_highligh_color); 106 } 107 108 @Override onBindViewHolder(PreferenceViewHolder holder, int position)109 public void onBindViewHolder(PreferenceViewHolder holder, int position) { 110 super.onBindViewHolder(holder, position); 111 updateBackground(holder, position); 112 } 113 114 @VisibleForTesting updateBackground(PreferenceViewHolder holder, int position)115 void updateBackground(PreferenceViewHolder holder, int position) { 116 View v = holder.itemView; 117 if (position == mHighlightPosition) { 118 // This position should be highlighted. If it's highlighted before - skip animation. 119 addHighlightBackground(v, !mFadeInAnimated); 120 } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) { 121 // View with highlight is reused for a view that should not have highlight 122 removeHighlightBackground(v, false /* animate */); 123 } 124 } 125 requestHighlight(View root, RecyclerView recyclerView)126 public void requestHighlight(View root, RecyclerView recyclerView) { 127 if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) { 128 return; 129 } 130 root.postDelayed(() -> { 131 final int position = getPreferenceAdapterPosition(mHighlightKey); 132 if (position < 0) { 133 return; 134 } 135 mHighlightRequested = true; 136 recyclerView.smoothScrollToPosition(position); 137 mHighlightPosition = position; 138 notifyItemChanged(position); 139 }, DELAY_HIGHLIGHT_DURATION_MILLIS); 140 } 141 isHighlightRequested()142 public boolean isHighlightRequested() { 143 return mHighlightRequested; 144 } 145 146 @VisibleForTesting requestRemoveHighlightDelayed(View v)147 void requestRemoveHighlightDelayed(View v) { 148 v.postDelayed(() -> { 149 mHighlightPosition = RecyclerView.NO_POSITION; 150 removeHighlightBackground(v, true /* animate */); 151 }, HIGHLIGHT_DURATION); 152 } 153 addHighlightBackground(View v, boolean animate)154 private void addHighlightBackground(View v, boolean animate) { 155 v.setTag(R.id.preference_highlighted, true); 156 if (!animate) { 157 v.setBackgroundColor(mHighlightColor); 158 Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background"); 159 requestRemoveHighlightDelayed(v); 160 return; 161 } 162 mFadeInAnimated = true; 163 final int colorFrom = Color.WHITE; 164 final int colorTo = mHighlightColor; 165 final ValueAnimator fadeInLoop = ValueAnimator.ofObject( 166 new ArgbEvaluator(), colorFrom, colorTo); 167 fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION); 168 fadeInLoop.addUpdateListener( 169 animator -> v.setBackgroundColor((int) animator.getAnimatedValue())); 170 fadeInLoop.setRepeatMode(ValueAnimator.REVERSE); 171 fadeInLoop.setRepeatCount(4); 172 fadeInLoop.start(); 173 Log.d(TAG, "AddHighlight: starting fade in animation"); 174 requestRemoveHighlightDelayed(v); 175 } 176 removeHighlightBackground(View v, boolean animate)177 private void removeHighlightBackground(View v, boolean animate) { 178 if (!animate) { 179 v.setTag(R.id.preference_highlighted, false); 180 v.setBackgroundResource(mNormalBackgroundRes); 181 Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background"); 182 return; 183 } 184 185 if (!Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) { 186 // Not highlighted, no-op 187 Log.d(TAG, "RemoveHighlight: Not highlighted - skipping"); 188 return; 189 } 190 int colorFrom = mHighlightColor; 191 int colorTo = Color.WHITE; 192 193 v.setTag(R.id.preference_highlighted, false); 194 final ValueAnimator colorAnimation = ValueAnimator.ofObject( 195 new ArgbEvaluator(), colorFrom, colorTo); 196 colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION); 197 colorAnimation.addUpdateListener( 198 animator -> v.setBackgroundColor((int) animator.getAnimatedValue())); 199 colorAnimation.addListener(new AnimatorListenerAdapter() { 200 @Override 201 public void onAnimationEnd(Animator animation) { 202 // Animation complete - the background is now white. Change to mNormalBackgroundRes 203 // so it is white and has ripple on touch. 204 v.setBackgroundResource(mNormalBackgroundRes); 205 } 206 }); 207 colorAnimation.start(); 208 Log.d(TAG, "Starting fade out animation"); 209 } 210 } 211