image.png

无需一切权限,不受各种国产ROM限制,默认可以显示的应用内悬浮窗。

应用内显示,无需申请任何权限

应用内显示,所有机型都可以默认显示悬浮窗,无需引导用户做更多设置

支持拖拽

超出屏幕限制移动

可自动吸附到屏幕边缘

可向左右边缘隐藏一半

核心类共三个:

CFloatingManager.java

import android.app.Activity;

import android.os.Handler;

import android.os.Looper;

import android.support.v4.view.ViewCompat;

import android.view.Gravity;

import android.view.ViewGroup;

import android.widget.FrameLayout;

import android.widget.RelativeLayout;

import com.imuxuan.floatingview.utils.EnContext;

/**

* @ClassName CFloatingManager

* @Description 悬浮窗管理器, 建造者模式

*/

public class CFloatingManager {

public static FloatingImp build() {

return new FloatingImp();

}

public static class FloatingImp {

private Handler mHandler;

private CFloatingView mCFloatingView;

private FrameLayout mContainer;

int layoutId = 0; //布局id

ViewGroup.LayoutParams params; //布局初始参数(位置,大小等)

CFloatingView.MagnetViewListener magnetViewListener;//监听

CFloatingView.IFloatingViews iFloatingViews;//监听

boolean isMovable = true;//是否可移动

boolean isHideEdge = true;//是否隐藏边缘

public FloatingImp create() {

mHandler = new Handler(Looper.getMainLooper());

synchronized (this) {

if (mCFloatingView != null) {

return this;

}

mCFloatingView = new CFloatingView(EnContext.get().getApplicationContext());

mCFloatingView.setLayout(EnContext.get().getApplicationContext(), layoutId);

if (null != iFloatingViews) {

iFloatingViews.onInitViews(mCFloatingView);

}

mCFloatingView.setLayoutParams(null == params ? defaultParams() : params);

mCFloatingView.setIsMovable(isMovable);

mCFloatingView.setIsHideEdge(isHideEdge);

if (mCFloatingView != null) {

mCFloatingView.setMagnetViewListener(magnetViewListener);

}

addViewToWindow(mCFloatingView);

}

return this;

}

/**

* 设置布局控件

*/

public FloatingImp setLayout(int layoutId) {

this.layoutId = layoutId;

return this;

}

/**

* 设置布局初始参数

*/

public FloatingImp setLayoutParams(ViewGroup.LayoutParams params) {

this.params = params;

return this;

}

/**

* 监听事件

*/

public FloatingImp setListener(CFloatingView.MagnetViewListener magnetViewListener) {

this.magnetViewListener = magnetViewListener;

return this;

}

public FloatingImp setIsMovable(boolean isMovable) {

this.isMovable = isMovable;

return this;

}

public FloatingImp setIsHideEdge(boolean isHideEdge) {

this.isHideEdge = isHideEdge;

return this;

}

public FloatingImp setInitViews(CFloatingView.IFloatingViews iFloatingViews) {

this.iFloatingViews = iFloatingViews;

return this;

}

public FloatingImp remove() {

new Handler(Looper.getMainLooper()).post(new Runnable() {

@Override

public void run() {

if (mCFloatingView == null) {

return;

}

if (ViewCompat.isAttachedToWindow(mCFloatingView) && mContainer != null) {

mContainer.removeView(mCFloatingView);

}

mCFloatingView = null;

}

});

return this;

}

/**

* 将view绑定到activity的布局中

*/

public FloatingImp attach(Activity activity) {

attach(getActivityRoot(activity));

return this;

}

/**

* 将view绑定到布局中

*/

public FloatingImp attach(FrameLayout container) {

if (container == null || mCFloatingView == null) {

mContainer = container;

return this;

}

if (mCFloatingView.getParent() == container) {

return this;

}

if (mContainer != null && mCFloatingView.getParent() == mContainer) {

mContainer.removeView(mCFloatingView);

}

mContainer = container;

container.addView(mCFloatingView);

mCFloatingView.onAttach();

return this;

}

/**

* 将view从activity的布局中解绑

*/

public FloatingImp detach(Activity activity) {

detach(getActivityRoot(activity));

return this;

}

/**

* 将view从布局中解绑

*/

public FloatingImp detach(FrameLayout container) {

if (mCFloatingView != null && container != null && ViewCompat.isAttachedToWindow(mCFloatingView)) {

container.removeView(mCFloatingView);

}

if (mContainer == container) {

mContainer = null;

}

mCFloatingView.onDetach();

return this;

}

/**

* 将view添加到当前窗口中

*/

private void addViewToWindow(final CFloatingView view) {

if (mContainer == null) {

return;

}

mContainer.addView(view);

}

/**

* 默認Params,用于设置最开始位置

*/

private FrameLayout.LayoutParams defaultParams() {

FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(

RelativeLayout.LayoutParams.WRAP_CONTENT,

RelativeLayout.LayoutParams.WRAP_CONTENT);

params.gravity = Gravity.BOTTOM | Gravity.START;

params.setMargins(13, params.topMargin, params.rightMargin, 56);

return params;

}

/**

* 获取activity所绑定的布局

*/

private FrameLayout getActivityRoot(Activity activity) {

if (activity == null) {

return null;

}

try {

return (FrameLayout) activity.getWindow().getDecorView().findViewById(android.R.id.content);

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

}

}

CFloatingView.java

import android.content.Context;

import android.support.annotation.NonNull;

import android.view.MotionEvent;

import android.view.View;

/**

* @ClassName CFloatingView

* @Description 悬浮窗,继承磁力吸附悬浮窗,封装点击事件

*/

public class CFloatingView extends FloatingMagnetView {

private MagnetViewListener mMagnetViewListener;

private long mLastTouchDownTime;//用于判断点击

private static final int TOUCH_TIME_THRESHOLD = 150;

public View view;

/**

* 悬浮窗点击接口

*/

public interface MagnetViewListener {

void onRemove(CFloatingView cFloatingView);

void onClick(CFloatingView cFloatingView);

void onEndAppear(CFloatingView cFloatingView);

void onEndHide(CFloatingView cFloatingView);

}

public interface IFloatingViews {

void onInitViews(CFloatingView cFloatingView);

}

public CFloatingView(@NonNull Context context) {

super(context, null);

}

public View setLayout(@NonNull Context context, int en_floating_view_id) {

return view = inflate(context, en_floating_view_id, this);

}

public void setMagnetViewListener(MagnetViewListener magnetViewListener) {

this.mMagnetViewListener = magnetViewListener;

}

@Override

public boolean onTouchEvent(MotionEvent event) {

super.onTouchEvent(event);

if (event != null) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

mLastTouchDownTime = System.currentTimeMillis();

clearAnimation();

break;

case MotionEvent.ACTION_UP:

if (isOnClickEvent()) {

dealClickEvent();

}

break;

}

}

return true;

}

protected void dealClickEvent() {

if (mMagnetViewListener != null) {

mMagnetViewListener.onClick(this);

}

}

public void onRemove() {

if (mMagnetViewListener != null) {

mMagnetViewListener.onRemove(this);

}

}

protected boolean isOnClickEvent() {

return System.currentTimeMillis() - mLastTouchDownTime < TOUCH_TIME_THRESHOLD;

}

/**

* 隐藏,点击又出现之后,由子类进行具体实现

*/

@Override

protected void onEndAppear() {

super.onEndAppear();

if (mMagnetViewListener != null) {

mMagnetViewListener.onEndAppear(this);

}

}

/**

* 隐藏半边之后

*/

@Override

protected void onEndHide() {

super.onEndHide();

if (mMagnetViewListener != null) {

mMagnetViewListener.onEndHide(this);

}

}

@Override

public void onAttach() {

super.onAttach();

handler.removeCallbacksAndMessages(null);

handler.postDelayed(new Runnable() {

@Override

public void run() {

hideEdgeAnima();

}

}, 2000);

}

}

FloatingMagnetView.java

import android.content.Context;

import android.os.Handler;

import android.os.Looper;

import android.os.Message;

import android.util.AttributeSet;

import android.view.MotionEvent;

import android.view.animation.Animation;

import android.view.animation.AnimationSet;

import android.view.animation.TranslateAnimation;

import android.widget.FrameLayout;

import com.imuxuan.floatingview.utils.SystemUtils;

/**

* @ClassName FloatingMagnetView

* @Description 磁力吸附悬浮窗

* @Author Yunpeng Li

* @Creation 2018/3/15 下午5:02

* @Mender Yunpeng Li

* @Modification 2018/3/15 下午5:02

*/

public class FloatingMagnetView extends FrameLayout {

public static final int MARGIN_EDGE = 13;

private float mOriginalRawX;

private float mOriginalRawY;

private float mOriginalX;

private float mOriginalY;

protected MoveAnimator mMoveAnimator;

protected int mScreenWidth;

private int mScreenHeight;

private int mStatusBarHeight;

boolean isMovable = true;//是否可移动

public FloatingMagnetView(Context context) {

this(context, null);

}

public FloatingMagnetView(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

public FloatingMagnetView(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

init();

}

private void init() {

mMoveAnimator = new MoveAnimator();

mStatusBarHeight = SystemUtils.getStatusBarHeight(getContext());

setClickable(true);

updateSize();

}

/**

* 设置是否可移动

*/

public void setIsMovable(boolean isMovable) {

this.isMovable = isMovable;

}

@Override

public boolean onTouchEvent(MotionEvent event) {

if (event == null) {

return false;

}

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

changeOriginalTouchParams(event);

updateSize();

mMoveAnimator.stop();

removeRunable();

onEndAppear();

break;

case MotionEvent.ACTION_MOVE:

if (isMovable) {

updateViewPosition(event);

}

break;

case MotionEvent.ACTION_UP:

moveToEdge();

break;

}

return true;

}

/**

* 更新view的坐标位置

*/

private void updateViewPosition(MotionEvent event) {

setX(mOriginalX + event.getRawX() - mOriginalRawX);

// 限制不可超出屏幕高度

float desY = mOriginalY + event.getRawY() - mOriginalRawY;

if (desY < 0) {

desY = 0;

}

if (desY > mScreenHeight - getHeight() * 2) {

desY = mScreenHeight - getHeight() * 2;

}

setY(desY);

}

private void changeOriginalTouchParams(MotionEvent event) {

mOriginalX = getX();

mOriginalY = getY();

mOriginalRawX = event.getRawX();

mOriginalRawY = event.getRawY();

}

protected void updateSize() {

mScreenWidth = (SystemUtils.getScreenWidth(getContext()) - this.getWidth());

mScreenHeight = SystemUtils.getScreenHeight(getContext());

}

public void moveToEdge() {

float moveDistance = isNearestLeft() ? MARGIN_EDGE : mScreenWidth - MARGIN_EDGE;

mMoveAnimator.start(moveDistance, getY());

}

protected boolean isNearestLeft() {

int middle = mScreenWidth / 2;

return getX() < middle;

}

protected class MoveAnimator implements Runnable {

private Handler handler = new Handler(Looper.getMainLooper());

private float destinationX;

private float destinationY;

private long startingTime;

void start(float x, float y) {

this.destinationX = x;

this.destinationY = y;

startingTime = System.currentTimeMillis();

handler.post(this);

}

@Override

public void run() {

if (getRootView() == null || getRootView().getParent() == null) {

return;

}

float progress = Math.min(1, (System.currentTimeMillis() - startingTime) / 400f);

float deltaX = (destinationX - getX()) * progress;

float deltaY = (destinationY - getY()) * progress;

move(deltaX, deltaY);

if (progress < 1) {

handler.post(this);

} else {

hideEdgeAnima();//开始隐藏

}

}

private void stop() {

handler.removeCallbacks(this);

}

}

private void move(float deltaX, float deltaY) {

setX(getX() + deltaX);

setY(getY() + deltaY);

}

public void onAttach() {

}

public void onDetach() {

}

//关于靠边隐藏==============================================================================================

boolean isHideEdge = true;//是否隐藏边缘

protected static final int HEDE_FLOAT_VIEW_TIME = 3000;//靠边隐藏时间

protected static final int MES_ANIMA_LEFT = 0;

protected static final int MES_ANIMA_RIGHT = 1;

protected static final int VIEW_GONE = 2;

protected static final int VIEW_Transparent = 3;

protected AnimationSet animationleft;

protected AnimationSet animationright;

/**

* 设置是否隐藏边缘

*/

public void setIsHideEdge(boolean isHideEdge) {

this.isHideEdge = isHideEdge;

}

/**

* 隐藏边缘动画

*/

protected void hideEdgeAnima() {

if (isHideEdge) {

if (isNearestLeft()) {

handler.postDelayed(myRunnableLeft, HEDE_FLOAT_VIEW_TIME);

} else {

handler.postDelayed(myRunnableRigth, HEDE_FLOAT_VIEW_TIME);

}

}

}

private void removeRunable() {

handler.removeCallbacksAndMessages(null);

}

protected Runnable runnableViewGone = new Runnable() {

public void run() {

Message message = handler.obtainMessage();

message.what = VIEW_GONE;

handler.sendMessage(message);

}

};

protected Runnable runnableTransparent = new Runnable() {

@Override

public void run() {

Message message = handler.obtainMessage();

message.what = VIEW_Transparent;

handler.sendMessage(message);

}

};

protected Runnable myRunnableLeft = new Runnable() {

public void run() {

Message message = handler.obtainMessage();

message.what = MES_ANIMA_LEFT;

handler.sendMessage(message);

}

};

protected Runnable myRunnableRigth = new Runnable() {

public void run() {

Message message = handler.obtainMessage();

message.what = MES_ANIMA_RIGHT;

handler.sendMessage(message);

}

};

protected Handler handler = new Handler(Looper.getMainLooper()) {

public void handleMessage(Message msg) {

switch (msg.what) {

case MES_ANIMA_LEFT:

if (null == animationleft) {

animationleft = new AnimationSet(true);

animationleft.setDuration(250);

animationleft.addAnimation(new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, -0.5f, 0, 0, 0, 0));

animationleft.setFillAfter(true);

}

startAnimation(animationleft);

animationleft.setAnimationListener(ainimaLeft);

break;

case MES_ANIMA_RIGHT:

if (null == animationright) {

animationright = new AnimationSet(true);

animationright.setDuration(250);

animationright.addAnimation(new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0.5f, 0, 0, 0, 0));

animationright.setFillAfter(true);

}

startAnimation(animationright);

animationright.setAnimationListener(ainimaRigth);

break;

// case VIEW_GONE:

//

// imgFloatView.setVisibility(View.GONE);

// Log.d(TAG, "handleMessage: bingo");

// break;

case VIEW_Transparent:

break;

default:

break;

}

}

};

protected Animation.AnimationListener ainimaLeft = new Animation.AnimationListener() {

@Override

public void onAnimationStart(Animation animation) {

}

@Override

public void onAnimationRepeat(Animation animation) {

}

@Override

public void onAnimationEnd(Animation animation) {

onEndHide();

}

};

protected Animation.AnimationListener ainimaRigth = new Animation.AnimationListener() {

@Override

public void onAnimationStart(Animation animation) {

}

@Override

public void onAnimationRepeat(Animation animation) {

}

@Override

public void onAnimationEnd(Animation animation) {

onEndHide();

}

};

/**

* 隐藏,点击又出现之后,由子类进行具体实现

*/

protected void onEndAppear() {

}

/**

* 隐藏半边之后,由子类进行具体实现

*/

protected void onEndHide() {

}

}

两个个辅助工具类:

EnContext.java

import android.app.Application;

/**

* Created by Yunpeng Li on 2018/11/8.

*/

public class EnContext {

private static final Application INSTANCE;

static {

Application app = null;

try {

app = (Application) Class.forName("android.app.AppGlobals").getMethod("getInitialApplication").invoke(null);

if (app == null)

throw new IllegalStateException("Static initialization of Applications must be on main thread.");

} catch (final Exception e) {

e.printStackTrace();

try {

app = (Application) Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null);

} catch (final Exception ex) {

e.printStackTrace();

}

} finally {

INSTANCE = app;

}

}

public static Application get() {

return INSTANCE;

}

}

SystemUtils.java

import android.content.Context;

/**

* Created by Yunpeng Li on 2018/3/15.

*/

public class SystemUtils {

public static int getStatusBarHeight(Context context) {

int result = 0;

int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");

if (resourceId > 0) {

result = context.getResources().getDimensionPixelSize(resourceId);

}

return result;

}

public static int getScreenWidth(Context context) {

int screenWith = -1;

try {

screenWith = context.getResources().getDisplayMetrics().widthPixels;

} catch (Exception e) {

e.printStackTrace();

}

return screenWith;

}

public static int getScreenHeight(Context context) {

int screenHeight = -1;

try {

screenHeight = context.getResources().getDisplayMetrics().heightPixels;

} catch (Exception e) {

e.printStackTrace();

}

return screenHeight;

}

}

应用例子:

在基础类的地方创建一个全局的CFloatingManager.FloatingImp floatingImp对象,目的是为了保持对象唯一,然后在各个子类中进行引用:

@Override

protected void onCreate(@Nullable Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

if (null == floatingImp) {

floatingImp = CFloatingManager.build()

.setLayout(R.layout.en_floating_view)//设置布局

.setInitViews(new CFloatingView.IFloatingViews() { // 可以在此处对布局内的子控件单独进行控制,todo:设置点击事件,则会影响到拖动事件

@Override

public void onInitViews(CFloatingView cFloatingView) {

cFloatingView.view.findViewById(R.id.icon).setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

Toast.makeText(TestActivity.this, "点击了icon", Toast.LENGTH_SHORT).show();

}

});

}

})

.setListener(new CFloatingView.MagnetViewListener() {//设置监听事件

@Override

public void onRemove(CFloatingView cFloatingView) {

Toast.makeText(TestActivity.this, "我没了", Toast.LENGTH_SHORT).show();

}

@Override

public void onClick(CFloatingView cFloatingView) {

Toast.makeText(TestActivity.this, "点到我了", Toast.LENGTH_SHORT).show();

}

@Override

public void onEndAppear(CFloatingView cFloatingView) {

((ImageView)cFloatingView.view.findViewById(R.id.icon)).setImageResource(R.drawable.jy_sdk_float_window_normal);

Toast.makeText(TestActivity.this, "又出现了", Toast.LENGTH_SHORT).show();

}

@Override

public void onEndHide(CFloatingView cFloatingView) {

((ImageView)cFloatingView.view.findViewById(R.id.icon)).setImageResource(R.drawable.jy_sdk_float_window_transparent);

Toast.makeText(TestActivity.this, "隐藏了一半", Toast.LENGTH_SHORT).show();

}

})

.setIsMovable(true)//控制是否可移动

.setIsHideEdge(true)//控制是否需要隐藏

.create();//创建实体

}

}

@Override

protected void onStart() {

super.onStart();

floatingImp.attach(this);//绑定实体至页面

}

@Override

protected void onStop() {

super.onStop();

floatingImp.detach(this);//移除实体离开页面

}

en_floating_view.xml

android:layout_width="wrap_content"

android:layout_height="wrap_content">

android:id="@+id/icon"

android:layout_width="50dp"

android:layout_height="50dp"

android:src="@drawable/imuxuan" />

android:id="@+id/icon2"

android:layout_width="50dp"

android:layout_height="50dp"

android:layout_toRightOf="@+id/icon"

android:src="@drawable/imuxuan" />

其中的小图标我就不上了,更换一下就行

android自动申请悬浮窗权限,Android 悬浮窗--无需权限相关推荐

  1. android 可编辑的表格框架,smartTable-一款android自动生成表格框架---A Android automatically generated table framework...

    一款android自动生成表格框架功能介绍 1. 快速配置自动生成表格: 2. 自动计算表格宽高: 3. 表格列标题组合: 4. 表格固定左序列.顶部序列.第一行.列标题.统计行: 5. 自动统计,排 ...

  2. android自动接听电话并回复,android自动接听电话各种异常处理

    public void autoAnswerPhone() { try { Log.i(TAG,"autoAnswerPhone"); ITelephony itelephony ...

  3. android 自动打包脚本,Jenkins实现Android自动化打包

    1.Tomcat 进入 https://tomcat.apache.org/ 官网,下载最新的 tomcat 安装包并且安装. 安装完成后,启动 tomcat 后,在浏览器中输入 http://loc ...

  4. android自动测试2:使用android studio实现设备循环自动重启

    一.前提: apk可以获得系统签名 二.适用: 需要对android设备进行循环重启测试 三.步骤: 1. AndroidManifest.xml中添加以下权限: <uses-permissio ...

  5. android 自动更新 服务端,搭建android版本更新服务器使用android系统自带的DownloadManager下载文件...

    这几天想自己做一个文件更新的功能,但是由于不知道怎样写服务端,所以一直没有去做,后来发现原来服务端编写简直是太简单了,所以今天就实现了 版本更新的这样一个功能. 一搭建版本更新服务器: 搭建这个一个服 ...

  6. android自动触发返回,ionic4处理android返回按钮事件

    前言 之前在这里介绍了ionic3如何处理android返回按钮 ionic4和ionic3关于android返回按钮的处理是不一样的,而且有点坑,所以本文介绍一下 效果演示 如下gif,所有返回操作 ...

  7. android自动屏幕点击事件,Android 中屏幕点击事件的实现

    在android下,事件的发生是在监听器下进行,android系统可以响应按键事件和触摸屏事件,事件说明如下: 常用实现OnClickListener,OnTouchListener,OnFocusC ...

  8. android 自动上下翻滚,如何让Android TextView自动向下滚动到最后?

    我有一个TextView,其内容从文本文件中复制.现在每次将文本文件的内容加载到TextView中时,我都希望它自动向下滚动到最后. 这是我的布局XML文件的部分内容: android:id=&quo ...

  9. android 自动打开第三方应用程序,Android如何做到应用程序图标隐藏,由第三方程序显示启动...

    Android如何做到应用程序图标隐藏,由第三方程序显示启动 发布时间:2020-07-13 03:25:02 来源:51CTO 阅读:11353 作者:ord1nary 在AndroidManife ...

最新文章

  1. 从零入门 Serverless | 一文详解 Serverless 技术选型
  2. php如何实现添加到购物车_PHP实现添加购物车功能
  3. 落在我手里,今天你能嫁出去算我输!
  4. .NET开源工作流CCFlow-快速入门
  5. UIPageViewController用法
  6. ssd raid0 linux 2018,2018-01-28 Linux学习之RAID与LVM硬盘阵列技术
  7. Camel In Action 读书笔记 (8)
  8. 理解与学习linux 文件系统的目录结构
  9. ATIchinapay银联支付模块.zip
  10. c3p0 服务启动获取连接超时_一次c3p0连接池连接异常错误的排查
  11. 【国产MCU移植】看看有没有你需要的,一起来查漏补缺吧!(附已报名的硬件)...
  12. 读取cpu温度的api_获取传感器温度-cpu 温度篇
  13. 单细胞分析实录(18): 基于CellPhoneDB的细胞通讯分析及可视化 (上篇)
  14. const的意义及作用
  15. 天呐!惊人的Springboot测试.Springboot测试类之@RunWith注解
  16. matlab和saber哪个好用,实例分析 saber与simulink谁更适合仿真
  17. 语音增强算法的概述[转]
  18. 软通22年秋季新员工入职考试
  19. against fate
  20. 阻塞/非阻塞 同步/异步

热门文章

  1. 一个计算机专业在读研究生的迷茫(转)
  2. P5.js创意编程之自我介绍
  3. 你为什么应该为软件付费
  4. AI插件开发-AIApplicationSuite模块-应用程序-AIActionManagerSuite模块-动作管理-illustrator插件
  5. 你可以提要求,别人也可以拒绝
  6. 苹果Mac电脑alt键在哪里
  7. 一个基于Java的syslog服务器,大家都来看看啊!
  8. 【mpeg】mpeg1、mpeg2与mpeg4码流结构区别分析
  9. 群发邮件怎么发?outlook发邮件如何撤回?
  10. (简易SSM框架搭建)物流查询系统