ConversationViewHeader.java revision 67aa9e5162a15fb8b46b4113ac627cd20668f095
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import android.content.Context;
21import android.content.res.Resources;
22import android.text.Spannable;
23import android.text.SpannableStringBuilder;
24import android.text.TextUtils;
25import android.text.style.BackgroundColorSpan;
26import android.text.style.ForegroundColorSpan;
27import android.util.AttributeSet;
28import android.view.View;
29import android.view.ViewGroup;
30import android.view.View.OnClickListener;
31import android.widget.RelativeLayout;
32import android.widget.TextView;
33
34import com.android.mail.R;
35import com.android.mail.browse.ConversationViewAdapter.ConversationHeaderItem;
36import com.android.mail.browse.FolderSpan.FolderSpanDimensions;
37import com.android.mail.providers.Conversation;
38import com.android.mail.providers.Folder;
39import com.android.mail.providers.Settings;
40import com.android.mail.ui.FolderDisplayer;
41import com.android.mail.utils.LogTag;
42import com.android.mail.utils.LogUtils;
43import com.android.mail.utils.Utils;
44
45/**
46 * A view for the subject and folders in the conversation view. This container
47 * makes an attempt to combine subject and folders on the same horizontal line if
48 * there is enough room to fit both without wrapping. If they overlap, it
49 * adjusts the layout to position the folders below the subject.
50 */
51public class ConversationViewHeader extends RelativeLayout implements OnClickListener {
52
53    public interface ConversationViewHeaderCallbacks {
54        /**
55         * Called in response to a click on the folders region.
56         */
57        void onFoldersClicked();
58
59        /**
60         * Called when the height of the {@link ConversationViewHeader} changes.
61         *
62         * @param newHeight the new height in px
63         */
64        void onConversationViewHeaderHeightChange(int newHeight);
65
66        /**
67         * Measure a subject string for display outside a conversation view and
68         * return the substring of trailing characters that didn't fit. Should
69         * not actually render the text, just measure it.
70         *
71         * @param subject string to measure
72         * @return the remainder of text that didn't fit
73         */
74        String getSubjectRemainder(String subject);
75    }
76
77    private static final String LOG_TAG = LogTag.getLogTag();
78    private TextView mSubjectView;
79    private FolderSpanTextView mFoldersView;
80    private ConversationViewHeaderCallbacks mCallbacks;
81    private ConversationAccountController mAccountController;
82    private ConversationFolderDisplayer mFolderDisplayer;
83    private ConversationHeaderItem mHeaderItem;
84
85    public ConversationViewHeader(Context context) {
86        this(context, null);
87    }
88
89    public ConversationViewHeader(Context context, AttributeSet attrs) {
90        super(context, attrs);
91    }
92
93    @Override
94    protected void onFinishInflate() {
95        super.onFinishInflate();
96
97        mSubjectView = (TextView) findViewById(R.id.subject);
98        mFoldersView = (FolderSpanTextView) findViewById(R.id.folders);
99
100        mFoldersView.setOnClickListener(this);
101        mFolderDisplayer = new ConversationFolderDisplayer(getContext(), mFoldersView);
102    }
103
104    @Override
105    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
106        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
107
108        // reposition the folders if they don't fit horizontally next to the
109        // subject
110        // (taking into account child margins and parent padding)
111        final int childWidthSum = getTotalMeasuredChildWidth(mSubjectView)
112                + getTotalMeasuredChildWidth(mFoldersView) + getPaddingLeft() + getPaddingRight();
113
114        if (childWidthSum > getMeasuredWidth()) {
115            LayoutParams params = (LayoutParams) mFoldersView.getLayoutParams();
116            params.addRule(RelativeLayout.BELOW, R.id.subject);
117            params.addRule(RelativeLayout.ALIGN_BASELINE, 0);
118            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
119        }
120    }
121
122    private static int getTotalMeasuredChildWidth(View child) {
123        final LayoutParams p = (LayoutParams) child.getLayoutParams();
124        return child.getMeasuredWidth() + p.leftMargin + p.rightMargin;
125    }
126
127    public void setCallbacks(ConversationViewHeaderCallbacks callbacks,
128            ConversationAccountController accountController) {
129        mCallbacks = callbacks;
130        mAccountController = accountController;
131    }
132
133    public void setSubject(final String subject) {
134        String subjectToShow = subject;
135        if (mCallbacks != null && mCallbacks.getSubjectRemainder(subject) == null) {
136            subjectToShow = null;
137        }
138        mSubjectView.setText(subjectToShow);
139
140        if (TextUtils.isEmpty(subjectToShow)) {
141            mSubjectView.setVisibility(GONE);
142        }
143    }
144
145    public void setFoldersVisible(boolean show) {
146        mFoldersView.setVisibility(show ? View.VISIBLE : View.GONE);
147    }
148
149    public void setFolders(Conversation conv) {
150        setFoldersVisible(true);
151        SpannableStringBuilder sb = new SpannableStringBuilder();
152        final Settings settings = mAccountController.getAccount().settings;
153        if (settings.priorityArrowsEnabled && conv.isImportant()) {
154            sb.append('.');
155            sb.setSpan(new PriorityIndicatorSpan(getContext(),
156                    R.drawable.ic_email_caret_none_important_unread, mFoldersView.getPadding(), 0),
157                    0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
158        }
159
160        mFolderDisplayer.loadConversationFolders(conv, null /* ignoreFolder */,
161                -1 /* ignoreFolderType */);
162        mFolderDisplayer.appendFolderSpans(sb);
163
164        mFoldersView.setText(sb);
165    }
166
167    public void bind(ConversationHeaderItem headerItem) {
168        mHeaderItem = headerItem;
169    }
170
171    private int measureHeight() {
172        ViewGroup parent = (ViewGroup) getParent();
173        if (parent == null) {
174            LogUtils.e(LOG_TAG, "Unable to measure height of conversation header");
175            return getHeight();
176        }
177        final int h = Utils.measureViewHeight(this, parent);
178        return h;
179    }
180
181    /**
182     * Update the conversation view header to reflect the updated conversation.
183     */
184    public void onConversationUpdated(Conversation conv) {
185        // The only things we have to worry about when the conversation changes
186        // in the conversation header are the folders and priority indicators.
187        // Updating these will resize the space for the header.
188        setFolders(conv);
189        if (mHeaderItem != null) {
190            final int h = measureHeight();
191            if (mHeaderItem.setHeight(h)) {
192                mCallbacks.onConversationViewHeaderHeightChange(h);
193            }
194        }
195    }
196
197    @Override
198    public void onClick(View v) {
199        if (R.id.folders == v.getId()) {
200            if (mCallbacks != null) {
201                mCallbacks.onFoldersClicked();
202            }
203        }
204    }
205
206    private static class ConversationFolderDisplayer extends FolderDisplayer {
207
208        private FolderSpanDimensions mDims;
209
210        public ConversationFolderDisplayer(Context context, FolderSpanDimensions dims) {
211            super(context);
212            mDims = dims;
213        }
214
215        public void appendFolderSpans(SpannableStringBuilder sb) {
216            for (final Folder f : mFoldersSortedSet) {
217                final int bgColor = Folder.getNonEmptyColor(f.bgColor, mDefaultBgColor);
218                final int fgColor = Folder.getNonEmptyColor(f.fgColor, mDefaultFgColor);
219                addSpan(sb, f.name, bgColor, fgColor);
220            }
221
222            if (mFoldersSortedSet.isEmpty()) {
223                final Resources r = mContext.getResources();
224                final String name = r.getString(R.string.add_label);
225                final int bgColor = r.getColor(R.color.conv_header_add_label_background);
226                final int fgColor = r.getColor(R.color.conv_header_add_label_text);
227                addSpan(sb, name, bgColor, fgColor);
228            }
229        }
230
231        private void addSpan(SpannableStringBuilder sb, String name, int bgColor,
232                             int fgColor) {
233            final int start = sb.length();
234            sb.append(name);
235            final int end = sb.length();
236
237            sb.setSpan(new BackgroundColorSpan(bgColor), start, end,
238                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
239            sb.setSpan(new ForegroundColorSpan(fgColor), start, end,
240                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
241            sb.setSpan(new FolderSpan(sb, mDims), start, end,
242                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
243        }
244
245    }
246}
247