/* * Copyright 2013 wada811<at.wada811@gmail.com> * * 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 at.wada811.widget; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.PixelFormat; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.TextView; import at.wada811.android.library.R; import at.wada811.utils.LogUtils; public class Toaster { public static final String TAG = Toaster.class.getSimpleName(); private ToasterService mToasterService; private ToasterServiceConnection mConnection = new ToasterServiceConnection(); private Context mContext; private class ToasterServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service){ LogUtils.d(); mToasterService = ((ToasterService.ToasterServiceBinder)service).getService(); } @Override public void onServiceDisconnected(ComponentName name){ LogUtils.d(); mToasterService = null; } }; public Toaster(Context context) { LogUtils.d(); mContext = context; if(mToasterService == null){ LogUtils.d(); boolean bindService = context.bindService(new Intent(context, ToasterService.class), mConnection, Context.BIND_AUTO_CREATE); LogUtils.d("bindService: " + bindService); } } public void unplug(){ LogUtils.d(); if(mConnection != null){ LogUtils.d(); mToasterService.cancelAllBread(); mContext.unbindService(mConnection); mToasterService = null; mConnection = null; } } public void cancelAll(){ if(mToasterService != null){ LogUtils.d(); mToasterService.cancelAllBread(); } } public ToastBread newToast(){ return new ToastBread(mContext); } public class ToastBread { private Bread mBread; private int mDuration; private View mNextView; private OnDismissListener mDismissListener; private ToastBread(Context context) { mBread = new Bread(); mBread.mY = context.getResources().getDimensionPixelSize(R.dimen.toasterBaselineHeight); } /** * Show the view for the specified duration. */ public void show(){ if(mNextView == null){ throw new RuntimeException("setView must have been called"); } Bread bread = mBread; bread.mNextView = mNextView; if(mToasterService != null){ mToasterService.enqueueBread(bread, mDuration); } } /** * Close the view if it's showing, or don't show it if it isn't showing yet. * You do not normally have to call this. Normally view will disappear on its own * after the appropriate duration. */ public void cancel(){ mBread.hide(); if(mToasterService != null){ mToasterService.cancelBread(mBread); } } /** * Set the view to show. * * @see #getView */ public void setView(View view){ mNextView = view; } /** * Return the view. * * @see #setView */ public View getView(){ return mNextView; } /** * Set how long to show the view for. * * @see #LENGTH_SHORT * @see #LENGTH_LONG */ public void setDuration(int duration){ mDuration = duration; } /** * Return the duration. * * @see #setDuration */ public int getDuration(){ return mDuration; } /** * Set the margins of the view. * * @param horizontalMargin The horizontal margin, in percentage of the * container width, between the container's edges and the * notification * @param verticalMargin The vertical margin, in percentage of the * container height, between the container's edges and the * notification */ public void setMargin(float horizontalMargin, float verticalMargin){ mBread.mHorizontalMargin = horizontalMargin; mBread.mVerticalMargin = verticalMargin; } /** * Return the horizontal margin. */ public float getHorizontalMargin(){ return mBread.mHorizontalMargin; } /** * Return the vertical margin. */ public float getVerticalMargin(){ return mBread.mVerticalMargin; } /** * Set the location at which the notification should appear on the screen. * * @see android.view.Gravity * @see #getGravity */ public void setGravity(int gravity, int xOffset, int yOffset){ mBread.mGravity = gravity; mBread.mX = xOffset; mBread.mY = yOffset; } /** * Get the location at which the notification should appear on the screen. * * @see android.view.Gravity * @see #getGravity */ public int getGravity(){ return mBread.mGravity; } /** * Return the X offset in pixels to apply to the gravity's location. */ public int getXOffset(){ return mBread.mX; } /** * Return the Y offset in pixels to apply to the gravity's location. */ public int getYOffset(){ return mBread.mY; } /** * Make a standard toast that just contains a text view. * * @param context The context to use. Usually your {@link android.app.Application} or * {@link android.app.Activity} object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * */ public ToastBread makeText(Context context, CharSequence text, int duration){ LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflate.inflate(R.layout.toast, null); TextView tv = (TextView)v.findViewById(R.id.message); tv.setText(text); mNextView = v; mDuration = duration; return this; } /** * Make a standard toast that just contains a text view with the text from a resource. * * @param context The context to use. Usually your {@link android.app.Application} or * {@link android.app.Activity} object. * @param resId The resource id of the string resource to use. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * * @throws Resources.NotFoundException if the resource can't be found. */ public ToastBread makeText(Context context, int resId, int duration) throws Resources.NotFoundException{ return makeText(context, context.getResources().getText(resId), duration); } /** * Update the text in a Toast that was previously created using one of the makeText() * methods. * * @param resId The new text for the Toast. */ public void setText(int resId){ setText(mContext.getText(resId)); } /** * Update the text in a Toast that was previously created using one of the makeText() * methods. * * @param s The new text for the Toast. */ public void setText(CharSequence s){ if(mNextView == null){ throw new RuntimeException("This Toast was not created with Toast.makeText()"); } TextView tv = (TextView)mNextView.findViewById(R.id.message); if(tv == null){ throw new RuntimeException("This Toast was not created with Toast.makeText()"); } tv.setText(s); } public void setAnimationStyle(int animationStyle){ mBread.mAnimationStyle = animationStyle; } public void setOnDismissListener(OnDismissListener onDismissListener){ mDismissListener = onDismissListener; } public class Bread implements ToasterCallback { public Bread() { // XXX This should be changed to use a Dialog, with a Theme. // Toast defined that sets up the layout params appropriately. final WindowManager.LayoutParams params = mParams; params.height = android.view.ViewGroup.LayoutParams.WRAP_CONTENT; params.width = android.view.ViewGroup.LayoutParams.WRAP_CONTENT; params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = android.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); } final Runnable mShow = new Runnable(){ @Override public void run(){ handleShow(); } }; final Runnable mHide = new Runnable(){ @Override public void run(){ handleHide(); // Don't do this in handleHide() because it is also invoked by handleShow() mNextView = null; } }; private final LayoutParams mParams = new WindowManager.LayoutParams(); final Handler mHandler = new Handler(); int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; int mX, mY; float mHorizontalMargin; float mVerticalMargin; int mAnimationStyle = android.R.style.Animation_Toast; View mView; View mNextView; WindowManager mWindowManager; @Override public void show(){ LogUtils.d(); mHandler.post(mShow); } @Override public void hide(){ LogUtils.d(); mHandler.post(mHide); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public void handleShow(){ if(mView != mNextView){ // remove the old view if necessary handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); if(context == null){ context = mView.getContext(); } mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); // We can resolve the Gravity here by using the Locale for getting // the layout direction final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL){ mParams.horizontalWeight = 1.0f; } if((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL){ mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.windowAnimations = mAnimationStyle; if(mView.getParent() != null){ mWindowManager.removeView(mView); } mWindowManager.addView(mView, mParams); trySendAccessibilityEvent(); } } private void trySendAccessibilityEvent(){ AccessibilityManager accessibilityManager = (AccessibilityManager)mView.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if(!accessibilityManager.isEnabled()){ return; } // treat toasts as notifications since they are used to // announce a transient piece of information to the user AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); event.setClassName(getClass().getName()); event.setPackageName(mView.getContext().getPackageName()); mView.dispatchPopulateAccessibilityEvent(event); accessibilityManager.sendAccessibilityEvent(event); } public void handleHide(){ if(mView != null){ // note: checking parent() just to make sure the view has // been added... i have seen cases where we get here when // the view isn't yet added, so let's try not to crash. if(mView.getParent() != null){ mWindowManager.removeView(mView); } if(mDismissListener != null){ mDismissListener.onDismiss(); mDismissListener = null; } mView = null; } } } } public static interface OnDismissListener { public void onDismiss(); } protected static interface ToasterCallback { public void show(); public void hide(); } }