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