Home | History | Annotate | Download | only in car
      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