先贴出最终的效果图:
看完效果,直接贴代码:
import { screen } from 'electron';
import ChildWindow from '../win/childWin';
import { NOTIFIER } from '../../config';
import { getOptions } from '../../utils/dialog';
import { easeInQuad, easeOutQuad } from '../common';
export type PopupNotify = {
title: string;
content: string;
};
// NOTIFIER 其实就是这个
export const NOTIFIER = {
width: 290,
height: 200,
url: '/notifier',
};
const offset = 8;
export default class Notifier {
isShown: boolean;
winInstance?: ChildWindow;
screenHeight: number = 0;
private timer: any = null;
constructor(args: PopupNotify) {
this.isShown = false;
this.createNotifier(args);
}
createNotifier(args: PopupNotify) {
let display = screen.getPrimaryDisplay().workAreaSize;
let WIDTH = display.width;
let HEIGHT = display.height; // 减去任务栏的高度
this.screenHeight = HEIGHT;
const winInstance = (this.winInstance = new ChildWindow({
width: NOTIFIER.width,
height: NOTIFIER.height,
x: WIDTH - NOTIFIER.width - offset,
y: HEIGHT,
frame: false,
resizable: false,
movable: false,
alwaysOnTop: true,
opacity: 0,
webPreferences: {
devTools: false,
},
}));
winInstance.bind({
readyToShow: () => {
this.show();
},
focus: this.focus,
blur: this.blur,
closed: this.closed,
});
const options = getOptions(NOTIFIER.url, {
...args,
min: false,
});
winInstance.loadFile(options, 'window');
return winInstance;
}
show() {
this.winInstance && this.winInstance.show();
this.animate(this.winClose);
}
/** 执行从底部划出动画 渐变y和opcity */
animate = (callback: Function, open: boolean = true) => {
let currentTime = 0;
this.timer = setInterval(() => {
currentTime += 10;
if (currentTime > 200) {
open && (this.isShown = true);
clearInterval(this.timer);
/** 开始执行销毁当前弹窗的方法 */
callback();
} else {
this.setBounds(Math.floor(
open ?
easeOutQuad(currentTime, this.screenHeight, NOTIFIER.height + offset, 200, !open)
: easeInQuad(currentTime, this.screenHeight - NOTIFIER.height - offset, NOTIFIER.height + offset, 200)));
this.setOpacity(Number(open ? easeInQuad(currentTime, 0, 1, 200, open).toFixed(2) : easeOutQuad(currentTime, 1, 0, 200, open).toFixed(2)));
}
}, 10);
};
setBounds = (y: number) => {
try {
this.winInstance && this.winInstance.win && !this.winInstance.win.isDestroyed() && this.winInstance.win.setBounds({ y });
} catch (error) {
console.log(error);
}
};
setOpacity = (opacity: number) => {
try {
this.winInstance && this.winInstance.win && !this.winInstance.win.isDestroyed() && this.winInstance.win.setOpacity(opacity);
} catch (error) {
console.log(error);
}
};
focus = () => {
/** 获取焦点之后 取消倒计时关闭当前窗口 但是由于窗口打开是默认获取焦点的 focus不会重复触发
* 所以这时候isShown为false 不会clearTimeout 除非先失去焦点 再获取焦点才会clearTimeout */
if (this.isShown && this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
};
blur = () => {
/** 开始倒计时关闭当前窗口 */
this.timer === null && this.winClose();
};
/** 窗口关闭触发的事件 */
closed = () => {
this.timer && clearTimeout(this.timer);
};
/** 倒计时2s之后关闭当前窗口 */
winClose = () => {
this.timer = setTimeout(() => {
this.animate(() => {
this.winInstance && this.winInstance.win && !this.winInstance.win.isDestroyed() && this.winInstance.win.close(); }, false);
}, 3000);
};
}
// common.ts 中的缓动函数的方法
/** linear */
export function linear(
currentTime: number,
startValue: number,
changeValue: number,
duration: number,
increase: boolean = true
) {
return increase ? (changeValue * currentTime) / duration + startValue
: startValue - (changeValue * currentTime) / duration;
}
/** ease-in */
export function easeInQuad(
currentTime: number,
startValue: number,
changeValue: number,
duration: number,
increase: boolean = true
) {
currentTime /= duration;
return increase ? changeValue * currentTime * currentTime + startValue
: startValue - changeValue * currentTime * currentTime;
}
/** ease-out */
export function easeOutQuad(
currentTime: number,
startValue: number,
changeValue: number,
duration: number,
increase: boolean = true
): number {
currentTime /= duration;
return increase ? -changeValue * currentTime * (currentTime - 2) + startValue
: startValue - -changeValue * currentTime * (currentTime - 2);
}
关于速度曲线可以查看这篇文章:缓动函数
// ChildWindow 其实就是对于new BrowserWindow 过程中的一个封装
import { BrowserWindow, BrowserWindowConstructorOptions, app } from 'electron';
import { LoadFileOption } from 'app/utils/dialog';
const merge = require('lodash/merge');
const path = require('path');
const os = require('os');
export interface WindowListener {
readyToShow?: () => void;
shown?: () => void;
finish?: () => void;
closed?: () => void;
focus?: () => void;
blur?: () => void;
}
export default class ChildWindow {
win: BrowserWindow | IAnyObject;
winOptions: BrowserWindowConstructorOptions;
constructor(args: BrowserWindowConstructorOptions) {
this.win = {};
this.winOptions = args;
this.createMainWindow();
}
createMainWindow = () => {
this.win = new BrowserWindow(
merge(
{
center: true,
// frame: false,
show: false,
resizable: false,
transparent: false,
webPreferences: {
nodeIntegration: true,
},
},
this.winOptions
)
);
if (os.platform() === 'win32') {
/** windows 下禁用右键 */
this.win.hookWindowMessage(278, (e: Event) => {
this.win.setEnabled(false);
setTimeout(() => this.win.setEnabled(true), 100);
return;
});
}
};
bind = (cb?: WindowListener) => {
if (!cb) return;
cb.readyToShow &&
this.win.once('ready-to-show', () => {
cb.readyToShow!();
});
cb.shown &&
this.win.once('show', () => {
cb.shown!();
});
cb.focus &&
this.win.on('focus', () => {
cb.focus!();
});
cb.blur &&
this.win.on('blur', () => {
cb.blur!();
});
cb.closed &&
this.win.once('closed', () => {
cb.closed!();
});
cb.finish &&
this.win.webContents.once('did-finish-load', () => {
cb.finish!();
});
};
loadURL = (url: string) => {
const userAgent = this.win.webContents.getUserAgent();
this.win.loadURL(url, { userAgent });
};
loadFile = (options: LoadFileOption, temp: string = 'index') => {
this.win.loadFile(
app.isPackaged
? path.resolve(__dirname, `pages/${temp}.html`)
: path.resolve(__dirname, `../../pages/${temp}.html`),
{
...options,
}
);
};
getWebContents = () => {
return this.win ? this.win.webContents : null;
};
getUrl = () => {
const webContents = this.win.webContents;
if (!webContents || webContents.isDestroyed() || webContents.isCrashed())
return '';
return webContents.getURL();
};
show = () => {
this.win.show();
};
focus = () => {
this.win.focus();
};
isVisible = () => {
return this.win.isVisible();
};
isDestroyed = () => {
return this.win.isDestroyed();
};
isMinimized = () => {
return this.win.isMinimized();
};
restore = () => {
this.win.restore();
};
mini = () => {
this.win.minimize();
};
hide = () => {
this.win.hide();
};
max = () => {
this.win.maximize();
};
reload = () => {
this.win.reload();
};
close = () => {
this.win.close();
};
}
实现的原理其实很简单,主要是利用了 electron browserWindow 的 setBounds 和 setOpacity 方法,然后通过 screen.getPrimaryDisplay().workAreaSize 获取到屏幕的宽高,默认先将窗口放在屏幕右下角,并向下偏移自身的高度,当弹窗触发readyToShow事件的时候,就开始执行过渡动画。
最后
实现的源码其实就在我的这个项目里面:electron_client
项目中本地数据的持久化使用的是 sqlite,然后使用 sequelize 数据库模型库。
自己也写了一个react ssr的脚手架工具:js-react-ssr-cli
已经实现项目 DOM、路由、store 和 css 同构,开发环境的热更新功能
支持选择 js 和 typescript,支持选择 MobX 和 Redux
默认的 css 预处理器为:stylus
欢迎使用哦。