blob: 34b5596b3490c062c3bcb5d6797d4950aa31ae01 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.browse;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Typeface;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.mail.R;
import com.android.mail.R.dimen;
import com.android.mail.R.id;
import com.android.mail.ui.ViewMode;
import com.android.mail.utils.Utils;
import com.google.common.base.Objects;
/**
* Represents the coordinates of elements inside a CanvasConversationHeaderView
* (eg, checkmark, star, subject, sender, folders, etc.) It will inflate a view,
* and record the coordinates of each element after layout. This will allows us
* to easily improve performance by creating custom view while still defining
* layout in XML files.
*
* @author phamm
*/
public class ConversationItemViewCoordinates {
// Modes
static final int MODE_COUNT = 2;
static final int WIDE_MODE = 0;
static final int NORMAL_MODE = 1;
// Left-side gadget modes
static final int GADGET_NONE = 0;
static final int GADGET_CONTACT_PHOTO = 1;
static final int GADGET_CHECKBOX = 2;
// Attachment previews modes
static final int ATTACHMENT_PREVIEW_NONE = 0;
static final int ATTACHMENT_PREVIEW_UNREAD = 1;
static final int ATTACHMENT_PREVIEW_READ = 2;
// For combined views
private static int COLOR_BLOCK_WIDTH = -1;
private static int COLOR_BLOCK_HEIGHT = -1;
/**
* Simple holder class for an item's abstract configuration state. ListView binding creates an
* instance per item, and {@link #forConfig(Context, Config, SparseArray)} uses it to hide/show
* optional views and determine the correct coordinates for that item configuration.
*/
public static final class Config {
private int mWidth;
private int mViewMode = ViewMode.UNKNOWN;
private int mGadgetMode = GADGET_NONE;
private int mAttachmentPreviewMode = ATTACHMENT_PREVIEW_NONE;
private boolean mShowFolders = false;
private boolean mShowReplyState = false;
private boolean mShowColorBlock = false;
private boolean mShowPersonalIndicator = false;
public Config setViewMode(int viewMode) {
mViewMode = viewMode;
return this;
}
public Config withGadget(int gadget) {
mGadgetMode = gadget;
return this;
}
public Config withAttachmentPreviews(int attachmentPreviewMode) {
mAttachmentPreviewMode = attachmentPreviewMode;
return this;
}
public Config showFolders() {
mShowFolders = true;
return this;
}
public Config showReplyState() {
mShowReplyState = true;
return this;
}
public Config showColorBlock() {
mShowColorBlock = true;
return this;
}
public Config showPersonalIndicator() {
mShowPersonalIndicator = true;
return this;
}
public Config updateWidth(int width) {
mWidth = width;
return this;
}
public int getWidth() {
return mWidth;
}
public int getViewMode() {
return mViewMode;
}
public int getGadgetMode() {
return mGadgetMode;
}
public int getAttachmentPreviewMode() {
return mAttachmentPreviewMode;
}
public boolean areFoldersVisible() {
return mShowFolders;
}
public boolean isReplyStateVisible() {
return mShowReplyState;
}
public boolean isColorBlockVisible() {
return mShowColorBlock;
}
public boolean isPersonalIndicatorVisible() {
return mShowPersonalIndicator;
}
private int getCacheKey() {
// hash the attributes that contribute to item height and child view geometry
return Objects.hashCode(mWidth, mViewMode, mGadgetMode, mAttachmentPreviewMode,
mShowFolders, mShowReplyState, mShowPersonalIndicator);
}
}
/**
* One of either NORMAL_MODE or WIDE_MODE.
*/
private final int mMode;
final int height;
// Checkmark.
final int checkmarkX;
final int checkmarkY;
// Star.
final int starX;
final int starY;
final int starWidth;
// Senders.
final int sendersX;
final int sendersY;
final int sendersWidth;
final int sendersHeight;
final int sendersLineCount;
final int sendersLineHeight;
final float sendersFontSize;
final int sendersAscent;
// Subject.
final int subjectX;
final int subjectY;
final int subjectWidth;
final int subjectHeight;
final int subjectLineCount;
final float subjectFontSize;
final int subjectAscent;
// Folders.
final int foldersX;
final int foldersXEnd;
final int foldersY;
final int foldersHeight;
final Typeface foldersTypeface;
final float foldersFontSize;
final int foldersAscent;
final int foldersTextBottomPadding;
// Date.
final int dateXEnd;
final int dateY;
final int datePaddingLeft;
final float dateFontSize;
final int dateAscent;
final int dateYBaseline;
// Paperclip.
final int paperclipY;
final int paperclipPaddingLeft;
// Color block.
final int colorBlockX;
final int colorBlockY;
final int colorBlockWidth;
final int colorBlockHeight;
// Reply state of a conversation.
final int replyStateX;
final int replyStateY;
final int personalIndicatorX;
final int personalIndicatorY;
final int contactImagesHeight;
final int contactImagesWidth;
final int contactImagesX;
final int contactImagesY;
// Attachment previews
final int attachmentPreviewsX;
final int attachmentPreviewsY;
final int attachmentPreviewsWidth;
final int attachmentPreviewsHeight;
// Attachment previews overflow badge and count
final int overflowXEnd;
final int overflowYEnd;
final int overflowDiameter;
final float overflowFontSize;
final Typeface overflowTypeface;
// Attachment previews progress bar
final int progressBarY;
final int progressBarWidth;
final int progressBarHeight;
/**
* The smallest item width for which we use the "wide" layout.
*/
private final int mMinListWidthForWide;
/**
* The smallest item width for which we use the "spacious" variant of the normal layout,
* if the normal version is used at all. Larger than {@link #mMinListWidthForWide}, we use
* wide mode anyway, and this value is unused.
*/
private final int mMinListWidthIsSpacious;
private final int mFolderCellWidth;
private final int mFolderMinimumWidth;
private ConversationItemViewCoordinates(Context context, Config config) {
Utils.traceBeginSection("CIV coordinates constructor");
final Resources res = context.getResources();
mFolderCellWidth = res.getDimensionPixelSize(R.dimen.folder_cell_width);
mMinListWidthForWide = res.getDimensionPixelSize(R.dimen.list_min_width_is_wide);
mMinListWidthIsSpacious = res.getDimensionPixelSize(
R.dimen.list_normal_mode_min_width_is_spacious);
mFolderMinimumWidth = res.getDimensionPixelSize(R.dimen.folder_minimum_width);
mMode = calculateMode(res, config);
final int layoutId;
if (mMode == WIDE_MODE) {
layoutId = R.layout.conversation_item_view_wide;
} else {
if (config.getWidth() >= mMinListWidthIsSpacious) {
layoutId = R.layout.conversation_item_view_normal_spacious;
} else {
layoutId = R.layout.conversation_item_view_normal;
}
}
final ViewGroup view = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null);
// Show/hide optional views before measure/layout call
View attachmentPreviews = null;
if (config.getAttachmentPreviewMode() != ATTACHMENT_PREVIEW_NONE) {
attachmentPreviews = view.findViewById(R.id.attachment_previews);
LayoutParams params = attachmentPreviews.getLayoutParams();
attachmentPreviews.setVisibility(View.VISIBLE);
params.height = getAttachmentPreviewsHeight(context, config.getAttachmentPreviewMode());
attachmentPreviews.setLayoutParams(params);
}
final TextView folders = (TextView) view.findViewById(R.id.folders);
folders.setVisibility(config.areFoldersVisible() ? View.VISIBLE : View.GONE);
// Add margin between attachment previews and folders
View attachmentPreviewsBottomMargin = view
.findViewById(R.id.attachment_previews_bottom_margin);
attachmentPreviewsBottomMargin.setVisibility(
attachmentPreviews != null && config.areFoldersVisible() ? View.VISIBLE
: View.GONE);
View contactImagesView = view.findViewById(R.id.contact_image);
View checkmark = view.findViewById(R.id.checkmark);
switch (config.getGadgetMode()) {
case GADGET_CONTACT_PHOTO:
contactImagesView.setVisibility(View.VISIBLE);
checkmark.setVisibility(View.GONE);
checkmark = null;
break;
case GADGET_CHECKBOX:
contactImagesView.setVisibility(View.GONE);
checkmark.setVisibility(View.VISIBLE);
contactImagesView = null;
break;
default:
contactImagesView.setVisibility(View.GONE);
checkmark.setVisibility(View.GONE);
contactImagesView = null;
checkmark = null;
break;
}
final View replyState = view.findViewById(R.id.reply_state);
replyState.setVisibility(config.isReplyStateVisible() ? View.VISIBLE : View.GONE);
final View personalIndicator = view.findViewById(R.id.personal_indicator);
personalIndicator.setVisibility(
config.isPersonalIndicatorVisible() ? View.VISIBLE : View.GONE);
// Layout the appropriate view.
final int widthSpec = MeasureSpec.makeMeasureSpec(config.getWidth(), MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
view.measure(widthSpec, heightSpec);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
// Utils.dumpViewTree((ViewGroup) view);
// Records coordinates.
if (checkmark != null) {
checkmarkX = getX(checkmark);
checkmarkY = getY(checkmark);
} else {
checkmarkX = checkmarkY = 0;
}
// Contact images view
if (contactImagesView != null) {
contactImagesWidth = contactImagesView.getWidth();
contactImagesHeight = contactImagesView.getHeight();
contactImagesX = getX(contactImagesView);
contactImagesY = getY(contactImagesView);
} else {
contactImagesX = contactImagesY = contactImagesWidth = contactImagesHeight = 0;
}
final View star = view.findViewById(R.id.star);
starX = getX(star);
starY = getY(star);
starWidth = star.getWidth();
final TextView senders = (TextView) view.findViewById(R.id.senders);
final int sendersTopAdjust = getLatinTopAdjustment(senders);
sendersX = getX(senders);
sendersY = getY(senders) + sendersTopAdjust;
sendersWidth = senders.getWidth();
sendersHeight = senders.getHeight();
sendersLineCount = getLineCount(senders);
sendersLineHeight = senders.getLineHeight();
sendersFontSize = senders.getTextSize();
sendersAscent = (int) senders.getPaint().ascent();
final TextView subject = (TextView) view.findViewById(R.id.subject);
final int subjectTopAdjust = getLatinTopAdjustment(subject);
subjectX = getX(subject);
if (isWide()) {
subjectY = getY(subject) + subjectTopAdjust;
} else {
subjectY = getY(subject) + sendersTopAdjust;
}
subjectWidth = subject.getWidth();
subjectHeight = subject.getHeight();
subjectLineCount = getLineCount(subject);
subjectFontSize = subject.getTextSize();
subjectAscent = (int) subject.getPaint().ascent();
if (config.areFoldersVisible()) {
// vertically align folders min left edge with subject
foldersX = subjectX;
foldersXEnd = getX(folders) + folders.getWidth();
if (isWide()) {
foldersY = getY(folders);
} else {
foldersY = getY(folders) + sendersTopAdjust;
}
foldersHeight = folders.getHeight();
foldersTypeface = folders.getTypeface();
foldersTextBottomPadding = res
.getDimensionPixelSize(R.dimen.folders_text_bottom_padding);
foldersFontSize = folders.getTextSize();
foldersAscent = (int) folders.getPaint().ascent();
} else {
foldersX = 0;
foldersXEnd = 0;
foldersY = 0;
foldersHeight = 0;
foldersTypeface = null;
foldersTextBottomPadding = 0;
foldersFontSize = 0;
foldersAscent = 0;
}
final View colorBlock = view.findViewById(R.id.color_block);
if (config.isColorBlockVisible() && colorBlock != null) {
colorBlockX = getX(colorBlock);
colorBlockY = getY(colorBlock);
colorBlockWidth = colorBlock.getWidth();
colorBlockHeight = colorBlock.getHeight();
} else {
colorBlockX = colorBlockY = colorBlockWidth = colorBlockHeight = 0;
}
if (config.isReplyStateVisible()) {
replyStateX = getX(replyState);
replyStateY = getY(replyState);
} else {
replyStateX = replyStateY = 0;
}
if (config.isPersonalIndicatorVisible()) {
personalIndicatorX = getX(personalIndicator);
personalIndicatorY = getY(personalIndicator);
} else {
personalIndicatorX = personalIndicatorY = 0;
}
final TextView date = (TextView) view.findViewById(R.id.date);
dateXEnd = getX(date) + date.getWidth();
dateY = getY(date);
datePaddingLeft = date.getPaddingLeft();
dateFontSize = date.getTextSize();
dateYBaseline = dateY + getLatinTopAdjustment(date) + date.getBaseline();
dateAscent = (int) date.getPaint().ascent();
final View paperclip = view.findViewById(R.id.paperclip);
paperclipY = getY(paperclip);
paperclipPaddingLeft = paperclip.getPaddingLeft();
if (attachmentPreviews != null) {
attachmentPreviewsX = getAttachmentPreviewsX(attachmentPreviews,
config.mAttachmentPreviewMode);
attachmentPreviewsY = getY(attachmentPreviews) + sendersTopAdjust;
final int attachmentPreviewsXEnd;
if (isWide()) {
attachmentPreviewsXEnd = subjectX + subjectWidth;
} else {
attachmentPreviewsXEnd = starX + starWidth;
}
attachmentPreviewsWidth = attachmentPreviewsXEnd - attachmentPreviewsX;
attachmentPreviewsHeight = attachmentPreviews.getHeight();
// We only care about the right and bottom of the overflow count
final TextView overflow = (TextView) view.findViewById(id.ap_overflow);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) overflow.getLayoutParams();
overflowXEnd = attachmentPreviewsX + attachmentPreviewsWidth - params.rightMargin;
overflowYEnd = attachmentPreviewsY + attachmentPreviewsHeight - params.bottomMargin;
overflowDiameter = overflow.getWidth();
overflowFontSize = overflow.getTextSize();
overflowTypeface = overflow.getTypeface();
final View progressBar = view.findViewById(id.ap_progress_bar);
progressBarWidth = progressBar.getWidth();
progressBarHeight = progressBar.getHeight();
progressBarY = attachmentPreviewsY + attachmentPreviewsHeight / 2
- progressBarHeight / 2;
} else {
attachmentPreviewsX = 0;
attachmentPreviewsY = 0;
attachmentPreviewsWidth = 0;
attachmentPreviewsHeight = 0;
overflowXEnd = 0;
overflowYEnd = 0;
overflowDiameter = 0;
overflowFontSize = 0;
overflowTypeface = null;
progressBarY = 0;
progressBarWidth = 0;
progressBarHeight = 0;
}
height = view.getHeight() + (isWide() ? 0 : sendersTopAdjust);
Utils.traceEndSection();
}
public int getMode() {
return mMode;
}
public boolean isWide() {
return mMode == WIDE_MODE;
}
/**
* Returns a negative corrective value that you can apply to a TextView's vertical dimensions
* that will nudge the first line of text upwards such that uppercase Latin characters are
* truly top-aligned.
* <p>
* N.B. this will cause other characters to draw above the top! only use this if you have
* adequate top margin.
*
*/
private static int getLatinTopAdjustment(TextView t) {
final FontMetricsInt fmi = t.getPaint().getFontMetricsInt();
return (fmi.top - fmi.ascent);
}
/**
* Returns the mode of the header view (Wide/Normal).
*/
private int calculateMode(Resources res, Config config) {
switch (config.getViewMode()) {
case ViewMode.CONVERSATION_LIST:
return config.getWidth() >= mMinListWidthForWide ? WIDE_MODE : NORMAL_MODE;
case ViewMode.SEARCH_RESULTS_LIST:
return res.getInteger(R.integer.conversation_list_search_header_mode);
default:
return res.getInteger(R.integer.conversation_header_mode);
}
}
private int getAttachmentPreviewsX(View attachmentPreviews, int attachmentPreviewMode) {
if (isWide() || attachmentPreviewMode == ATTACHMENT_PREVIEW_READ) {
return subjectX;
}
return getX(attachmentPreviews);
}
private int getAttachmentPreviewsHeight(Context context, int attachmentPreviewMode) {
Resources res = context.getResources();
switch (attachmentPreviewMode) {
case ATTACHMENT_PREVIEW_UNREAD:
return (int) (isWide() ? res.getDimension(dimen.attachment_preview_height_tall_wide)
: res.getDimension(dimen.attachment_preview_height_tall));
case ATTACHMENT_PREVIEW_READ:
return (int) res.getDimension(dimen.attachment_preview_height_short);
default:
return 0;
}
}
/**
* Returns the x coordinates of a view by tracing up its hierarchy.
*/
private static int getX(View view) {
int x = 0;
while (view != null) {
x += (int) view.getX();
ViewParent parent = view.getParent();
view = parent != null ? (View) parent : null;
}
return x;
}
/**
* Returns the y coordinates of a view by tracing up its hierarchy.
*/
private static int getY(View view) {
int y = 0;
while (view != null) {
y += (int) view.getY();
ViewParent parent = view.getParent();
view = parent != null ? (View) parent : null;
}
return y;
}
/**
* Returns the number of lines of this text view. Delegates to built-in TextView logic on JB+.
*/
private static int getLineCount(TextView textView) {
if (Utils.isRunningJellybeanOrLater()) {
return textView.getMaxLines();
} else {
return Math.round(((float) textView.getHeight()) / textView.getLineHeight());
}
}
/**
* Returns the length (maximum of characters) of subject in this mode.
*/
public static int getSendersLength(Context context, int mode, boolean hasAttachments) {
final Resources res = context.getResources();
if (hasAttachments) {
return res.getIntArray(R.array.senders_with_attachment_lengths)[mode];
} else {
return res.getIntArray(R.array.senders_lengths)[mode];
}
}
@Deprecated
public static int getColorBlockWidth(Context context) {
Resources res = context.getResources();
if (COLOR_BLOCK_WIDTH <= 0) {
COLOR_BLOCK_WIDTH = res.getDimensionPixelSize(R.dimen.color_block_width);
}
return COLOR_BLOCK_WIDTH;
}
@Deprecated
public static int getColorBlockHeight(Context context) {
Resources res = context.getResources();
if (COLOR_BLOCK_HEIGHT <= 0) {
COLOR_BLOCK_HEIGHT = res.getDimensionPixelSize(R.dimen.color_block_height);
}
return COLOR_BLOCK_HEIGHT;
}
public static boolean displaySendersInline(int mode) {
switch (mode) {
case WIDE_MODE:
return false;
case NORMAL_MODE:
return true;
default:
throw new IllegalArgumentException("Unknown conversation header view mode " + mode);
}
}
/**
* Returns coordinates for elements inside a conversation header view given
* the view width.
*/
public static ConversationItemViewCoordinates forConfig(Context context, Config config,
SparseArray<ConversationItemViewCoordinates> cache) {
final int cacheKey = config.getCacheKey();
ConversationItemViewCoordinates coordinates = cache.get(cacheKey);
if (coordinates != null) {
return coordinates;
}
coordinates = new ConversationItemViewCoordinates(context, config);
cache.put(cacheKey, coordinates);
return coordinates;
}
/**
* Return the minimum width of a folder cell with no text. Essentially this is the left+right
* intra-cell margin within cells.
*
*/
public int getFolderCellWidth() {
return mFolderCellWidth;
}
/**
* Return the minimum width of a folder cell, period. This will affect the
* maximum number of folders we can display.
*/
public int getFolderMinimumWidth() {
return mFolderMinimumWidth;
}
public static boolean isWideMode(int mode) {
return mode == WIDE_MODE;
}
}