Re-work the layout computation for MessageListItem

This now uses an XML layout that gets inflated and computed after a
layout pass. The drawing simply uses the coordinates stored after the
layout.

This makes it a lot easier to maintain the different views and allows us
to simply provide different XML files for different modes/views, isntead
of trying to hand tweak Java layout code.

Some TODOs - clean up the "paints" and optimize the layout/drawing
computation

Change-Id: I784919f726bd4d80aba8744a8f047fcfe79ad93a
This commit is contained in:
Ben Komalo 2011-04-15 11:09:37 -07:00
parent 058eb3984f
commit 8b2109f047
8 changed files with 684 additions and 236 deletions

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<!-- This layout is used as a template to create a custom message item view
in narrow mode. To be able to get the correct measurements, every source field should
be populated with data here. E.g:
- Text View should set text to a random long string (android:text="@string/long_string")
- Image View should set source to a specific asset -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="@+id/checkmark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:src="@drawable/btn_check_on_normal_holo_light" />
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/senders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/long_string"
android:textSize="@dimen/senders_font_size"
android:lines="1" />
<TextView
android:id="@+id/subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/long_string"
android:textSize="@dimen/subject_font_size"
android:lines="2" />
</LinearLayout>
<LinearLayout
android:layout_width="80dip"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="right">
<View
android:id="@+id/color_chip"
android:layout_width="@dimen/message_list_item_color_tip_width"
android:layout_height="@dimen/message_list_item_color_tip_height"
android:layout_marginRight="16dip" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<ImageView
android:id="@+id/paperclip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_attachment_holo_light" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:layout_marginRight="16dip"
android:text="@string/long_string"
android:lines="1" />
</LinearLayout>
<ImageView
android:id="@+id/star"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:src="@drawable/btn_star_off_normal_email_holo_light" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<!-- This layout is used as a template to create custom view CanvasConversationHeaderView
in normal mode. To be able to get the correct measurements, every source field should
be populated with data here. E.g:
- Text View should set text to a random long string (android:text="@string/long_string")
- Image View should set source to a specific asset -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="@+id/checkmark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:src="@drawable/btn_check_on_normal_holo_light" />
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/senders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/long_string"
android:textSize="@dimen/senders_font_size"
android:lines="1" />
<TextView
android:id="@+id/subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/long_string"
android:textSize="@dimen/subject_font_size"
android:lines="2" />
</LinearLayout>
<LinearLayout
android:layout_width="136dip"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="right">
<View
android:id="@+id/color_chip"
android:layout_width="@dimen/message_list_item_color_tip_width"
android:layout_height="@dimen/message_list_item_color_tip_height"
android:layout_marginRight="8dip"
android:paddingTop="2dip" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<ImageView
android:id="@+id/paperclip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_attachment_holo_light" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dip"
android:layout_marginRight="8dip"
android:text="@string/long_string"
android:lines="1" />
</LinearLayout>
<ImageView
android:id="@+id/star"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip"
android:layout_marginRight="8dip"
android:src="@drawable/btn_star_off_normal_email_holo_light" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<!-- This layout is used as a template to create custom message list item view
in wide mode. To be able to get the correct measurements, every source field should
be populated with data here. E.g:
- Text View should set text to a random long string (android:text="@string/long_string")
- Image View should set source to a specific asset -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="@+id/checkmark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:layout_gravity="center_vertical"
android:src="@drawable/btn_check_on_normal_holo_light" />
<TextView
android:id="@+id/senders"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:layout_gravity="center_vertical"
android:text="@string/long_string"
android:textSize="@dimen/wide_senders_font_size"
android:lines="1" />
<TextView
android:id="@+id/subject"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:layout_gravity="center_vertical"
android:text="@string/long_string"
android:textSize="@dimen/subject_font_size"
android:lines="2" />
<RelativeLayout
android:layout_width="144dip"
android:layout_height="match_parent">
<View
android:id="@+id/color_chip"
android:layout_width="@dimen/message_list_item_color_tip_width"
android:layout_height="@dimen/message_list_item_color_tip_height"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:paddingTop="2dip" />
<ImageView
android:id="@+id/paperclip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@drawable/ic_attachment_holo_light" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:text="@string/long_string"
android:lines="1" />
</RelativeLayout>
<ImageView
android:id="@+id/star"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:layout_gravity="center_vertical"
android:src="@drawable/btn_star_off_normal_email_holo_light" />
</LinearLayout>

View File

@ -190,4 +190,22 @@
<item>@color/combined_view_account_color_8</item>
<item>@color/combined_view_account_color_9</item>
</array>
<!-- TODO: rename -->
<!-- Values in dip of the message list item heights in the various modes:
wide, normal, and narrow -->
<integer-array name="conversation_heights">
<item>61</item>
<item>77</item>
<item>77</item>
</integer-array>
<!-- TODO: rename to subject_widths -->
<!-- Length of the subject text in the message list items in dips for the
various modes: wide, normal, and narrow -->
<integer-array name="subject_lengths">
<item>50</item>
<item>50</item>
<item>25</item>
</integer-array>
</resources>

View File

@ -19,26 +19,10 @@
<resources>
<dimen name="button_minWidth">100sp</dimen>
<dimen name="message_list_item_text_size">16dip</dimen>
<dimen name="message_list_item_checkbox_hit_width">64dip</dimen>
<dimen name="message_list_item_favorite_hit_width">64dip</dimen>
<dimen name="message_list_item_favorite_padding_right">16dip</dimen>
<dimen name="message_list_item_badge_padding_top">8dip</dimen>
<dimen name="message_list_item_badge_padding_right">-8dip</dimen>
<dimen name="message_list_item_date_icon_width_wide">144dip</dimen>
<dimen name="message_list_item_date_icon_width_narrow">112dip</dimen>
<dimen name="message_list_item_sender_padding_top_narrow">10dip</dimen>
<dimen name="message_list_item_sender_width">200dip</dimen>
<dimen name="message_list_item_padding_large">32dip</dimen>
<dimen name="message_list_item_padding_medium">6dip</dimen>
<dimen name="message_list_item_padding_small">4dip</dimen>
<dimen name="message_list_item_padding_very_small">2dip</dimen>
<dimen name="message_list_item_height_wide">64dip</dimen>
<dimen name="message_list_item_height_narrow">80dip</dimen>
<dimen name="message_list_item_minimum_width_wide_mode">720dip</dimen>
<dimen name="message_list_item_color_tip_width">35dip</dimen>
<dimen name="message_list_item_color_tip_height">8dip</dimen>
<dimen name="message_list_item_color_tip_right_margin_on_narrow">32dip</dimen>
<dimen name="message_list_item_color_tip_right_margin_on_wide">65dip</dimen>
<dimen name="error_message_height">48dip</dimen>
@ -71,4 +55,11 @@
<dimen name="account_spinner_dropdown_account_name_max_width">352dip</dimen>
<!-- MessageListItem -->
<dimen name="senders_font_size">14sp</dimen>
<dimen name="wide_senders_font_size">16sp</dimen>
<dimen name="subject_font_size">16sp</dimen>
<dimen name="date_font_size">14sp</dimen>
<dimen name="minimum_width_wide_mode">800dip</dimen>
<dimen name="minimum_width_normal_mode">550dip</dimen>
</resources>

View File

@ -1093,4 +1093,8 @@ save attachment.</string>
<string name="mailbox_options_check_frequency_label">Mailbox check frequency</string>
<!-- In Mailbox setings, label for email check lookback selector -->
<string name="mailbox_options_lookback_label">Mailbox check lookback</string>
<!-- A long placeholder string to be used in template XML files message_list_item_*.xml.
Used only in layout computation, and never actually exposed to the user. -->
<string name="long_string" translatable="false">looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong</string>
</resources>

View File

@ -25,8 +25,6 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.Layout.Alignment;
@ -38,7 +36,6 @@ import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.format.DateUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.MotionEvent;
@ -56,6 +53,8 @@ public class MessageListItem extends View {
/* package */ long mAccountId;
private MessagesAdapter mAdapter;
private MessageListItemCoordinates mCoordinates;
private Context mContext;
private boolean mDownEvent;
@ -77,13 +76,11 @@ public class MessageListItem extends View {
init(context);
}
// We always show two lines of subject/snippet
private static final int MAX_SUBJECT_SNIPPET_LINES = 2;
// Narrow mode shows sender/snippet and time/favorite stacked to save real estate; due to this,
// it is also somewhat taller
private static final int MODE_NARROW = 1;
private static final int MODE_NARROW = MessageListItemCoordinates.NARROW_MODE;
// Wide mode shows sender, snippet, time, and favorite spread out across the screen
private static final int MODE_WIDE = 2;
private static final int MODE_WIDE = MessageListItemCoordinates.WIDE_MODE;
// Sentinel indicating that the view needs layout
public static final int NEEDS_LAYOUT = -1;
@ -96,7 +93,6 @@ public class MessageListItem extends View {
private static Bitmap sInviteIcon;
private static Bitmap sFavoriteIconOff;
private static Bitmap sFavoriteIconOn;
private static int sFavoriteIconWidth;
private static Bitmap sSelectedIconOn;
private static Bitmap sSelectedIconOff;
private static String sSubjectSnippetDivider;
@ -105,6 +101,7 @@ public class MessageListItem extends View {
public CharSequence mText;
public CharSequence mSnippet;
public String mSubject;
private StaticLayout mSubjectLayout;
public boolean mRead;
public long mTimestamp;
public boolean mHasAttachment = false;
@ -117,31 +114,10 @@ public class MessageListItem extends View {
private int mViewWidth = 0;
private int mViewHeight = 0;
private int mSenderSnippetWidth;
private int mSnippetWidth;
private int mDateFaveWidth;
private static int sCheckboxHitWidth;
private static int sDateIconWidthWide;
private static int sDateIconWidthNarrow;
private static int sFavoriteHitWidth;
private static int sFavoritePaddingRight;
private static int sBadgePaddingTop;
private static int sBadgePaddingRight;
private static int sSenderPaddingTopNarrow;
private static int sSenderWidth;
private static int sPaddingLarge;
private static int sPaddingVerySmall;
private static int sPaddingSmall;
private static int sPaddingMedium;
private static int sTextSize;
private static int sItemHeightWide;
private static int sItemHeightNarrow;
private static int sMinimumWidthWideMode;
private static int sColorTipWidth;
private static int sColorTipHeight;
private static int sColorTipRightMarginOnNarrow;
private static int sColorTipRightMarginOnWide;
// Note: these cannot be shared Drawables because they are selectors which have state.
private Drawable mReadSelector;
@ -150,66 +126,28 @@ public class MessageListItem extends View {
private Drawable mWideUnreadSelector;
public int mSnippetLineCount = NEEDS_LAYOUT;
private final CharSequence[] mSnippetLines = new CharSequence[MAX_SUBJECT_SNIPPET_LINES];
private CharSequence mFormattedSender;
private CharSequence mFormattedDate;
private void init(Context context) {
mContext = context;
if (!sInit) {
Resources r = context.getResources();
sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider);
sCheckboxHitWidth =
r.getDimensionPixelSize(R.dimen.message_list_item_checkbox_hit_width);
sFavoriteHitWidth =
r.getDimensionPixelSize(R.dimen.message_list_item_favorite_hit_width);
sFavoritePaddingRight =
r.getDimensionPixelSize(R.dimen.message_list_item_favorite_padding_right);
sBadgePaddingTop =
r.getDimensionPixelSize(R.dimen.message_list_item_badge_padding_top);
sBadgePaddingRight =
r.getDimensionPixelSize(R.dimen.message_list_item_badge_padding_right);
sSenderPaddingTopNarrow =
r.getDimensionPixelSize(R.dimen.message_list_item_sender_padding_top_narrow);
sDateIconWidthWide =
r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_wide);
sDateIconWidthNarrow =
r.getDimensionPixelSize(R.dimen.message_list_item_date_icon_width_narrow);
sSenderWidth =
r.getDimensionPixelSize(R.dimen.message_list_item_sender_width);
sPaddingLarge =
r.getDimensionPixelSize(R.dimen.message_list_item_padding_large);
sPaddingMedium =
r.getDimensionPixelSize(R.dimen.message_list_item_padding_medium);
sPaddingSmall =
r.getDimensionPixelSize(R.dimen.message_list_item_padding_small);
sPaddingVerySmall =
r.getDimensionPixelSize(R.dimen.message_list_item_padding_very_small);
sTextSize =
r.getDimensionPixelSize(R.dimen.message_list_item_text_size);
sItemHeightWide =
r.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
sItemHeightNarrow =
r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow);
sMinimumWidthWideMode =
r.getDimensionPixelSize(R.dimen.message_list_item_minimum_width_wide_mode);
sColorTipWidth =
r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_width);
sColorTipHeight =
r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_height);
sColorTipRightMarginOnNarrow =
r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_narrow);
sColorTipRightMarginOnWide =
r.getDimensionPixelSize(R.dimen.message_list_item_color_tip_right_margin_on_wide);
sDefaultPaint.setTypeface(Typeface.DEFAULT);
sDefaultPaint.setTextSize(sTextSize);
sDefaultPaint.setAntiAlias(true);
sDatePaint.setTypeface(Typeface.DEFAULT);
sDatePaint.setTextSize(sTextSize - 1);
sDatePaint.setAntiAlias(true);
sDatePaint.setTextAlign(Align.RIGHT);
sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
sBoldPaint.setTextSize(sTextSize);
sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
sBoldPaint.setAntiAlias(true);
sHighlightPaint.setColor(TextUtilities.HIGHLIGHT_COLOR_INT);
sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment);
@ -223,7 +161,6 @@ public class MessageListItem extends View {
sSelectedIconOn =
BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);
sFavoriteIconWidth = sFavoriteIconOff.getWidth();
sInit = true;
}
}
@ -235,16 +172,12 @@ public class MessageListItem extends View {
* @return The mode of the view
*/
private int getViewMode(int width) {
int mode = MODE_NARROW;
if (width > sMinimumWidthWideMode) {
mode = MODE_WIDE;
}
return mode;
return MessageListItemCoordinates.getMode(mContext, width);
}
private Drawable mCurentBackground = null; // Only used by updateBackground()
/* package */ void updateBackground() {
private void updateBackground() {
final Drawable newBackground;
if (mRead) {
if (mMode == MODE_WIDE) {
@ -300,43 +233,19 @@ public class MessageListItem extends View {
}
mText = ssb;
if (mMode == MODE_WIDE) {
mDateFaveWidth = sFavoriteHitWidth + sDateIconWidthWide;
} else {
mDateFaveWidth = sDateIconWidthNarrow;
}
mSenderSnippetWidth = mViewWidth - mDateFaveWidth - sCheckboxHitWidth;
// In wide mode, we use 3/4 for snippet and 1/4 for sender
mSnippetWidth = mSenderSnippetWidth;
if (mMode == MODE_WIDE) {
mSnippetWidth = mSenderSnippetWidth - sSenderWidth - sPaddingLarge;
}
// Create a StaticLayout with our snippet to get the line breaks
StaticLayout layout = new StaticLayout(mText, 0, mText.length(), sDefaultPaint,
mSnippetWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
// Get the number of lines needed to render the whole snippet
mSnippetLineCount = layout.getLineCount();
// Go through our maximum number of lines, and save away what we'll end up displaying
// for those lines
for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES; i++) {
int start = layout.getLineStart(i);
if (i == MAX_SUBJECT_SNIPPET_LINES - 1) {
int end = mText.length();
if (start > end) continue;
// For the final line, ellipsize the text to our width
mSnippetLines[i] = TextUtils.ellipsize(mText.subSequence(start, end), sDefaultPaint,
mSnippetWidth, TruncateAt.END);
} else {
// Just extract from start to end
mSnippetLines[i] = mText.subSequence(start, layout.getLineEnd(i));
}
sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
mSubjectLayout = new StaticLayout(mText, sDefaultPaint,
mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
if (mCoordinates.subjectLineCount < mSubjectLayout.getLineCount()) {
// TODO: ellipsize.
int end = mSubjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
mSubjectLayout = new StaticLayout(mText.subSequence(0, end),
sDefaultPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
}
// Now, format the sender for its width
TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
int senderWidth = (mMode == MODE_WIDE) ? sSenderWidth : mSenderSnippetWidth;
int senderWidth = mCoordinates.sendersWidth;
// And get the ellipsized string for the calculated width
if (TextUtils.isEmpty(mSender)) {
mFormattedSender = "";
@ -345,9 +254,7 @@ public class MessageListItem extends View {
TruncateAt.END);
}
// Get a nicely formatted date string (relative to today)
String date = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString();
// And make it fit to our size
mFormattedDate = TextUtils.ellipsize(date, sDatePaint, sDateIconWidthWide, TruncateAt.END);
mFormattedDate = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString();
}
@Override
@ -404,133 +311,72 @@ public class MessageListItem extends View {
super.draw(canvas);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mCoordinates = MessageListItemCoordinates.forWidth(mContext, mViewWidth);
}
@Override
protected void onDraw(Canvas canvas) {
if (mSnippetLineCount == NEEDS_LAYOUT) {
calculateDrawingData();
}
// Snippet starts at right of checkbox
int snippetX = sCheckboxHitWidth;
int snippetY;
int lineHeight = (int)sDefaultPaint.getFontSpacing() + sPaddingVerySmall;
FontMetricsInt fontMetrics = sDefaultPaint.getFontMetricsInt();
int ascent = fontMetrics.ascent;
int descent = fontMetrics.descent;
int senderY;
if (mMode == MODE_WIDE) {
// Get the right starting point for the snippet
snippetX += sSenderWidth + sPaddingLarge;
// And center the sender and snippet
senderY = (mViewHeight - descent - ascent) / 2;
snippetY = ((mViewHeight - (2 * lineHeight)) / 2) - ascent;
} else {
senderY = -ascent + sSenderPaddingTopNarrow;
snippetY = senderY + lineHeight + sPaddingVerySmall;
}
// Draw the color chip
// Draw the color chip indicating the mailbox this belongs to
if (mColorChipPaint != null) {
final int rightMargin = (mMode == MODE_WIDE)
? sColorTipRightMarginOnWide : sColorTipRightMarginOnNarrow;
final int x = mViewWidth - rightMargin - sColorTipWidth;
canvas.drawRect(x, 0, x + sColorTipWidth, sColorTipHeight, mColorChipPaint);
canvas.drawRect(
mCoordinates.chipX, mCoordinates.chipY,
mCoordinates.chipX + mCoordinates.chipWidth,
mCoordinates.chipY + mCoordinates.chipHeight,
mColorChipPaint);
}
// Draw the checkbox
int checkboxLeft = (sCheckboxHitWidth - sSelectedIconOff.getWidth()) / 2;
int checkboxTop = (mViewHeight - sSelectedIconOff.getHeight()) / 2;
canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
checkboxLeft, checkboxTop, sDefaultPaint);
mCoordinates.checkmarkX, mCoordinates.checkmarkY, sDefaultPaint);
// Draw the sender name
canvas.drawText(mFormattedSender, 0, mFormattedSender.length(), sCheckboxHitWidth, senderY,
canvas.drawText(mFormattedSender, 0, mFormattedSender.length(),
mCoordinates.sendersX, mCoordinates.sendersY - mCoordinates.sendersAscent,
mRead ? sDefaultPaint : sBoldPaint);
// Draw each of the snippet lines
for (int i = 0; i < MAX_SUBJECT_SNIPPET_LINES && i < mSnippetLineCount; i++) {
CharSequence line = mSnippetLines[i];
int drawX = snippetX;
if (line != null) {
SpannableStringBuilder ssb = (SpannableStringBuilder)line;
Object[] spans = ssb.getSpans(0, line.length(), Object.class);
int curr = 0;
for (Object span: spans) {
if (span != null) {
int spanStart = ssb.getSpanStart(span);
int spanEnd = ssb.getSpanEnd(span);
if (curr < spanStart) {
canvas.drawText(line, curr, spanStart, drawX, snippetY, sDefaultPaint);
drawX += sDefaultPaint.measureText(line, curr, spanStart);
}
TextPaint spanPaint =
(span instanceof StyleSpan) ? sBoldPaint : sDefaultPaint;
float textWidth = spanPaint.measureText(line, spanStart, spanEnd);
if (span instanceof BackgroundColorSpan) {
canvas.drawRect(drawX, snippetY + ascent, drawX + textWidth,
snippetY + descent, sHighlightPaint);
}
canvas.drawText(line, spanStart, spanEnd, drawX, snippetY, spanPaint);
drawX += textWidth;
curr = spanEnd;
}
}
canvas.drawText(line, curr, line.length(), drawX, snippetY, sDefaultPaint);
snippetY += lineHeight;
}
}
// Draw the attachment and invite icons, if necessary
int datePaddingRight;
if (mMode == MODE_WIDE) {
datePaddingRight = sFavoriteHitWidth;
} else {
datePaddingRight = sPaddingLarge;
}
int left = mViewWidth - datePaddingRight - (int)sDefaultPaint.measureText(mFormattedDate,
0, mFormattedDate.length()) - sPaddingMedium;
int iconTop;
if (mHasAttachment) {
left -= sAttachmentIcon.getWidth();
if (mMode == MODE_WIDE) {
iconTop = (mViewHeight - sAttachmentIcon.getHeight()) / 2;
left -= sPaddingSmall;
} else {
iconTop = senderY - sAttachmentIcon.getHeight() + sBadgePaddingTop;
left -= sBadgePaddingRight;
}
canvas.drawBitmap(sAttachmentIcon, left, iconTop, sDefaultPaint);
}
if (mHasInvite) {
left -= sInviteIcon.getWidth();
if (mMode == MODE_WIDE) {
iconTop = (mViewHeight - sInviteIcon.getHeight()) / 2;
left -= sPaddingSmall;
} else {
iconTop = senderY - sInviteIcon.getHeight() + sBadgePaddingTop;
left -= sBadgePaddingRight;
}
canvas.drawBitmap(sInviteIcon, left, iconTop, sDefaultPaint);
}
// Subject and snippet.
sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
sDefaultPaint.setTypeface(Typeface.DEFAULT);
canvas.save();
canvas.translate(
mCoordinates.subjectX,
mCoordinates.subjectY);
mSubjectLayout.draw(canvas);
canvas.restore();
// Draw the date
canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), mViewWidth - datePaddingRight,
senderY, sDatePaint);
sDatePaint.setTextSize(mCoordinates.dateFontSize);
int dateX = mCoordinates.dateXEnd
- (int) sDatePaint.measureText(mFormattedDate, 0, mFormattedDate.length());
canvas.drawText(mFormattedDate, 0, mFormattedDate.length(),
dateX, mCoordinates.dateY - mCoordinates.dateAscent, sDatePaint);
// Draw the favorite icon
int faveLeft = mViewWidth - sFavoriteIconWidth;
if (mMode == MODE_WIDE) {
faveLeft -= sFavoritePaddingRight;
} else {
faveLeft -= sPaddingLarge;
canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff,
mCoordinates.starX, mCoordinates.starY, sDefaultPaint);
// TODO: deal with the icon layouts better from the coordinate class so that this logic
// doesn't have to exist.
// Draw the attachment and invite icons, if necessary.
int iconsLeft = dateX;
if (mHasAttachment) {
iconsLeft = iconsLeft - sAttachmentIcon.getWidth();
canvas.drawBitmap(sAttachmentIcon, iconsLeft, mCoordinates.paperclipY, sDefaultPaint);
}
int faveTop = (mViewHeight - sFavoriteIconOff.getHeight()) / 2;
if (mMode == MODE_NARROW) {
faveTop += sSenderPaddingTopNarrow;
if (mHasInvite) {
iconsLeft -= sInviteIcon.getWidth();
canvas.drawBitmap(sInviteIcon, iconsLeft, mCoordinates.paperclipY, sDefaultPaint);
}
canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff, faveLeft, faveTop,
sDefaultPaint);
}
/**
@ -550,8 +396,8 @@ public class MessageListItem extends View {
public boolean onTouchEvent(MotionEvent event) {
boolean handled = false;
int touchX = (int) event.getX();
int checkRight = sCheckboxHitWidth;
int starLeft = mViewWidth - sFavoriteHitWidth;
int checkRight = mCoordinates.checkmarkWidthIncludingMargins;
int starLeft = mViewWidth - mCoordinates.starWidthIncludingMargins;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:

View File

@ -0,0 +1,311 @@
/*
* Copyright (C) 2011 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.email.activity;
import com.android.email.R;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.TextView;
/**
* Represents the coordinates of elements inside a CanvasConversationHeaderView
* (eg, checkmark, star, subject, sender, labels, 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.
*/
public class MessageListItemCoordinates {
// Modes.
public static final int WIDE_MODE = 0;
public static final int NORMAL_MODE = 1;
public static final int NARROW_MODE = 2;
// Static threshold.
private static int MINIMUM_WIDTH_WIDE_MODE = -1;
private static int MINIMUM_WIDTH_NORMAL_MODE = -1;
private static int[] CONVERSATION_HEIGHTS;
private static int[] SUBJECT_LENGTHS;
// Checkmark.
int checkmarkX;
int checkmarkY;
int checkmarkWidthIncludingMargins;
int checkmarkHeightIncludingMargins;
// Star.
int starX;
int starY;
int starWidthIncludingMargins;
int starHeightIncludingMargins;
// Senders.
int sendersX;
int sendersY;
int sendersWidth;
int sendersLineCount;
int sendersLineHeight;
int sendersFontSize;
int sendersAscent;
// Subject.
int subjectX;
int subjectY;
int subjectWidth;
int subjectLineCount;
int subjectLineHeight;
int subjectFontSize;
int subjectAscent;
// Color chip.
int chipX;
int chipY;
int chipWidth;
int chipHeight;
// Date.
int dateXEnd;
int dateY;
int dateFontSize;
int dateAscent;
// Paperclip.
int paperclipY;
// Cache to save Coordinates based on view width.
private static SparseArray<MessageListItemCoordinates> mCache =
new SparseArray<MessageListItemCoordinates>();
private static TextPaint sPaint = new TextPaint();
static {
sPaint.setTypeface(Typeface.DEFAULT);
sPaint.setAntiAlias(true);
}
// Not directly instantiable.
private MessageListItemCoordinates() {}
/**
* Returns the mode of the header view (Wide/Normal/Narrow) given the its
* measured width.
*/
public static int getMode(Context context, int width) {
Resources res = context.getResources();
if (MINIMUM_WIDTH_NORMAL_MODE <= 0) {
MINIMUM_WIDTH_WIDE_MODE = res.getDimensionPixelSize(R.dimen.minimum_width_wide_mode);
MINIMUM_WIDTH_NORMAL_MODE = res
.getDimensionPixelSize(R.dimen.minimum_width_normal_mode);
}
// Choose the correct mode based on view width.
int mode = NARROW_MODE;
if (width > MINIMUM_WIDTH_NORMAL_MODE) {
mode = NORMAL_MODE;
}
if (width > MINIMUM_WIDTH_WIDE_MODE) {
mode = WIDE_MODE;
}
return mode;
}
/**
* Returns the layout id to be inflated in this mode.
*/
private static int getLayoutId(int mode) {
switch (mode) {
case WIDE_MODE:
return R.layout.message_list_item_wide;
case NORMAL_MODE:
return R.layout.message_list_item_normal;
case NARROW_MODE:
return R.layout.message_list_item_narrow;
default:
throw new IllegalArgumentException("Unknown conversation header view mode " + mode);
}
}
/**
* Returns a value array multiplied by the specified density.
*/
public static int[] getDensityDependentArray(int[] values, float density) {
int result[] = new int[values.length];
for (int i = 0; i < values.length; ++i) {
result[i] = (int) (values[i] * density);
}
return result;
}
/**
* Returns the height of the view in this mode.
*/
public static int getHeight(Context context, int mode) {
Resources res = context.getResources();
float density = res.getDisplayMetrics().scaledDensity;
if (CONVERSATION_HEIGHTS == null) {
CONVERSATION_HEIGHTS = getDensityDependentArray(
res.getIntArray(R.array.conversation_heights), density);
}
return CONVERSATION_HEIGHTS[mode];
}
/**
* 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 width of a view.
*
* @param includeMargins whether or not to include margins when calculating
* width.
*/
public static int getWidth(View view, boolean includeMargins) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
return view.getWidth() + (includeMargins ? params.leftMargin + params.rightMargin : 0);
}
/**
* Returns the height of a view.
*
* @param includeMargins whether or not to include margins when calculating
* height.
*/
public static int getHeight(View view, boolean includeMargins) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
return view.getHeight() + (includeMargins ? params.topMargin + params.bottomMargin : 0);
}
/**
* Returns the number of lines of this text view.
*/
private static int getLineCount(TextView textView) {
return textView.getHeight() / textView.getLineHeight();
}
/**
* Returns the length (maximum of characters) of subject in this mode.
*/
public static int getSubjectLength(Context context, int mode) {
Resources res = context.getResources();
if (SUBJECT_LENGTHS == null) {
SUBJECT_LENGTHS = res.getIntArray(R.array.subject_lengths);
}
return SUBJECT_LENGTHS[mode];
}
/**
* Returns coordinates for elements inside a conversation header view given
* the view width.
*/
public static MessageListItemCoordinates forWidth(Context context, int width) {
MessageListItemCoordinates coordinates = mCache.get(width);
if (coordinates == null) {
coordinates = new MessageListItemCoordinates();
mCache.put(width, coordinates);
// TODO: make the field computation done inside of the constructor and mark fields final
// Layout the appropriate view.
int mode = getMode(context, width);
int height = getHeight(context, mode);
View view = LayoutInflater.from(context).inflate(getLayoutId(mode), null);
int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
view.layout(0, 0, width, height);
// Records coordinates.
View checkmark = view.findViewById(R.id.checkmark);
coordinates.checkmarkX = getX(checkmark);
coordinates.checkmarkY = getY(checkmark);
coordinates.checkmarkWidthIncludingMargins = getWidth(checkmark, true);
View star = view.findViewById(R.id.star);
coordinates.starX = getX(star);
coordinates.starY = getY(star);
coordinates.starWidthIncludingMargins = getWidth(star, true);
TextView senders = (TextView) view.findViewById(R.id.senders);
coordinates.sendersX = getX(senders);
coordinates.sendersY = getY(senders);
coordinates.sendersWidth = getWidth(senders, false);
coordinates.sendersLineCount = getLineCount(senders);
coordinates.sendersLineHeight = senders.getLineHeight();
coordinates.sendersFontSize = (int) senders.getTextSize();
sPaint.setTextSize(coordinates.sendersFontSize);
coordinates.sendersAscent = (int) sPaint.ascent();
TextView subject = (TextView) view.findViewById(R.id.subject);
coordinates.subjectX = getX(subject);
coordinates.subjectY = getY(subject);
coordinates.subjectWidth = getWidth(subject, false);
coordinates.subjectLineCount = getLineCount(subject);
coordinates.subjectLineHeight = subject.getLineHeight();
coordinates.subjectFontSize = (int) subject.getTextSize();
sPaint.setTextSize(coordinates.subjectFontSize);
coordinates.subjectAscent = (int) sPaint.ascent();
View chip = view.findViewById(R.id.color_chip);
coordinates.chipX = getX(chip);
coordinates.chipY = getY(chip);
coordinates.chipWidth = getWidth(chip, false);
coordinates.chipHeight = getHeight(chip, false);
TextView date = (TextView) view.findViewById(R.id.date);
coordinates.dateXEnd = getX(date) + date.getWidth();
coordinates.dateY = getY(date);
coordinates.dateFontSize = (int) date.getTextSize();
sPaint.setTextSize(coordinates.dateFontSize);
coordinates.dateAscent = (int) sPaint.ascent();
// The x-value is computed relative to the date.
View paperclip = view.findViewById(R.id.paperclip);
coordinates.paperclipY = getY(paperclip);
}
return coordinates;
}
}