1 /* 2 * Copyright (C) 2015 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.systemui.statusbar.car; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.Paint.Align; 28 import android.graphics.drawable.Drawable; 29 import android.graphics.drawable.GradientDrawable; 30 import android.support.v4.view.PagerAdapter; 31 import android.support.v4.view.ViewPager; 32 import android.support.v4.view.animation.FastOutSlowInInterpolator; 33 import android.util.AttributeSet; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.ImageView; 38 import android.widget.LinearLayout; 39 import android.widget.TextView; 40 41 import com.android.systemui.R; 42 import com.android.systemui.statusbar.phone.StatusBar; 43 import com.android.systemui.statusbar.policy.UserSwitcherController; 44 45 /** 46 * Displays a ViewPager with icons for the users in the system to allow switching between users. 47 * One of the uses of this is for the lock screen in auto. 48 */ 49 public class UserGridView extends ViewPager { 50 private static final int EXPAND_ANIMATION_TIME_MS = 200; 51 private static final int HIDE_ANIMATION_TIME_MS = 133; 52 53 private StatusBar mStatusBar; 54 private UserSwitcherController mUserSwitcherController; 55 private Adapter mAdapter; 56 private UserSelectionListener mUserSelectionListener; 57 private ValueAnimator mHeightAnimator; 58 private int mTargetHeight; 59 private int mHeightChildren; 60 private boolean mShowing; 61 62 public UserGridView(Context context, AttributeSet attrs) { 63 super(context, attrs); 64 } 65 66 public void init(StatusBar statusBar, UserSwitcherController userSwitcherController, 67 boolean showInitially) { 68 mStatusBar = statusBar; 69 mUserSwitcherController = userSwitcherController; 70 mAdapter = new Adapter(mUserSwitcherController); 71 addOnLayoutChangeListener(mAdapter); 72 setAdapter(mAdapter); 73 mShowing = showInitially; 74 } 75 76 public boolean isShowing() { 77 return mShowing; 78 } 79 80 public void show() { 81 mShowing = true; 82 animateHeightChange(getMeasuredHeight(), mHeightChildren); 83 } 84 85 public void hide() { 86 mShowing = false; 87 animateHeightChange(getMeasuredHeight(), 0); 88 } 89 90 public void onUserSwitched(int newUserId) { 91 // Bring up security view after user switch is completed. 92 post(this::showOfflineAuthUi); 93 } 94 95 public void setUserSelectionListener(UserSelectionListener userSelectionListener) { 96 mUserSelectionListener = userSelectionListener; 97 } 98 99 void showOfflineAuthUi() { 100 // TODO: Show keyguard UI in-place. 101 mStatusBar.executeRunnableDismissingKeyguard(null, null, true, true, true); 102 } 103 104 @Override 105 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 106 // Wrap content doesn't work in ViewPagers, so simulate the behavior in code. 107 int height = 0; 108 if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { 109 height = MeasureSpec.getSize(heightMeasureSpec); 110 } else { 111 for (int i = 0; i < getChildCount(); i++) { 112 View child = getChildAt(i); 113 child.measure(widthMeasureSpec, 114 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 115 height = Math.max(child.getMeasuredHeight(), height); 116 } 117 118 mHeightChildren = height; 119 120 // Override the height if it's not showing. 121 if (!mShowing) { 122 height = 0; 123 } 124 125 // Respect the AT_MOST request from parent. 126 if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) { 127 height = Math.min(MeasureSpec.getSize(heightMeasureSpec), height); 128 } 129 } 130 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 131 132 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 133 } 134 135 private void animateHeightChange(int oldHeight, int newHeight) { 136 // If there is no change in height or an animation is already in progress towards the 137 // desired height, then there's no need to make any changes. 138 if (oldHeight == newHeight || newHeight == mTargetHeight) { 139 return; 140 } 141 142 // Animation in progress is not going towards the new target, so cancel it. 143 if (mHeightAnimator != null){ 144 mHeightAnimator.cancel(); 145 } 146 147 mTargetHeight = newHeight; 148 mHeightAnimator = ValueAnimator.ofInt(oldHeight, mTargetHeight); 149 mHeightAnimator.addUpdateListener(valueAnimator -> { 150 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 151 layoutParams.height = (Integer) valueAnimator.getAnimatedValue(); 152 requestLayout(); 153 }); 154 mHeightAnimator.addListener(new AnimatorListener() { 155 @Override 156 public void onAnimationStart(Animator animator) {} 157 158 @Override 159 public void onAnimationEnd(Animator animator) { 160 // ValueAnimator does not guarantee that the update listener will get an update 161 // to the final value, so here, the final value is set. Though the final calculated 162 // height (mTargetHeight) could be set, WRAP_CONTENT is more appropriate. 163 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 164 layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 165 requestLayout(); 166 mHeightAnimator = null; 167 } 168 169 @Override 170 public void onAnimationCancel(Animator animator) {} 171 172 @Override 173 public void onAnimationRepeat(Animator animator) {} 174 }); 175 176 mHeightAnimator.setInterpolator(new FastOutSlowInInterpolator()); 177 if (oldHeight < newHeight) { 178 // Expanding 179 mHeightAnimator.setDuration(EXPAND_ANIMATION_TIME_MS); 180 } else { 181 // Hiding 182 mHeightAnimator.setDuration(HIDE_ANIMATION_TIME_MS); 183 } 184 mHeightAnimator.start(); 185 } 186 187 /** 188 * This is a ViewPager.PagerAdapter which deletegates the work to a 189 * UserSwitcherController.BaseUserAdapter. Java doesn't support multiple inheritance so we have 190 * to use composition instead to achieve the same goal since both the base classes are abstract 191 * classes and not interfaces. 192 */ 193 private final class Adapter extends PagerAdapter implements View.OnLayoutChangeListener { 194 private final int mPodWidth; 195 private final int mPodMarginBetween; 196 private final int mPodImageAvatarWidth; 197 private final int mPodImageAvatarHeight; 198 199 private final WrappedBaseUserAdapter mUserAdapter; 200 private int mContainerWidth; 201 202 public Adapter(UserSwitcherController controller) { 203 super(); 204 mUserAdapter = new WrappedBaseUserAdapter(controller, this); 205 206 Resources res = getResources(); 207 mPodWidth = res.getDimensionPixelSize(R.dimen.car_fullscreen_user_pod_width); 208 mPodMarginBetween = res.getDimensionPixelSize( 209 R.dimen.car_fullscreen_user_pod_margin_between); 210 mPodImageAvatarWidth = res.getDimensionPixelSize( 211 R.dimen.car_fullscreen_user_pod_image_avatar_width); 212 mPodImageAvatarHeight = res.getDimensionPixelSize( 213 R.dimen.car_fullscreen_user_pod_image_avatar_height); 214 } 215 216 @Override 217 public void destroyItem(ViewGroup container, int position, Object object) { 218 container.removeView((View) object); 219 } 220 221 private int getIconsPerPage() { 222 // We need to know how many pods we need in this page. Each pod has its own width and 223 // a margin between them. We can then divide the measured width of the parent by the 224 // sum of pod width and margin to get the number of pods that will completely fit. 225 // There is one less margin than the number of pods (eg. for 5 pods, there are 4 226 // margins), so need to add the margin to the measured width to account for that. 227 return (mContainerWidth + mPodMarginBetween) / 228 (mPodWidth + mPodMarginBetween); 229 } 230 231 @Override 232 public Object instantiateItem(ViewGroup container, int position) { 233 Context context = getContext(); 234 LayoutInflater inflater = LayoutInflater.from(context); 235 236 ViewGroup pods = (ViewGroup) inflater.inflate( 237 R.layout.car_fullscreen_user_pod_container, null); 238 239 int iconsPerPage = getIconsPerPage(); 240 int limit = Math.min(mUserAdapter.getCount(), (position + 1) * iconsPerPage); 241 for (int i = position * iconsPerPage; i < limit; i++) { 242 View v = makeUserPod(inflater, context, i, pods); 243 pods.addView(v); 244 // This is hacky, but the dividers on the pod container LinearLayout don't seem 245 // to work for whatever reason. Instead, set a right margin on the pod if it's not 246 // the right-most pod and there is more than one pod in the container. 247 if (i < limit - 1 && limit > 1) { 248 LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( 249 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 250 params.setMargins(0, 0, mPodMarginBetween, 0); 251 v.setLayoutParams(params); 252 } 253 } 254 container.addView(pods); 255 return pods; 256 } 257 258 /** 259 * Returns the default user icon. This icon is a circle with a letter in it. The letter is 260 * the first character in the username. 261 * 262 * @param userName the username of the user for which the icon is to be created 263 */ 264 private Bitmap getDefaultUserIcon(CharSequence userName) { 265 CharSequence displayText = userName.subSequence(0, 1); 266 Bitmap out = Bitmap.createBitmap(mPodImageAvatarWidth, mPodImageAvatarHeight, 267 Bitmap.Config.ARGB_8888); 268 Canvas canvas = new Canvas(out); 269 270 // Draw the circle background. 271 GradientDrawable shape = new GradientDrawable(); 272 shape.setShape(GradientDrawable.RADIAL_GRADIENT); 273 shape.setGradientRadius(1.0f); 274 shape.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_bgcolor)); 275 shape.setBounds(0, 0, mPodImageAvatarWidth, mPodImageAvatarHeight); 276 shape.draw(canvas); 277 278 // Draw the letter in the center. 279 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 280 paint.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_fgcolor)); 281 paint.setTextAlign(Align.CENTER); 282 paint.setTextSize(getResources().getDimensionPixelSize( 283 R.dimen.car_fullscreen_user_pod_icon_text_size)); 284 Paint.FontMetricsInt metrics = paint.getFontMetricsInt(); 285 // The Y coordinate is measured by taking half the height of the pod, but that would 286 // draw the character putting the bottom of the font in the middle of the pod. To 287 // correct this, half the difference between the top and bottom distance metrics of the 288 // font gives the offset of the font. Bottom is a positive value, top is negative, so 289 // the different is actually a sum. The "half" operation is then factored out. 290 canvas.drawText(displayText.toString(), mPodImageAvatarWidth / 2, 291 (mPodImageAvatarHeight - (metrics.bottom + metrics.top)) / 2, paint); 292 293 return out; 294 } 295 296 private View makeUserPod(LayoutInflater inflater, Context context, 297 int position, ViewGroup parent) { 298 final UserSwitcherController.UserRecord record = mUserAdapter.getItem(position); 299 View view = inflater.inflate(R.layout.car_fullscreen_user_pod, parent, false); 300 301 TextView nameView = view.findViewById(R.id.user_name); 302 if (record != null) { 303 nameView.setText(mUserAdapter.getName(context, record)); 304 view.setActivated(record.isCurrent); 305 } else { 306 nameView.setText(context.getString(R.string.unknown_user_label)); 307 } 308 309 ImageView iconView = (ImageView) view.findViewById(R.id.user_avatar); 310 if (record == null || (record.picture == null && !record.isAddUser)) { 311 iconView.setImageBitmap(getDefaultUserIcon(nameView.getText())); 312 } else if (record.isAddUser) { 313 Drawable icon = context.getDrawable(R.drawable.ic_add_circle_qs); 314 icon.setTint(context.getColor(R.color.car_user_switcher_no_user_image_bgcolor)); 315 iconView.setImageDrawable(icon); 316 } else { 317 iconView.setImageBitmap(record.picture); 318 } 319 320 iconView.setOnClickListener(v -> { 321 if (record == null) { 322 return; 323 } 324 325 if (mUserSelectionListener != null) { 326 mUserSelectionListener.onUserSelected(record); 327 } 328 329 if (record.isCurrent) { 330 showOfflineAuthUi(); 331 } else { 332 mUserSwitcherController.switchTo(record); 333 } 334 }); 335 336 return view; 337 } 338 339 @Override 340 public int getCount() { 341 int iconsPerPage = getIconsPerPage(); 342 if (iconsPerPage == 0) { 343 return 0; 344 } 345 return (int) Math.ceil((double) mUserAdapter.getCount() / getIconsPerPage()); 346 } 347 348 public void refresh() { 349 mUserAdapter.refresh(); 350 } 351 352 @Override 353 public boolean isViewFromObject(View view, Object object) { 354 return view == object; 355 } 356 357 @Override 358 public void onLayoutChange(View v, int left, int top, int right, int bottom, 359 int oldLeft, int oldTop, int oldRight, int oldBottom) { 360 mContainerWidth = Math.max(left - right, right - left); 361 notifyDataSetChanged(); 362 } 363 } 364 365 private final class WrappedBaseUserAdapter extends UserSwitcherController.BaseUserAdapter { 366 private Adapter mContainer; 367 368 public WrappedBaseUserAdapter(UserSwitcherController controller, Adapter container) { 369 super(controller); 370 mContainer = container; 371 } 372 373 @Override 374 public View getView(int position, View convertView, ViewGroup parent) { 375 throw new UnsupportedOperationException("unused"); 376 } 377 378 @Override 379 public void notifyDataSetChanged() { 380 super.notifyDataSetChanged(); 381 mContainer.notifyDataSetChanged(); 382 } 383 } 384 385 interface UserSelectionListener { 386 void onUserSelected(UserSwitcherController.UserRecord record); 387 }; 388 } 389