1 /* 2 * Copyright (C) 2020 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.wm.shell.pip.tv; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorInflater; 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.graphics.drawable.Drawable; 24 import android.util.AttributeSet; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.widget.ImageView; 28 import android.widget.RelativeLayout; 29 import android.widget.TextView; 30 31 import com.android.wm.shell.R; 32 33 /** 34 * A View that represents Pip Menu action button, such as "Fullscreen" and "Close" as well custom 35 * (provided by the application in Pip) and media buttons. 36 */ 37 public class TvPipMenuActionButton extends RelativeLayout implements View.OnClickListener { 38 private final ImageView mIconImageView; 39 private final ImageView mButtonImageView; 40 private final TextView mDescriptionTextView; 41 private Animator mTextFocusGainAnimator; 42 private Animator mButtonFocusGainAnimator; 43 private Animator mTextFocusLossAnimator; 44 private Animator mButtonFocusLossAnimator; 45 private OnClickListener mOnClickListener; 46 TvPipMenuActionButton(Context context)47 public TvPipMenuActionButton(Context context) { 48 this(context, null, 0, 0); 49 } 50 TvPipMenuActionButton(Context context, AttributeSet attrs)51 public TvPipMenuActionButton(Context context, AttributeSet attrs) { 52 this(context, attrs, 0, 0); 53 } 54 TvPipMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr)55 public TvPipMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { 56 this(context, attrs, defStyleAttr, 0); 57 } 58 TvPipMenuActionButton( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)59 public TvPipMenuActionButton( 60 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 61 super(context, attrs, defStyleAttr, defStyleRes); 62 final LayoutInflater inflater = (LayoutInflater) getContext() 63 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 64 inflater.inflate(R.layout.tv_pip_menu_action_button, this); 65 66 mIconImageView = findViewById(R.id.icon); 67 mButtonImageView = findViewById(R.id.button); 68 mDescriptionTextView = findViewById(R.id.desc); 69 70 final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; 71 final TypedArray typedArray = context.obtainStyledAttributes(attrs, values, defStyleAttr, 72 defStyleRes); 73 74 setImageResource(typedArray.getResourceId(0, 0)); 75 final int textResId = typedArray.getResourceId(1, 0); 76 if (textResId != 0) { 77 setTextAndDescription(getContext().getString(textResId)); 78 } 79 80 typedArray.recycle(); 81 } 82 83 @Override onFinishInflate()84 public void onFinishInflate() { 85 super.onFinishInflate(); 86 mButtonImageView.setOnFocusChangeListener((v, hasFocus) -> { 87 if (hasFocus) { 88 startFocusGainAnimation(); 89 } else { 90 startFocusLossAnimation(); 91 } 92 }); 93 94 mTextFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), 95 R.anim.tv_pip_controls_focus_gain_animation); 96 mTextFocusGainAnimator.setTarget(mDescriptionTextView); 97 mButtonFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), 98 R.anim.tv_pip_controls_focus_gain_animation); 99 mButtonFocusGainAnimator.setTarget(mButtonImageView); 100 101 mTextFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(), 102 R.anim.tv_pip_controls_focus_loss_animation); 103 mTextFocusLossAnimator.setTarget(mDescriptionTextView); 104 mButtonFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(), 105 R.anim.tv_pip_controls_focus_loss_animation); 106 mButtonFocusLossAnimator.setTarget(mButtonImageView); 107 } 108 109 @Override setOnClickListener(OnClickListener listener)110 public void setOnClickListener(OnClickListener listener) { 111 // We do not want to set an OnClickListener to the TvPipMenuActionButton itself, but only to 112 // the ImageView. So let's "cash" the listener we've been passed here and set a "proxy" 113 // listener to the ImageView. 114 mOnClickListener = listener; 115 mButtonImageView.setOnClickListener(listener != null ? this : null); 116 } 117 118 @Override onClick(View v)119 public void onClick(View v) { 120 if (mOnClickListener != null) { 121 // Pass the correct view - this. 122 mOnClickListener.onClick(this); 123 } 124 } 125 126 /** 127 * Sets the drawable for the button with the given drawable. 128 */ setImageDrawable(Drawable d)129 public void setImageDrawable(Drawable d) { 130 mIconImageView.setImageDrawable(d); 131 } 132 133 /** 134 * Sets the drawable for the button with the given resource id. 135 */ setImageResource(int resId)136 public void setImageResource(int resId) { 137 if (resId != 0) { 138 mIconImageView.setImageResource(resId); 139 } 140 } 141 142 /** 143 * Sets the text for description the with the given string. 144 */ setTextAndDescription(CharSequence text)145 public void setTextAndDescription(CharSequence text) { 146 mButtonImageView.setContentDescription(text); 147 mDescriptionTextView.setText(text); 148 } 149 cancelAnimator(Animator animator)150 private static void cancelAnimator(Animator animator) { 151 if (animator.isStarted()) { 152 animator.cancel(); 153 } 154 } 155 156 /** 157 * Starts the focus gain animation. 158 */ startFocusGainAnimation()159 public void startFocusGainAnimation() { 160 cancelAnimator(mButtonFocusLossAnimator); 161 cancelAnimator(mTextFocusLossAnimator); 162 mTextFocusGainAnimator.start(); 163 if (mButtonImageView.getAlpha() < 1f) { 164 // If we had faded out the ripple drawable, run our manual focus change animation. 165 // See the comment at {@link #startFocusLossAnimation()} for the reason of manual 166 // animator. 167 mButtonFocusGainAnimator.start(); 168 } 169 } 170 171 /** 172 * Starts the focus loss animation. 173 */ startFocusLossAnimation()174 public void startFocusLossAnimation() { 175 cancelAnimator(mButtonFocusGainAnimator); 176 cancelAnimator(mTextFocusGainAnimator); 177 mTextFocusLossAnimator.start(); 178 if (mButtonImageView.hasFocus()) { 179 // Button uses ripple that has the default animation for the focus changes. 180 // However, it doesn't expose the API to fade out while it is focused, so we should 181 // manually run the fade out animation when PIP controls row loses focus. 182 mButtonFocusLossAnimator.start(); 183 } 184 } 185 186 /** 187 * Resets to initial state. 188 */ reset()189 public void reset() { 190 cancelAnimator(mButtonFocusGainAnimator); 191 cancelAnimator(mTextFocusGainAnimator); 192 cancelAnimator(mButtonFocusLossAnimator); 193 cancelAnimator(mTextFocusLossAnimator); 194 mButtonImageView.setAlpha(1f); 195 mDescriptionTextView.setAlpha(mButtonImageView.hasFocus() ? 1f : 0f); 196 } 197 } 198