1 /*
2  * Copyright (C) 2016 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.settings.display;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.PorterDuff;
23 import android.graphics.drawable.Drawable;
24 import android.graphics.drawable.LayerDrawable;
25 import android.text.TextUtils;
26 import android.util.AttributeSet;
27 import android.view.Gravity;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.FrameLayout;
32 import android.widget.LinearLayout;
33 import android.widget.TextView;
34 
35 import com.android.settings.R;
36 
37 /**
38  * The view for a single entry in a conversation. This is a simplified version of
39  * com.android.messaging.ui.conversation.ConversationMessageView class.
40  */
41 public class ConversationMessageView extends FrameLayout {
42     private final boolean mIncoming;
43     private final CharSequence mMessageText;
44     private final CharSequence mTimestampText;
45     private final CharSequence mIconText;
46     private final int mIconTextColor;
47     private final int mIconBackgroundColor;
48 
49     private LinearLayout mMessageBubble;
50     private ViewGroup mMessageTextAndInfoView;
51     private TextView mMessageTextView;
52     private TextView mStatusTextView;
53     private TextView mContactIconView;
54 
ConversationMessageView(Context context)55     public ConversationMessageView(Context context) {
56         this(context, null);
57     }
58 
ConversationMessageView(final Context context, final AttributeSet attrs)59     public ConversationMessageView(final Context context, final AttributeSet attrs) {
60         this(context, attrs, 0);
61     }
62 
ConversationMessageView(Context context, AttributeSet attrs, int defStyleAttr)63     public ConversationMessageView(Context context, AttributeSet attrs, int defStyleAttr) {
64         this(context, attrs, defStyleAttr, 0);
65     }
66 
ConversationMessageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)67     public ConversationMessageView(Context context, AttributeSet attrs, int defStyleAttr,
68             int defStyleRes) {
69         super(context, attrs, defStyleAttr, defStyleRes);
70 
71         final TypedArray a = context.obtainStyledAttributes(attrs,
72                 R.styleable.ConversationMessageView);
73 
74         mIncoming = a.getBoolean(R.styleable.ConversationMessageView_incoming, true);
75         mMessageText = a.getString(R.styleable.ConversationMessageView_messageText);
76         mTimestampText = a.getString(R.styleable.ConversationMessageView_timestampText);
77         mIconText = a.getString(R.styleable.ConversationMessageView_iconText);
78         mIconTextColor = a.getColor(R.styleable.ConversationMessageView_iconTextColor, 0);
79         mIconBackgroundColor = a.getColor(R.styleable.ConversationMessageView_iconBackgroundColor,
80                 0);
81         a.recycle();
82         LayoutInflater.from(context).inflate(R.layout.conversation_message_icon, this);
83         LayoutInflater.from(context).inflate(R.layout.conversation_message_content, this);
84     }
85 
86     @Override
onFinishInflate()87     protected void onFinishInflate() {
88         mMessageBubble = (LinearLayout) findViewById(R.id.message_content);
89         mMessageTextAndInfoView = (ViewGroup) findViewById(R.id.message_text_and_info);
90         mMessageTextView = (TextView) findViewById(R.id.message_text);
91         mStatusTextView = (TextView) findViewById(R.id.message_status);
92         mContactIconView = (TextView) findViewById(R.id.conversation_icon);
93         updateViewContent();
94     }
95 
96     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)97     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
98         updateViewAppearance();
99 
100         final int horizontalSpace = MeasureSpec.getSize(widthMeasureSpec);
101 
102         final int unspecifiedMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
103         int iconMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
104 
105         mContactIconView.measure(iconMeasureSpec, iconMeasureSpec);
106         iconMeasureSpec = MeasureSpec.makeMeasureSpec(
107                 Math.max(mContactIconView.getMeasuredWidth(), mContactIconView.getMeasuredHeight()),
108                 MeasureSpec.EXACTLY);
109         mContactIconView.measure(iconMeasureSpec, iconMeasureSpec);
110 
111         final int messageBubblePadding =
112                 getResources().getDimensionPixelOffset(R.dimen.message_bubble_left_right_padding);
113 
114         // We need to subtract contact icon width twice from the horizontal space to get
115         // the max leftover space because we want the message bubble to extend no further than the
116         // starting position of the message bubble in the opposite direction.
117         final int maxLeftoverSpace = horizontalSpace - mContactIconView.getMeasuredWidth() * 2
118                 - messageBubblePadding - getPaddingLeft() - getPaddingRight();
119         final int messageContentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxLeftoverSpace,
120                 MeasureSpec.AT_MOST);
121 
122         mMessageBubble.measure(messageContentWidthMeasureSpec, unspecifiedMeasureSpec);
123 
124         final int maxHeight = Math.max(mContactIconView.getMeasuredHeight(),
125                 mMessageBubble.getMeasuredHeight());
126         setMeasuredDimension(horizontalSpace, maxHeight + getPaddingBottom() + getPaddingTop());
127     }
128 
129     @Override
onLayout(final boolean changed, final int left, final int top, final int right, final int bottom)130     protected void onLayout(final boolean changed, final int left, final int top, final int right,
131             final int bottom) {
132         final boolean isRtl = isLayoutRtl(this);
133 
134         final int iconWidth = mContactIconView.getMeasuredWidth();
135         final int iconHeight = mContactIconView.getMeasuredHeight();
136         final int iconTop = getPaddingTop();
137         final int contentWidth = (right -left) - iconWidth - getPaddingLeft() - getPaddingRight();
138         final int contentHeight = mMessageBubble.getMeasuredHeight();
139         final int contentTop = iconTop;
140 
141         final int iconLeft;
142         final int contentLeft;
143 
144         if (mIncoming) {
145             if (isRtl) {
146                 iconLeft = (right - left) - getPaddingRight() - iconWidth;
147                 contentLeft = iconLeft - contentWidth;
148             } else {
149                 iconLeft = getPaddingLeft();
150                 contentLeft = iconLeft + iconWidth;
151             }
152         } else {
153             if (isRtl) {
154                 iconLeft = getPaddingLeft();
155                 contentLeft = iconLeft + iconWidth;
156             } else {
157                 iconLeft = (right - left) - getPaddingRight() - iconWidth;
158                 contentLeft = iconLeft - contentWidth;
159             }
160         }
161 
162         mContactIconView.layout(iconLeft, iconTop, iconLeft + iconWidth, iconTop + iconHeight);
163 
164         mMessageBubble.layout(contentLeft, contentTop, contentLeft + contentWidth,
165                 contentTop + contentHeight);
166     }
167 
isLayoutRtl(final View view)168     private static boolean isLayoutRtl(final View view) {
169         return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection();
170     }
171 
updateViewContent()172     private void updateViewContent() {
173         mMessageTextView.setText(mMessageText);
174         mStatusTextView.setText(mTimestampText);
175         mStatusTextView.setVisibility(TextUtils.isEmpty(mTimestampText) ? GONE : VISIBLE);
176         mContactIconView.setText(mIconText);
177 
178         mContactIconView.setTextColor(mIconTextColor);
179         final Drawable iconBackground = getContext().getDrawable(
180                 R.drawable.conversation_message_icon);
181         final Drawable icon = getContext().getDrawable(R.drawable.ic_person);
182         final LayerDrawable layerDrawable = new LayerDrawable(
183                 new Drawable[]{
184                         getTintedDrawable(getContext(), iconBackground, mIconBackgroundColor),
185                         getTintedDrawable(getContext(), icon,
186                                 getContext().getColor(R.color.message_icon_color))});
187         final int iconInset = getResources().getDimensionPixelOffset(R.dimen.message_icon_inset);
188         layerDrawable.setLayerInset(/* index= */ 1, iconInset, iconInset, iconInset, iconInset);
189         mContactIconView.setBackground(layerDrawable);
190     }
191 
updateViewAppearance()192     private void updateViewAppearance() {
193         final Resources res = getResources();
194 
195         final int messageBubbleLeftRightPadding = res.getDimensionPixelOffset(
196                 R.dimen.message_bubble_left_right_padding);
197         final int messageTextLeftRightPadding = res.getDimensionPixelOffset(
198                 R.dimen.message_text_left_right_padding);
199         final int textTopPadding = res.getDimensionPixelOffset(
200                 R.dimen.message_text_top_padding);
201         final int textBottomPadding = res.getDimensionPixelOffset(
202                 R.dimen.message_text_bottom_padding);
203 
204         final int textLeftPadding, textRightPadding;
205         textLeftPadding = messageTextLeftRightPadding;
206         textRightPadding = messageTextLeftRightPadding;
207 
208         final int messageBubbleLeftPadding = mIncoming ? messageBubbleLeftRightPadding : 0;
209         final int messageBubbleRightPadding = mIncoming ? 0 : messageBubbleLeftRightPadding;
210 
211         // These values do not depend on whether the message includes attachments
212         final int gravity = mIncoming ? (Gravity.START | Gravity.CENTER_VERTICAL) :
213                 (Gravity.END | Gravity.CENTER_VERTICAL);
214         final int messageTopPadding = res.getDimensionPixelSize(
215                 R.dimen.message_padding_default);
216         final int metadataTopPadding =  res.getDimensionPixelOffset(
217                 R.dimen.message_metadata_top_padding);
218 
219         // Update the message text/info views
220         final int bubbleDrawableResId = mIncoming ? R.drawable.msg_bubble_incoming
221                 : R.drawable.msg_bubble_outgoing;
222         final int bubbleColorResId = mIncoming ? R.color.message_bubble_incoming
223                 : R.color.message_bubble_outgoing;
224         final Context context = getContext();
225 
226         final Drawable textBackgroundDrawable = getTintedDrawable(context,
227                 context.getDrawable(R.drawable.conversation_message_text_info_view_background),
228                 context.getColor(bubbleColorResId));
229         mMessageTextAndInfoView.setBackground(textBackgroundDrawable);
230 
231         if (isLayoutRtl(this)) {
232             // Need to switch right and left padding in RtL mode
233             mMessageTextAndInfoView.setPadding(textRightPadding,
234                     textTopPadding + metadataTopPadding,
235                     textLeftPadding, textBottomPadding);
236         } else {
237             mMessageTextAndInfoView.setPadding(textLeftPadding,
238                     textTopPadding + metadataTopPadding,
239                     textRightPadding, textBottomPadding);
240         }
241 
242         // Update the message row and message bubble views
243         setPadding(getPaddingLeft(), messageTopPadding, getPaddingRight(), 0);
244         mMessageBubble.setGravity(gravity);
245         mMessageBubble.setPaddingRelative(messageBubbleLeftPadding, /* top= */ 0,
246                 messageBubbleRightPadding, /* bottom= */ 0);
247 
248         updateTextAppearance();
249     }
250 
updateTextAppearance()251     private void updateTextAppearance() {
252         final int messageColorResId = (mIncoming ? R.color.message_text_incoming
253                 : R.color.message_text_outgoing);
254         final int timestampColorResId = mIncoming ? R.color.timestamp_text_incoming
255                 : R.color.timestamp_text_outgoing;
256         final int messageColor = getContext().getColor(messageColorResId);
257 
258         mMessageTextView.setTextColor(messageColor);
259         mMessageTextView.setLinkTextColor(messageColor);
260         mStatusTextView.setTextColor(timestampColorResId);
261     }
262 
getTintedDrawable(final Context context, final Drawable drawable, final int color)263     private static Drawable getTintedDrawable(final Context context, final Drawable drawable,
264             final int color) {
265         // For some reason occasionally drawables on JB has a null constant state
266         final Drawable.ConstantState constantStateDrawable = drawable.getConstantState();
267         final Drawable retDrawable = (constantStateDrawable != null)
268                 ? constantStateDrawable.newDrawable(context.getResources()).mutate()
269                 : drawable;
270         retDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
271         return retDrawable;
272     }
273 }
274