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