1 /* 2 * Copyright (C) 2014 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.mail.ui; 18 19 import android.animation.ObjectAnimator; 20 import android.app.LoaderManager; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.os.Bundle; 24 import androidx.annotation.LayoutRes; 25 import android.util.AttributeSet; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.animation.DecelerateInterpolator; 30 import android.widget.AbsListView; 31 import android.widget.ImageView; 32 import android.widget.LinearLayout; 33 import android.widget.TextView; 34 35 import com.android.mail.R; 36 import com.android.mail.browse.ConversationCursor; 37 import com.android.mail.providers.Folder; 38 import com.android.mail.utils.LogTag; 39 40 /** 41 * Base class to display tip teasers in the thread list. 42 * Supports two-line text and start/end icons. 43 */ 44 public abstract class ConversationTipView extends LinearLayout 45 implements ConversationSpecialItemView, SwipeableItemView, View.OnClickListener { 46 protected static final String LOG_TAG = LogTag.getLogTag(); 47 48 protected Context mContext; 49 protected AnimatedAdapter mAdapter; 50 51 private int mScrollSlop; 52 private int mShrinkAnimationDuration; 53 private int mAnimatedHeight = -1; 54 55 protected View mSwipeableContent; 56 private View mContent; 57 private TextView mText; 58 ConversationTipView(Context context)59 public ConversationTipView(Context context) { 60 this(context, null); 61 } 62 ConversationTipView(Context context, AttributeSet attrs)63 public ConversationTipView(Context context, AttributeSet attrs) { 64 super(context, attrs); 65 mContext = context; 66 67 final Resources resources = context.getResources(); 68 mScrollSlop = resources.getInteger(R.integer.swipeScrollSlop); 69 mShrinkAnimationDuration = resources.getInteger( 70 R.integer.shrink_animation_duration); 71 72 // Inflate the actual content and add it to this view 73 mContent = LayoutInflater.from(mContext).inflate(getChildLayout(), this, false); 74 addView(mContent); 75 setupViews(); 76 } 77 78 @Override getLayoutParams()79 public ViewGroup.LayoutParams getLayoutParams() { 80 ViewGroup.LayoutParams params = super.getLayoutParams(); 81 if (params != null) { 82 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 83 params.height = ViewGroup.LayoutParams.WRAP_CONTENT; 84 } 85 return params; 86 } 87 getChildLayout()88 protected @LayoutRes int getChildLayout() { 89 // Should override setupViews as well if this is overridden. 90 return R.layout.conversation_tip_view; 91 } 92 setupViews()93 protected void setupViews() { 94 // If this is overridden, child classes cannot rely on setText/getStartIconAttr/etc. 95 mSwipeableContent = mContent.findViewById(R.id.conversation_tip_swipeable_content); 96 mText = (TextView) mContent.findViewById(R.id.conversation_tip_text); 97 final ImageView startImage = (ImageView) mContent.findViewById(R.id.conversation_tip_icon1); 98 final ImageView dismiss = (ImageView) mContent.findViewById(R.id.dismiss_icon); 99 100 // Bind content (text content must be bound by calling setText(..)) 101 bindIcon(startImage, getStartIconAttr()); 102 103 // Bind listeners 104 dismiss.setOnClickListener(this); 105 mText.setOnClickListener(getTextAreaOnClickListener()); 106 } 107 108 /** 109 * Helper function to bind the additional attributes to the icon, or make the icon GONE. 110 */ bindIcon(ImageView image, ImageAttrSet attr)111 private void bindIcon(ImageView image, ImageAttrSet attr) { 112 if (attr != null) { 113 image.setVisibility(VISIBLE); 114 image.setContentDescription(attr.contentDescription); 115 // Must override resId for the actual icon, so no need to check -1 here. 116 image.setImageResource(attr.resId); 117 if (attr.background != -1) { 118 image.setBackgroundResource(attr.background); 119 } 120 } else { 121 image.setVisibility(GONE); 122 } 123 } 124 125 @Override onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)126 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 127 if (mAnimatedHeight == -1) { 128 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 129 } else { 130 setMeasuredDimension(View.MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight); 131 } 132 } 133 getStartIconAttr()134 protected ImageAttrSet getStartIconAttr() { 135 return null; 136 } 137 setText(CharSequence text)138 protected void setText(CharSequence text) { 139 mText.setText(text); 140 } 141 getTextAreaOnClickListener()142 protected OnClickListener getTextAreaOnClickListener() { 143 return null; 144 } 145 146 @Override onClick(View view)147 public void onClick(View view) { 148 // Default on click for the default dismiss button 149 dismiss(); 150 } 151 152 @Override onUpdate(Folder folder, ConversationCursor cursor)153 public void onUpdate(Folder folder, ConversationCursor cursor) { 154 // Do nothing by default 155 } 156 157 @Override onGetView()158 public void onGetView() { 159 // Do nothing by default 160 } 161 162 @Override getPosition()163 public int getPosition() { 164 // By default the tip teasers go on top of the list. 165 return 0; 166 } 167 168 @Override setAdapter(AnimatedAdapter adapter)169 public void setAdapter(AnimatedAdapter adapter) { 170 mAdapter = adapter; 171 } 172 173 @Override bindFragment(LoaderManager loaderManager, Bundle savedInstanceState)174 public void bindFragment(LoaderManager loaderManager, Bundle savedInstanceState) { 175 // Do nothing by default 176 } 177 178 @Override cleanup()179 public void cleanup() { 180 // Do nothing by default 181 } 182 183 @Override onConversationSelected()184 public void onConversationSelected() { 185 // Do nothing by default 186 } 187 188 @Override onCabModeEntered()189 public void onCabModeEntered() { 190 // Do nothing by default 191 } 192 193 @Override onCabModeExited()194 public void onCabModeExited() { 195 // Do nothing by default 196 } 197 198 @Override acceptsUserTaps()199 public boolean acceptsUserTaps() { 200 return true; 201 } 202 203 @Override onConversationListVisibilityChanged(boolean visible)204 public void onConversationListVisibilityChanged(boolean visible) { 205 // Do nothing by default 206 } 207 208 @Override saveInstanceState(Bundle outState)209 public void saveInstanceState(Bundle outState) { 210 // Do nothing by default 211 } 212 213 @Override commitLeaveBehindItem()214 public boolean commitLeaveBehindItem() { 215 // Tip has no leave-behind by default 216 return false; 217 } 218 219 @Override getSwipeableView()220 public SwipeableView getSwipeableView() { 221 return SwipeableView.from(mSwipeableContent); 222 } 223 224 @Override canChildBeDismissed()225 public boolean canChildBeDismissed() { 226 return true; 227 } 228 229 @Override dismiss()230 public void dismiss() { 231 startDestroyAnimation(); 232 } 233 234 @Override getMinAllowScrollDistance()235 public float getMinAllowScrollDistance() { 236 return mScrollSlop; 237 } 238 startDestroyAnimation()239 private void startDestroyAnimation() { 240 final int start = getHeight(); 241 final int end = 0; 242 mAnimatedHeight = start; 243 final ObjectAnimator heightAnimator = 244 ObjectAnimator.ofInt(this, "animatedHeight", start, end); 245 heightAnimator.setInterpolator(new DecelerateInterpolator(2.0f)); 246 heightAnimator.setDuration(mShrinkAnimationDuration); 247 heightAnimator.start(); 248 249 /* 250 * Ideally, we would like to call mAdapter.notifyDataSetChanged() in a listener's 251 * onAnimationEnd(), but we are in the middle of a touch event, and this will cause all the 252 * views to get recycled, which will cause problems. 253 * 254 * Instead, we'll just leave the item in the list with a height of 0, and the next 255 * notifyDatasetChanged() will remove it from the adapter. 256 */ 257 } 258 259 /** 260 * This method is used by the animator. It is explicitly kept in proguard.flags to prevent it 261 * from being removed, inlined, or obfuscated. 262 * Edit ./vendor/unbundled/packages/apps/UnifiedGmail/proguard.flags 263 * In the future, we want to use @Keep 264 */ setAnimatedHeight(final int height)265 public void setAnimatedHeight(final int height) { 266 mAnimatedHeight = height; 267 requestLayout(); 268 } 269 270 public static class ImageAttrSet { 271 // -1 for these resIds to not override the default value. 272 public int resId; 273 public int background; 274 public String contentDescription; 275 ImageAttrSet(int resId, int background, String contentDescription)276 public ImageAttrSet(int resId, int background, String contentDescription) { 277 this.resId = resId; 278 this.background = background; 279 this.contentDescription = contentDescription; 280 } 281 } 282 } 283