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 android.support.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