Add a mini-player

This will open in response to clicks on the notification. It provides
a quick way to change a song or pause music. (Mainly it just looks legit)
This commit is contained in:
Christopher Eby 2010-02-21 00:08:47 -06:00
parent ddb0193d65
commit d1be1d80cc
12 changed files with 429 additions and 161 deletions

View File

@ -8,12 +8,18 @@
android:name="Tumult">
<activity
android:name="NowPlayingActivity"
android:label="@string/app_name" android:theme="@style/Theme.NoBackground">
android:theme="@style/NoBackground"
android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="RemoteActivity"
android:theme="@android:style/Theme.Dialog"
android:excludeFromRecents="true"
android:launchMode="singleInstance" />
<service android:name="PlaybackService" />
<activity android:name="PreferencesActivity" />
<activity android:name="SongSelector"/>

View File

@ -36,27 +36,6 @@
android:layout_gravity="bottom|center_horizontal"
android:background="#a000"
android:orientation="horizontal">
<ImageButton
android:id="@+id/previous"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="15px"
android:background="@null"
android:src="@drawable/previous" />
<ImageButton
android:id="@+id/play_pause"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="25px"
android:background="@null"
android:src="@drawable/play" />
<ImageButton
android:id="@+id/next"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="25px"
android:layout_marginRight="15px"
android:background="@null"
android:src="@drawable/next" />
<include layout="@layout/playback_buttons" />
</LinearLayout>
</merge>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageButton
android:id="@+id/previous"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="15px"
android:background="@null"
android:src="@drawable/previous" />
<ImageButton
android:id="@+id/play_pause"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="25px"
android:background="@null"
android:src="@drawable/play" />
<ImageButton
android:id="@+id/next"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="25px"
android:layout_marginRight="15px"
android:background="@null"
android:src="@drawable/next" />
</merge>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<org.kreed.tumult.RemoteLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
<org.kreed.tumult.CoverView
android:id="@+id/cover_view"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
<LinearLayout
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
<include layout="@layout/playback_buttons" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
<Button
android:id="@+id/open_button"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Open Player" />
<Button
android:id="@+id/kill_button"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Kill Service" />
</LinearLayout>
</org.kreed.tumult.RemoteLayout>

View File

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Tumult</string>
<string name="headset_only_summary_on">Audio only plays when a headset is plugged in</string>
<string name="headset_only_title">Headset only</string>
<string name="headset_only_summary_on">Audio only plays when a headset is plugged in</string>
<string name="headset_only_summary_off">Audio may play without a headset plugged in</string>
<string name="remote_player_title">Use Remote Player</string>
<string name="remote_player_summary_on">Clicking the notification will open a mini-player dialog</string>
<string name="remote_player_summary_off">Clicking the notification will open a the full player activity</string>
</resources>

View File

@ -1,5 +1,6 @@
<resources>
<style name="Theme.NoBackground" parent="android:Theme.NoTitleBar">
<style name="NoBackground" parent="android:style/Theme">
<item name="android:windowBackground">@null</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>

View File

@ -8,4 +8,10 @@
android:defaultValue="false"
android:summaryOn="@string/headset_only_summary_on"
android:summaryOff="@string/headset_only_summary_off" />
<CheckBoxPreference
android:key="remote_player"
android:title="@string/remote_player_title"
android:defaultValue="true"
android:summaryOn="@string/remote_player_summary_on"
android:summaryOff="@string/remote_player_summary_off" />
</PreferenceScreen>

View File

@ -9,7 +9,11 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
@ -17,26 +21,20 @@ import android.widget.Scroller;
public class CoverView extends View {
private static final int SNAP_VELOCITY = 1000;
private static final int STORE_SIZE = 3;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private float mLastMotionX;
private float mStartX;
private float mStartY;
private CoverViewWatcher mListener;
private IPlaybackService mService;
Song[] mSongs = new Song[3];
private Bitmap[] mBitmaps = new Bitmap[3];
private int mTentativeCover = -1;
public interface CoverViewWatcher {
public void next();
public void previous();
public void clicked();
}
public CoverView(Context context, AttributeSet attributes)
{
super(context, attributes);
@ -44,14 +42,14 @@ public class CoverView extends View {
mScroller = new Scroller(context);
}
public void setWatcher(CoverViewWatcher listener)
public void setPlaybackService(IPlaybackService service)
{
mListener = listener;
}
public Song getActiveSong()
{
return mSongs[1];
try {
mService = service;
mService.registerWatcher(mWatcher);
refreshSongs();
} catch (RemoteException e) {
}
}
private void drawText(Canvas canvas, String text, float left, float top, float width, float maxWidth, Paint paint)
@ -159,18 +157,15 @@ public class CoverView extends View {
if (oldBitmap != null)
oldBitmap.recycle();
}
public void setSongs(Song[] songs)
{
mSongs = songs;
regenerateBitmaps();
}
public void setSong(int delta, Song song)
private void refreshSongs()
{
int i = 1 + delta;
mSongs[i] = song;
createBitmap(i);
try {
mSongs = mService.getCurrentSongs();
regenerateBitmaps();
} catch (RemoteException e) {
Log.e("Tumult", "RemoteException", e);
}
}
public void regenerateBitmaps()
@ -178,27 +173,58 @@ public class CoverView extends View {
if (getWidth() == 0 || getHeight() == 0)
return;
for (int i = mSongs.length; --i != -1; )
for (int i = STORE_SIZE; --i != -1; )
createBitmap(i);
reset();
}
public void shiftBackward()
public void nextCover()
{
System.arraycopy(mSongs, 1, mSongs, 0, mSongs.length - 1);
System.arraycopy(mBitmaps, 1, mBitmaps, 0, mBitmaps.length - 1);
mSongs[mSongs.length - 1] = null;
mBitmaps[mBitmaps.length - 1] = null;
reset();
if (mService == null)
return;
try {
mService.nextSong();
System.arraycopy(mSongs, 1, mSongs, 0, STORE_SIZE - 1);
System.arraycopy(mBitmaps, 1, mBitmaps, 0, STORE_SIZE - 1);
mSongs[STORE_SIZE - 1] = null;
mBitmaps[STORE_SIZE - 1] = null;
reset();
mHandler.sendMessage(mHandler.obtainMessage(QUERY_SONG, 2, 0));
} catch (RemoteException e) {
}
}
public void shiftForward()
public void previousCover()
{
System.arraycopy(mSongs, 0, mSongs, 1, mSongs.length - 1);
System.arraycopy(mBitmaps, 0, mBitmaps, 1, mBitmaps.length - 1);
mSongs[0] = null;
mBitmaps[0] = null;
reset();
if (mService == null)
return;
try {
mService.previousSong();
System.arraycopy(mSongs, 0, mSongs, 1, STORE_SIZE - 1);
System.arraycopy(mBitmaps, 0, mBitmaps, 1, STORE_SIZE - 1);
mSongs[0] = null;
mBitmaps[0] = null;
reset();
mHandler.sendMessage(mHandler.obtainMessage(QUERY_SONG, 0, 0));
} catch (RemoteException e) {
}
}
public void togglePlayback()
{
if (mService == null)
return;
try {
mService.togglePlayback();
} catch (RemoteException e) {
}
}
public void reset()
@ -227,7 +253,7 @@ public class CoverView extends View {
canvas.drawColor(Color.BLACK);
for (int x = 0, i = 0; i != mBitmaps.length; ++i, x += width) {
for (int x = 0, i = 0; i != STORE_SIZE; ++i, x += width) {
if (mBitmaps[i] != null && clip.intersects(x, 0, x + width, height)) {
int xOffset = (width - mBitmaps[i].getWidth()) / 2;
int yOffset = (height - mBitmaps[i].getHeight()) / 2;
@ -274,7 +300,7 @@ public class CoverView extends View {
break;
case MotionEvent.ACTION_UP:
if (Math.abs(mStartX - x) + Math.abs(mStartY - ev.getY()) < 10) {
mListener.clicked();
performClick();
} else {
VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);
@ -314,13 +340,46 @@ public class CoverView extends View {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
} else if (mTentativeCover != -1) {
if (mListener != null) {
if (mTentativeCover == 2)
mListener.next();
else if (mTentativeCover == 0)
mListener.previous();
}
if (mTentativeCover == 2)
nextCover();
else if (mTentativeCover == 0)
previousCover();
mTentativeCover = -1;
}
}
}
private static final int REFRESH_SONGS = 0;
private static final int QUERY_SONG = 1;
private Handler mHandler = new Handler() {
public void handleMessage(Message message) {
switch (message.what) {
case REFRESH_SONGS:
refreshSongs();
break;
case QUERY_SONG:
try {
int i = message.arg1;
int delta = i - STORE_SIZE / 2;
mSongs[i] = mService.getSong(delta);
createBitmap(i);
} catch (RemoteException e) {
}
break;
}
}
};
private IMusicPlayerWatcher mWatcher = new IMusicPlayerWatcher.Stub() {
public void songChanged(Song playingSong)
{
if (!playingSong.equals(mSongs[STORE_SIZE / 2]))
mHandler.sendEmptyMessage(REFRESH_SONGS);
}
public void stateChanged(int oldState, int newState)
{
}
};
}

View File

@ -115,9 +115,10 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
private PlaybackService mService;
private RemoteCallbackList<IMusicPlayerWatcher> mWatchers;
private boolean mHeadsetOnly = true;
private boolean mHeadsetOnly;
private boolean mUseRemotePlayer;
private Handler mHandler;
private MediaPlayer mMediaPlayer;
private Random mRandom;
@ -149,6 +150,7 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
private static final int HANDLE_PLAY = 7;
private static final int HANDLE_PAUSE = 8;
private static final int RETRIEVE_SONGS = 9;
private static final int REMOTE_PLAYER_PREF_CHANGED = 10;
private static final int ITEM_SONG = 0;
private static final int ITEM_RESET = 1;
@ -220,6 +222,11 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
case RETRIEVE_SONGS:
retrieveSongs();
break;
case REMOTE_PLAYER_PREF_CHANGED:
mUseRemotePlayer = message.arg1 == 1;
if (mState == STATE_PLAYING)
mService.startForegroundCompat(NOTIFICATION_ID, createNotfication());
break;
}
}
};
@ -237,11 +244,12 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
mMediaPlayer.setWakeMode(mService, PowerManager.PARTIAL_WAKE_LOCK);
mMediaPlayer.setOnCompletionListener(this);
retrieveSongs();
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mService);
mHeadsetOnly = settings.getBoolean("headset_only", false);
mUseRemotePlayer = settings.getBoolean("remote_player", true);
settings.registerOnSharedPreferenceChangeListener(this);
setCurrentSong(1);
Looper.loop();
@ -302,7 +310,7 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
notification.contentView = views;
notification.icon = R.drawable.status_icon;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
Intent intent = new Intent(mService, NowPlayingActivity.class);
Intent intent = new Intent(mService, mUseRemotePlayer ? RemoteActivity.class : NowPlayingActivity.class);
notification.contentIntent = PendingIntent.getActivity(mService, 0, intent, 0);
return notification;
@ -416,9 +424,15 @@ public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener,
public void onSharedPreferenceChanged(SharedPreferences settings, String key)
{
if ("headset_only".equals(key) && mHandler != null) {
if (mHandler == null)
return;
if ("headset_only".equals(key)) {
int arg = settings.getBoolean(key, false) ? 1 : 0;
mHandler.sendMessage(mHandler.obtainMessage(HEADSET_PREF_CHANGED, arg, 0));
} else if ("remote_player".equals(key)) {
int arg = settings.getBoolean(key, true) ? 1 : 0;
mHandler.sendMessage(mHandler.obtainMessage(REMOTE_PLAYER_PREF_CHANGED, arg, 0));
}
}
}

View File

@ -1,7 +1,5 @@
package org.kreed.tumult;
import org.kreed.tumult.CoverView.CoverViewWatcher;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
@ -25,7 +23,7 @@ import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
public class NowPlayingActivity extends Activity implements CoverViewWatcher, ServiceConnection, View.OnClickListener, SeekBar.OnSeekBarChangeListener, View.OnFocusChangeListener {
public class NowPlayingActivity extends Activity implements ServiceConnection, View.OnClickListener, SeekBar.OnSeekBarChangeListener, View.OnFocusChangeListener {
private IPlaybackService mService;
private ViewGroup mLayout;
@ -55,8 +53,8 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
setContentView(R.layout.nowplaying);
mCoverView = (CoverView)findViewById(R.id.cover_view);
mCoverView.setWatcher(this);
mCoverView.setOnClickListener(this);
mLayout = (ViewGroup)mCoverView.getParent();
mControlsTop = findViewById(R.id.controls_top);
@ -139,21 +137,12 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
unbindService(this);
}
private void refreshSongs()
{
try {
mCoverView.setSongs(mService.getCurrentSongs());
} catch (RemoteException e) {
Log.e("Tumult", "RemoteException", e);
}
}
public void onServiceConnected(ComponentName name, IBinder service)
{
mService = IPlaybackService.Stub.asInterface(service);
try {
mService.registerWatcher(mWatcher);
refreshSongs();
mCoverView.setPlaybackService(mService);
setState(mService.getState());
mDuration = mService.getDuration();
} catch (RemoteException e) {
@ -175,15 +164,6 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
mHandler.sendEmptyMessage(UPDATE_PROGRESS);
} catch (RemoteException e) {
}
if (!playingSong.equals(mCoverView.getActiveSong())) {
runOnUiThread(new Runnable() {
public void run()
{
refreshSongs();
}
});
}
}
public void stateChanged(final int oldState, final int newState)
@ -197,34 +177,6 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
}
};
public void next()
{
try {
mService.nextSong();
mCoverView.shiftBackward();
mHandler.sendMessage(mHandler.obtainMessage(QUERY_SONG, 1, 0));
} catch (RemoteException e) {
}
}
public void previous()
{
try {
mService.previousSong();
mCoverView.shiftForward();
mHandler.sendMessage(mHandler.obtainMessage(QUERY_SONG, -1, 0));
} catch (RemoteException e) {
}
}
private void togglePlayback()
{
try {
mService.togglePlayback();
} catch (RemoteException e) {
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
@ -261,30 +213,13 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
clicked();
onClick(mCoverView);
return true;
}
return false;
}
public void clicked()
{
if (mControlsTop.getVisibility() == View.VISIBLE) {
mControlsTop.setVisibility(View.GONE);
if (mState == MusicPlayer.STATE_PLAYING)
mControlsBottom.setVisibility(View.GONE);
} else {
mControlsTop.setVisibility(View.VISIBLE);
mControlsBottom.setVisibility(View.VISIBLE);
mPlayPauseButton.requestFocus();
updateProgress();
sendHideMessage();
}
}
private String stringForTime(int ms)
{
int seconds = ms / 1000;
@ -331,18 +266,30 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
{
sendHideMessage();
if (view == mNextButton) {
next();
if (view == mCoverView) {
if (mControlsTop.getVisibility() == View.VISIBLE) {
mControlsTop.setVisibility(View.GONE);
if (mState == MusicPlayer.STATE_PLAYING)
mControlsBottom.setVisibility(View.GONE);
} else {
mControlsTop.setVisibility(View.VISIBLE);
mControlsBottom.setVisibility(View.VISIBLE);
mPlayPauseButton.requestFocus();
updateProgress();
}
} else if (view == mNextButton) {
mCoverView.nextCover();
} else if (view == mPreviousButton) {
previous();
mCoverView.previousCover();
} else if (view == mPlayPauseButton) {
togglePlayback();
mCoverView.togglePlayback();
}
}
private static final int HIDE = 0;
private static final int UPDATE_PROGRESS = 1;
private static final int QUERY_SONG = 2;
private Handler mHandler = new Handler() {
public void handleMessage(Message message) {
@ -355,12 +302,6 @@ public class NowPlayingActivity extends Activity implements CoverViewWatcher, Se
case UPDATE_PROGRESS:
updateProgress();
break;
case QUERY_SONG:
try {
int delta = message.arg1;
mCoverView.setSong(delta, mService.getSong(delta));
} catch (RemoteException e) {
}
}
}
};

View File

@ -0,0 +1,125 @@
package org.kreed.tumult;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.View;
import android.view.Window;
import android.widget.ImageButton;
public class RemoteActivity extends Activity implements ServiceConnection, View.OnClickListener {
private CoverView mCoverView;
private View mOpenButton;
private View mKillButton;
private View mPreviousButton;
private ImageButton mPlayPauseButton;
private View mNextButton;
@Override
public void onCreate(Bundle state)
{
super.onCreate(state);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.remote_dialog);
mCoverView = (CoverView)findViewById(R.id.cover_view);
mOpenButton = findViewById(R.id.open_button);
mOpenButton.setOnClickListener(this);
mKillButton = findViewById(R.id.kill_button);
mKillButton.setOnClickListener(this);
mPreviousButton = findViewById(R.id.previous);
mPreviousButton.setOnClickListener(this);
mPlayPauseButton = (ImageButton)findViewById(R.id.play_pause);
mPlayPauseButton.setOnClickListener(this);
mNextButton = findViewById(R.id.next);
mNextButton.setOnClickListener(this);
}
@Override
public void onResume()
{
super.onResume();
reconnect();
}
private void reconnect()
{
Intent intent = new Intent(this, PlaybackService.class);
startService(intent);
bindService(intent, this, Context.BIND_AUTO_CREATE);
}
@Override
public void onPause()
{
super.onPause();
unbindService(this);
}
public void onServiceConnected(ComponentName name, IBinder binder)
{
IPlaybackService service = IPlaybackService.Stub.asInterface(binder);
mCoverView.setPlaybackService(service);
try {
service.registerWatcher(mWatcher);
setState(service.getState());
} catch (RemoteException e) {
}
}
public void onServiceDisconnected(ComponentName name)
{
reconnect();
}
public void onClick(View view)
{
if (view == mKillButton) {
stopService(new Intent(this, PlaybackService.class));
finish();
} else if (view == mOpenButton) {
startActivity(new Intent(this, NowPlayingActivity.class));
finish();
} else if (view == mNextButton) {
mCoverView.nextCover();
} else if (view == mPreviousButton) {
mCoverView.previousCover();
} else if (view == mPlayPauseButton) {
mCoverView.togglePlayback();
}
}
private void setState(int state)
{
if (state == MusicPlayer.STATE_NO_MEDIA)
finish();
mPlayPauseButton.setImageResource(state == MusicPlayer.STATE_PLAYING ? R.drawable.pause : R.drawable.play);
}
private IMusicPlayerWatcher mWatcher = new IMusicPlayerWatcher.Stub() {
public void songChanged(Song playingSong)
{
}
public void stateChanged(final int oldState, final int newState)
{
runOnUiThread(new Runnable() {
public void run()
{
setState(newState);
}
});
}
};
}

View File

@ -0,0 +1,76 @@
package org.kreed.tumult;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/*
* RemoteLayout acts like a very simple vertical LinearLayout with special
* case: all CoverViews placed will be made square at all costs.
*/
public class RemoteLayout extends ViewGroup {
private int mCoverSize;
public RemoteLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
int measuredHeight = 0;
int measuredWidth = 0;
View coverView = null;
for (int i = getChildCount(); --i != -1; ) {
View view = getChildAt(i);
if (view instanceof CoverView) {
coverView = view;
} else {
int spec = MeasureSpec.makeMeasureSpec(maxHeight - measuredHeight, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, spec);
measuredHeight += view.getMeasuredHeight();
if (view.getMeasuredWidth() > measuredWidth)
measuredWidth = view.getMeasuredWidth();
}
}
if (coverView != null) {
if (measuredHeight + measuredWidth > maxHeight) {
mCoverSize = maxHeight - measuredHeight;
measuredHeight = maxHeight;
} else {
mCoverSize = measuredWidth;
measuredHeight += measuredWidth;
}
}
setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4)
{
int layoutWidth = getMeasuredWidth();
int top = 0;
for (int i = 0, end = getChildCount(); i != end; ++i) {
View view = getChildAt(i);
if (view instanceof CoverView) {
view.layout(0, top, layoutWidth, top + mCoverSize);
top += mCoverSize;
} else {
int height = view.getMeasuredHeight();
int width = view.getMeasuredWidth();
int left = (layoutWidth - width) / 2;
view.layout(left, top, layoutWidth - left, top + height);
top += height;
}
}
}
}