效果
特点
1.可多次显示
2.可滑动删除,滑动时改变透明度
3.可快速滑动删除。
4.自动显示与消失动画
使用
很简单模仿的Toast的api,不过没有处理时间参数,默认显示2秒钟, 以后再逐步完善吧。
@Override
public void onClick(View v) {
SlideToast.MakeText(this, "第" + n + "次show").show();
n++;
}
}
实现
一.动画效果
动画效果主要是通过模仿系统的Toast,在添加到WindowsManager时设置WindowManager.LayoutParams的windowAnimations属性。如下
mParams.windowAnimations = R.style.anim_view;//设置进入退出动画效果
style文件
<style name="anim_view">
<item name="@android:windowEnterAnimation">@anim/anim_in</item>
<item name="@android:windowExitAnimation">@anim/anim_out</item>
</style>
进入动画
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0"
android:fromYDelta="-205"
android:toXDelta="0"
android:toYDelta="0"
android:duration="350"
android:fillAfter="true"
/>
<alpha
android:fromAlpha="0"
android:toAlpha="1"
android:duration="100"
/>
</set>
消失动画
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1"
android:toAlpha="0"
android:duration="800"/>
</set>
二,从屏幕顶端显示
认真观察可以发现,我的这个toast是从屏幕顶端显示的,及从状态栏开始显示。主要是通过设置WindowManager.LayoutParams.gravity和设置WindowManager.LayoutParams.y来实现的,将显示的view向上偏移状态栏高度即可。
mParams.gravity = Gravity.LEFT | Gravity.TOP;
//在Toast高度上加上状态栏的高度
mParams.height = dip2px(context, mToastHight) + sStateBarHight;
//将Toast从屏幕顶端显示
mParams.y = -sStateBarHight;
获取状态栏高度:
/**
* 获取状态栏高度
* @param context
* @return
*/
private static int getStateBarHight(Context context) {
int statebarHight = 0;
if (context instanceof Activity) {
Activity a = (Activity) context;
Rect frame = new Rect();
a.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
statebarHight = frame.top;
}
//如果第一种方法获取失败就使用第二种
if (statebarHight == 0) {
Class<?> c = null;
Object obj = null;
Field field = null;
int x = 0, sbar = 38;//默认为38,貌似大部分是这样的
try {
c = Class.forName("com.android.internal.R$dimen");
obj = c.newInstance();
field = c.getField("status_bar_height");
x = Integer.parseInt(field.get(obj).toString());
statebarHight = context.getResources().getDimensionPixelSize(x);
} catch (Exception e1) {
e1.printStackTrace();
}
}
return statebarHight;
}
在此补充一点,我原本想把Toast显示在状态栏之上,发现不行。
这是通知栏的type,由注释可知,只能有一个通知栏,我原来设置过一次结果,权限被拒绝崩溃了。
/**
* Window type: the status bar. There can be only one status bar
* window; it is placed at the top of the screen, and all other
* windows are shifted down so they are below it.
* In multiuser systems shows on all users' windows.
*/
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
后来不死心使用了这个type
mParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
/**
* Window type: system window, such as low power alert. These windows
* are always on top of application windows.
* In multiuser systems shows only on the owning user's window.
*/
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;
不过要使用这个type需要声明权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
还必须在手机中赋予显示悬浮窗的权限,这是我的魅族手机的,一般应用默认拒绝悬浮窗权限的
心想这样总可以了吧,结果并没有什么乱用。
不死心我又Google了怎么显示在状态栏上,结果找到这些
有个家伙说设置成2003就可以显示在状态栏之上
可是TYPE_SYSTEM_ALERT本来就是2003。
因为它的声明是这样的
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;
而FIRST_SYSTEM_WINDOW的声明是这样的
/**
* Start of system-specific window types. These are not normally
* created by applications.
*/
public static final int FIRST_SYSTEM_WINDOW = 2000;
所以2000+3=2003,而且现在不支持直接设置数字了,不信你看。
因为type被注解限制了值的范围,这方面就不深入讨论了。综上所述,目前我还没有找到显示在状态栏的方法。所以我还是设置成
mParams.type = WindowManager.LayoutParams.TYPE_TOAST;
效果是一样的,还不用声明权限。
三.实现可滑动
这方面的资料比较多,其主要思想是,在按下的时候记录按下的位置,在滑动的时候,计算滑动的偏移量,并通过调用 WindowManager的updateViewLayout方法改变View的横纵坐标;
首先添加onTouch监听
//加载自己的布局
mToastView = View.inflate(context, R.layout.layout_toast, null);
mTextView = (TextView) mToastView.findViewById(R.id.tv_info);
mTextView.setText(text);
//设置touch监听
mToastView.setOnTouchListener(this);
原来想过通过自定义view重写onTouch,后来发现view中没有WindowManger,而且以后Toast的布局可能改变,所以只能透过这种方式了。
重写onTouch
//按下的位置
int xdown = 0;
//记录按下的时间,用于计算速度
long downTime=0;
//记录第一次按下的x坐标
int mFistDown=0;
@Override
public boolean onTouch(View v, MotionEvent event) {
//如果正在动画直接return
if ((restoreAnimator != null && restoreAnimator.isRunning()||(dismissAnimator!=null&&dismissAnimator.isRunning()))) {
return true;
}
//记录下点击的x点
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xdown = (int) event.getRawX();
mFistDown=xdown;
downTime=System.currentTimeMillis();
break;
case MotionEvent.ACTION_MOVE:
//计算滑动距离
int moveX = (int) (event.getRawX() - xdown);
//更新位置
update(mParams.x+moveX, -sStateBarHight);
//取消自动消失
mHandler.removeMessages(MSG_HIDE);
//更新按下的位置
xdown = (int) event.getRawX();
break;
case MotionEvent.ACTION_UP:
//计算速度
long usetime=System.currentTimeMillis()-downTime;
int speeed= (int) ((xdown-mFistDown)*1000/usetime);
//根据位置设置相应的动画
autoDismissOrRestore(speeed);
break;
}
return true;
}
更新位置
private boolean update(int x, int y) {
if (mIsShow) {
mParams.x = x;
mParams.y = y;
mWdm.updateViewLayout(mToastView, mParams);
//修改透明度
float alpha= (float) (1.0-0.5*Math.abs(mParams.x-0)*2.0/mScreenWidth);
mToastView.setAlpha(alpha);
return true;
}
return false;
}
注意:在onTouch中获取x坐标使用的是event.getRawX()而不是event.getX(),以为getX()获取的是相对于父容器的坐标,而getRawX()是相对于屏幕的坐标,我原来用的就是getX()结果出现跳动现象。
实现自动关闭或复位
主要是通过view的x坐标判断偏移量,思路是ACTION_UP时判断速度如果速度小再判断左右滑动的距离如果小于屏幕一半就复位,否则消失。如果速度大就判断应该显示的方向。
private void autoDismissOrRestore(int speeed) {
//判断是关闭还是还原
boolean needDismis = true;
boolean leftToDismiss=true;
//在低速情况下
if(Math.abs(speeed)<1500) {
//如果X坐标大于屏幕的-1/2并且小于1/2。还原。并添加关闭事件
if (mParams.x >= -(mScreenWidth / 2) && mParams.x <= (mScreenWidth / 2)) {
needDismis = false;
} else {
//默认是从左边消失,如果x坐标大于屏幕的1/2则从右边消失
if (mParams.x > mScreenWidth / 2) {
leftToDismiss = false;
}
}
}else{
//当速度大于消失速度时,自动消失。判断消失的方向。
if(speeed>0){
leftToDismiss=false;
}
}
//根据状态创建动画。
if (!needDismis) {
createRestoreAnimator();
restoreAnimator.start();
}else{
creatDismissAnimator(leftToDismiss);
dismissAnimator.start();
}
}
其实关于如何实现滑动删除原来想过通过ViewDragHelper什么的,但是发现ViewDragHelper解决的是父容器中的View拖拽处理,最终调用的也是scrollTo或者scrollBy,而此处明显不一样,因为是添加到WindowManger的,没有父容器。所以思来想去只能用属性动画中的值动画。
private void createRestoreAnimator() {
if (restoreAnimator == null) {
restoreAnimator = new ObjectAnimator().ofInt(mParams.x, 0);
//根据移动距离的百分比设置相应的时长,如果时间一样长的话,在移动距离很短的时候会变的很慢。
int duration= (int) (250*Math.abs(mParams.x - 0)*2.0/mScreenWidth);
restoreAnimator.setDuration(duration);
restoreAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
update(value, -sStateBarHight);
}
});
restoreAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mHandler.sendEmptyMessageDelayed(MSG_HIDE, 2000);
restoreAnimator = null;
}
});
}
}
private void creatDismissAnimator(boolean left){
if (dismissAnimator == null) {
//根据消失的方向设置目标值
dismissAnimator = new ObjectAnimator().ofInt(mParams.x, left?-mScreenWidth:mScreenWidth);
dismissAnimator.setDuration(200);
dismissAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
update(value, -sStateBarHight);
}
});
dismissAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//立即消失
mHandler.sendEmptyMessage(MSG_HIDE);
dismissAnimator=null;
}
});
}
}