Ticker.java revision e6f7f2e3a01b8deb00e03ccfa93751c315f14ef0
1/* 2 * Copyright (C) 2008 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 17package com.android.systemui.statusbar.phone; 18 19import android.service.notification.StatusBarNotification; 20import android.content.Context; 21import android.content.res.Resources; 22import android.graphics.drawable.Drawable; 23import android.os.Handler; 24import android.text.StaticLayout; 25import android.text.Layout.Alignment; 26import android.text.TextPaint; 27import android.view.View; 28import android.view.animation.AnimationUtils; 29import android.widget.TextSwitcher; 30import android.widget.TextView; 31import android.widget.ImageSwitcher; 32 33import java.util.ArrayList; 34 35import com.android.internal.statusbar.StatusBarIcon; 36import com.android.internal.util.CharSequences; 37 38import com.android.systemui.R; 39import com.android.systemui.statusbar.StatusBarIconView; 40 41public abstract class Ticker { 42 private static final int TICKER_SEGMENT_DELAY = 3000; 43 44 private Context mContext; 45 private Handler mHandler = new Handler(); 46 private ArrayList<Segment> mSegments = new ArrayList(); 47 private TextPaint mPaint; 48 private View mTickerView; 49 private ImageSwitcher mIconSwitcher; 50 private TextSwitcher mTextSwitcher; 51 private float mIconScale; 52 53 public static boolean isGraphicOrEmoji(char c) { 54 int gc = Character.getType(c); 55 return gc != Character.CONTROL 56 && gc != Character.FORMAT 57 && gc != Character.UNASSIGNED 58 && gc != Character.LINE_SEPARATOR 59 && gc != Character.PARAGRAPH_SEPARATOR 60 && gc != Character.SPACE_SEPARATOR; 61 } 62 63 private final class Segment { 64 StatusBarNotification notification; 65 Drawable icon; 66 CharSequence text; 67 int current; 68 int next; 69 boolean first; 70 71 StaticLayout getLayout(CharSequence substr) { 72 int w = mTextSwitcher.getWidth() - mTextSwitcher.getPaddingLeft() 73 - mTextSwitcher.getPaddingRight(); 74 return new StaticLayout(substr, mPaint, w, Alignment.ALIGN_NORMAL, 1, 0, true); 75 } 76 77 CharSequence rtrim(CharSequence substr, int start, int end) { 78 while (end > start && !isGraphicOrEmoji(substr.charAt(end-1))) { 79 end--; 80 } 81 if (end > start) { 82 return substr.subSequence(start, end); 83 } 84 return null; 85 } 86 87 /** returns null if there is no more text */ 88 CharSequence getText() { 89 if (this.current > this.text.length()) { 90 return null; 91 } 92 CharSequence substr = this.text.subSequence(this.current, this.text.length()); 93 StaticLayout l = getLayout(substr); 94 int lineCount = l.getLineCount(); 95 if (lineCount > 0) { 96 int start = l.getLineStart(0); 97 int end = l.getLineEnd(0); 98 this.next = this.current + end; 99 return rtrim(substr, start, end); 100 } else { 101 throw new RuntimeException("lineCount=" + lineCount + " current=" + current + 102 " text=" + text); 103 } 104 } 105 106 /** returns null if there is no more text */ 107 CharSequence advance() { 108 this.first = false; 109 int index = this.next; 110 final int len = this.text.length(); 111 while (index < len && !isGraphicOrEmoji(this.text.charAt(index))) { 112 index++; 113 } 114 if (index >= len) { 115 return null; 116 } 117 118 CharSequence substr = this.text.subSequence(index, this.text.length()); 119 StaticLayout l = getLayout(substr); 120 final int lineCount = l.getLineCount(); 121 int i; 122 for (i=0; i<lineCount; i++) { 123 int start = l.getLineStart(i); 124 int end = l.getLineEnd(i); 125 if (i == lineCount-1) { 126 this.next = len; 127 } else { 128 this.next = index + l.getLineStart(i+1); 129 } 130 CharSequence result = rtrim(substr, start, end); 131 if (result != null) { 132 this.current = index + start; 133 return result; 134 } 135 } 136 this.current = len; 137 return null; 138 } 139 140 Segment(StatusBarNotification n, Drawable icon, CharSequence text) { 141 this.notification = n; 142 this.icon = icon; 143 this.text = text; 144 int index = 0; 145 final int len = text.length(); 146 while (index < len && !isGraphicOrEmoji(text.charAt(index))) { 147 index++; 148 } 149 this.current = index; 150 this.next = index; 151 this.first = true; 152 } 153 }; 154 155 public Ticker(Context context, View sb) { 156 mContext = context; 157 final Resources res = context.getResources(); 158 final int outerBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_size); 159 final int imageBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size); 160 mIconScale = (float)imageBounds / (float)outerBounds; 161 162 mTickerView = sb.findViewById(R.id.ticker); 163 164 mIconSwitcher = (ImageSwitcher)sb.findViewById(R.id.tickerIcon); 165 mIconSwitcher.setInAnimation( 166 AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in)); 167 mIconSwitcher.setOutAnimation( 168 AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out)); 169 mIconSwitcher.setScaleX(mIconScale); 170 mIconSwitcher.setScaleY(mIconScale); 171 172 mTextSwitcher = (TextSwitcher)sb.findViewById(R.id.tickerText); 173 mTextSwitcher.setInAnimation( 174 AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in)); 175 mTextSwitcher.setOutAnimation( 176 AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out)); 177 178 // Copy the paint style of one of the TextSwitchers children to use later for measuring 179 TextView text = (TextView)mTextSwitcher.getChildAt(0); 180 mPaint = text.getPaint(); 181 } 182 183 184 public void addEntry(StatusBarNotification n) { 185 int initialCount = mSegments.size(); 186 187 // If what's being displayed has the same text and icon, just drop it 188 // (which will let the current one finish, this happens when apps do 189 // a notification storm). 190 if (initialCount > 0) { 191 final Segment seg = mSegments.get(0); 192 if (n.getPkg().equals(seg.notification.getPkg()) 193 && n.getNotification().icon == seg.notification.getNotification().icon 194 && n.getNotification().iconLevel == seg.notification.getNotification().iconLevel 195 && CharSequences.equals(seg.notification.getNotification().tickerText, 196 n.getNotification().tickerText)) { 197 return; 198 } 199 } 200 201 final Drawable icon = StatusBarIconView.getIcon(mContext, 202 new StatusBarIcon(n.getPkg(), n.getUser(), n.getNotification().icon, n.getNotification().iconLevel, 0, 203 n.getNotification().tickerText)); 204 final CharSequence text = n.getNotification().tickerText; 205 final Segment newSegment = new Segment(n, icon, text); 206 207 // If there's already a notification schedule for this package and id, remove it. 208 for (int i=0; i<mSegments.size(); i++) { 209 Segment seg = mSegments.get(i); 210 if (n.getId() == seg.notification.getId() && n.getPkg().equals(seg.notification.getPkg())) { 211 // just update that one to use this new data instead 212 mSegments.remove(i--); // restart iteration here 213 } 214 } 215 216 mSegments.add(newSegment); 217 218 if (initialCount == 0 && mSegments.size() > 0) { 219 Segment seg = mSegments.get(0); 220 seg.first = false; 221 222 mIconSwitcher.setAnimateFirstView(false); 223 mIconSwitcher.reset(); 224 mIconSwitcher.setImageDrawable(seg.icon); 225 226 mTextSwitcher.setAnimateFirstView(false); 227 mTextSwitcher.reset(); 228 mTextSwitcher.setText(seg.getText()); 229 230 tickerStarting(); 231 scheduleAdvance(); 232 } 233 } 234 235 public void removeEntry(StatusBarNotification n) { 236 for (int i=mSegments.size()-1; i>=0; i--) { 237 Segment seg = mSegments.get(i); 238 if (n.getId() == seg.notification.getId() && n.getPkg().equals(seg.notification.getPkg())) { 239 mSegments.remove(i); 240 } 241 } 242 } 243 244 public void halt() { 245 mHandler.removeCallbacks(mAdvanceTicker); 246 mSegments.clear(); 247 tickerHalting(); 248 } 249 250 public void reflowText() { 251 if (mSegments.size() > 0) { 252 Segment seg = mSegments.get(0); 253 CharSequence text = seg.getText(); 254 mTextSwitcher.setCurrentText(text); 255 } 256 } 257 258 private Runnable mAdvanceTicker = new Runnable() { 259 public void run() { 260 while (mSegments.size() > 0) { 261 Segment seg = mSegments.get(0); 262 263 if (seg.first) { 264 // this makes the icon slide in for the first one for a given 265 // notification even if there are two notifications with the 266 // same icon in a row 267 mIconSwitcher.setImageDrawable(seg.icon); 268 } 269 CharSequence text = seg.advance(); 270 if (text == null) { 271 mSegments.remove(0); 272 continue; 273 } 274 mTextSwitcher.setText(text); 275 276 scheduleAdvance(); 277 break; 278 } 279 if (mSegments.size() == 0) { 280 tickerDone(); 281 } 282 } 283 }; 284 285 private void scheduleAdvance() { 286 mHandler.postDelayed(mAdvanceTicker, TICKER_SEGMENT_DELAY); 287 } 288 289 public abstract void tickerStarting(); 290 public abstract void tickerDone(); 291 public abstract void tickerHalting(); 292} 293 294