自从android4.4开始,android手机状态栏再也不是一成黑的时代,之前叫做变色龙,miui6发布会把他叫做沉浸式,之后大家就自然而然的接受了沉浸式这个名称,其实实际应该叫做Translucent Bar,即为透明状态栏。
沉浸式实现原理其实是使整个activity布局延伸到整个屏幕,然后使状态栏变成透明色,有些手机会有导航栏,同样也可以把导航栏变成透明色,这样会使一些app更加美观。
先看两个概念
- 状态栏
状态栏.png
- 导航栏
导航栏.png
废话不多说了,直接看GIF

整体效果.gif
1、引入
github仓库地址:https://github.com/gyf-dev/ImmersionBar
dependencies {
compile 'com.gyf.barlibrary:barlibrary:2.2.1'
}
2、特性
1)基本介绍
- 基础用法,建议在BaseActivity里调用
public class BaseActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); ImmersionBar.with(this).init(); //初始化,默认透明状态栏和黑色导航栏 } @Override protected void onDestroy() { super.onDestroy(); ImmersionBar.with(this).destroy(); //不调用该方法,如果界面bar发生改变,在不关闭app的情况下,退出此界面再进入将记忆最后一次bar改变的状态 } }
- 高级用法,如果基础用法不能满足你的需求,可以试试这里的方法
ImmersionBar.with(this) .transparentStatusBar() //透明状态栏,不写默认透明色 .transparentNavigationBar() //透明导航栏,不写默认黑色(设置此方法,fullScreen()方法自动为true) .transparentBar() //透明状态栏和导航栏,不写默认状态栏为透明色,导航栏为黑色(设置此方法,fullScreen()方法自动为true) .statusBarColor(R.color.colorPrimary) //状态栏颜色,不写默认透明色 .navigationBarColor(R.color.colorPrimary) //导航栏颜色,不写默认黑色 .barColor(R.color.colorPrimary) //同时自定义状态栏和导航栏颜色,不写默认状态栏为透明色,导航栏为黑色 .statusBarAlpha(0.3f) //状态栏透明度,不写默认0.0f .navigationBarAlpha(0.4f) //导航栏透明度,不写默认0.0F .barAlpha(0.3f) //状态栏和导航栏透明度,不写默认0.0f .statusBarDarkFont(true) //状态栏字体是深色,不写默认为亮色 .fullScreen(true) //有导航栏的情况下,activity全屏显示,也就是activity最下面被导航栏覆盖,不写默认非全屏 .hideBar(BarHide.FLAG_HIDE_BAR) //隐藏状态栏或导航栏或两者,不写默认不隐藏 .setViewSupportTransformColor(toolbar) //设置支持view变色,支持一个view,不指定颜色,默认和状态栏同色,还有两个重载方法 .addViewSupportTransformColor(toolbar) //设置支持view变色,可以添加多个view,不指定颜色,默认和状态栏同色,还有两个重载方法 .statusBarView(view) //解决状态栏和布局重叠问题,下面有详细说明 .fitsSystemWindows(false) //解决状态栏和布局重叠问题,默认为false,当为true时一定要指定statusBarColor(),不然状态栏为透明色,下面有详细说明 .statusBarColorTransform(R.color.orange) //状态栏变色后的颜色 .navigationBarColorTransform(R.color.orange) //导航栏变色后的颜色 .barColorTransform(R.color.orange) //状态栏和导航栏变色后的颜色 .removeSupportView() //移除通过setViewSupportTransformColor()方法指定的view .removeSupportView(toolbar) //移除指定view支持 .removeSupportAllView() //移除全部view支持 .init(); //必须调用方可沉浸式
2)详细介绍
-
解决状态栏和布局顶部重合
上面已经说了,沉浸式原理就是使整个布局延伸到状态栏和导航栏,既然这样必然导致一个问题,就是状态栏和布局顶部重叠,直接看图
状态栏和布局顶部重叠.png眼神好的同学已经看到上图中给了四种解决方案啦,在这里我就说一下
- 1️⃣ 使用dimen自定义状态栏高度
- 在values-v19/dimens.xml文件下
<dimen name="status_bar_height">25dp</dimen>
- 在values/dimens.xml文件下
<dimen name="status_bar_height">0dp</dimen>
- 然后在布局界面添加view标签,高度指定为status_bar_height
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/darker_gray" android:orientation="vertical"> <View android:layout_width="match_parent" android:layout_height="@dimen/status_bar_height" android:background="@color/colorPrimary" /> <android.support.v7.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary" app:title="方法一" app:titleTextColor="@android:color/white" /> </LinearLayout>
- 在values-v19/dimens.xml文件下
- 2️⃣ 使用系统的fitsSystemWindows属性
- 在布局文件的根节点使用android:fitsSystemWindows=”true”属性
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:fitsSystemWindows="true"> </LinearLayout>
- 然后使用ImmersionBar时候必须指定状态栏颜色
ImmersionBar.with(this) .statusBarColor(R.color.colorPrimary) .init();
- 在布局文件的根节点使用android:fitsSystemWindows=”true”属性
- 3️⃣ 使用ImmersionBar的fitsSystemWindows(boolean fits)方法
- 实现原理是获得rootView的根节点,然后设置距离顶部的padding值为状态栏的高度值
ImmersionBar.with(this) .statusBarColor(R.color.colorPrimary) .fitsSystemWindows(true) //使用该属性必须指定状态栏的颜色,不然状态栏透明,很难看 .init();
- 实现原理是获得rootView的根节点,然后设置距离顶部的padding值为状态栏的高度值
- 4️⃣ 使用ImmersionBar的statusBarView(View view)方法
- 在标题栏的上方增加View标签(也可以为其他标签),高度指定为0dp
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/darker_gray" android:orientation="vertical"> <View android:layout_width="match_parent" android:layout_height="0dp" android:background="@color/colorPrimary" /> <android.support.v7.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary" app:title="方法四" app:titleTextColor="@android:color/white" /> </LinearLayout>
- 然后使用ImmersionBar的statusBarView方法,指定view就可以啦,实现原理:ImmersionBar获取状态栏的高度,传入view,设置高度为获取到的状态栏高度
ImmersionBar.with(this) .statusBarView(view) .init();
- 在标题栏的上方增加View标签(也可以为其他标签),高度指定为0dp
- 总结:这四种方法,任选其一使用就可以了,不要一起使用哦,根据项目而定,比如有侧边栏的,建议使用第1️⃣种或者第4️⃣种,最后来一张效果图
四种方法最后效果图.jpg
- 1️⃣ 使用dimen自定义状态栏高度
-
图片状态栏+彩色导航栏
ImmersionBar.with(this) .transparentStatusBar() //不写也可以,默认就是透明色 .navigationBarColor(R.color.colorPrimary) .init();
图片状态栏+彩色导航栏.jpg -
全屏图片
ImmersionBar.with(this).transparentBar().init();

全屏图片.jpg
-
彩色状态栏+彩色导航栏
ImmersionBar.with(this) .statusBarColor(R.color.colorPrimary) .navigationBarColor(R.color.btn8) .init();
彩色状态栏+彩色导航栏.jpg -
结合DrawerLayout使用
结合DrawerLayout使用.gif -
结合Fragment使用(fragment+viewpager)
为了使每个fragment都可以设置不同的沉浸式样式,这里给出两种解决方式
- 1️⃣使用viewpager的addOnPageChangeListener方法,直接上代码
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { ImmersionBar immersionBar = ImmersionBar.with(FragmentActivity.this); switch (position) { case 0: immersionBar.statusBarDarkFont(false) .navigationBarColor(R.color.btn4) .init(); break; case 1: immersionBar.statusBarDarkFont(true) .navigationBarColor(R.color.btn3) .init(); break; case 2: immersionBar.statusBarDarkFont(false) .navigationBarColor(R.color.btn13) .init(); break; case 3: immersionBar.statusBarDarkFont(true) .navigationBarColor(R.color.btn1) .init(); break; } } @Override public void onPageScrollStateChanged(int state) { } });
- 2️⃣ fragment继承库里的ImmersionFragment,然后在immersionInit()方法中初始化,直接上代码
public class OneFragment extends ImmersionFragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_one, container, false); } @Override protected void immersionInit() { ImmersionBar.with(getActivity()) .statusBarDarkFont(false) .navigationBarColor(R.color.btn4) .init(); } }
1️⃣和2️⃣实现效果相同,如下
结合fragment使用.gif
- 1️⃣使用viewpager的addOnPageChangeListener方法,直接上代码
-
结合侧滑返回使用
侧滑返回.gif -
修改状态栏字体颜色为深色
ImmersionBar.with(this).statusBarDarkFont(true).init();
修改状态栏字体颜色为深色.jpg -
设置状态栏和导航栏透明度
ImmersionBar.with(this) .navigationBarColor(R.color.colorPrimary) .barAlpha(0.2f) .init();

设置状态栏和导航栏透明度.jpg
-
隐藏状态栏
ImmersionBar.with(this).hideBar(BarHide.FLAG_HIDE_STATUS_BAR).init();
-
隐藏导航栏
ImmersionBar.with(this).hideBar(BarHide.FLAG_HIDE_NAVIGATION_BAR).init();
-
隐藏状态栏+导航栏
ImmersionBar.with(this).hideBar(BarHide.FLAG_HIDE_BAR).init();
-
恢复状态栏+导航栏
ImmersionBar.with(this).hideBar(BarHide.FLAG_SHOW_BAR).init();
-
解决EditText和软键盘的问题
实现原理:监听界面容器的layout变化,当发生变化时,通过检查窗口可见区域高度,判断键盘是否弹起,如果弹起,则修改容器bottom padding,也就是手动实现adjustResize效果,给键盘留出显示空间。
KeyboardPatch.patch(this, linearLayout).enable(); //解决底部EditText和软键盘的问题,linearLayout指的是当前布局的根节点

解决EditText和软键盘的问题.gif
-
当白色背景状态栏遇到不能改变状态栏字体为深色的设备时,解决方案
if(ImmersionBar.isSupportStatusBarDarkFont()){ //判断当前设备支不支持状态栏字体变色 //处理状态栏字体为黑色 }else { //处理状态栏有透明度 }
白色背景状态栏.png
-
特性总结
ImmersionBar除了这些特性之外,还有其他特性哦,这里就不一一指出了,大家参考高级用法的注释,可以去实现看看哦,下面就来分析源码吧
3、源码分析
本库采用类似建造者模式来完成,只为了方便大家更灵活的去设置状态栏和导航栏风格。实现沉浸式是分为两块,一块是android5.0以上,一块是android4.4,这两块实现原理完全不一样,在讲解原理之前先看几个概念,下面需要用到
- View.SYSTEM_UI_FLAG_VISIBLE:显示状态栏,Activity不全屏显示(恢复到有状态的正常情况)。
- View.INVISIBLE:隐藏状态栏,同时Activity会伸展全屏显示。
- View.SYSTEM_UI_FLAG_FULLSCREEN:Activity全屏显示,且状态栏被隐藏覆盖掉。
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:Activity全屏显示,但状态栏不会被隐藏覆盖,状态栏依然可见,Activity顶端布局部分会被状态遮住。
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION:效果同View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- View.SYSTEM_UI_LAYOUT_FLAGS:效果同View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION:隐藏虚拟按键(导航栏)。有些手机会用虚拟按键来代替物理按键。
- View.SYSTEM_UI_FLAG_LOW_PROFILE:状态栏显示处于低能显示状态(low profile模式),状态栏上一些图标显示会被隐藏。
-
android 5.0以上核心代码
Android自5.0起,为我们提供了设置状态栏和导航栏颜色的API,我们可以自己设置状态栏和导航栏的颜色。本框架在android5.0以上就是采用官方api完成的,网上关于5.0以上的实现基本都是这样,在这里就不多说了,在这里只说一点,就是设置颜色的时候不是直接填入开发者传入的颜色值,而是采用v4包下的ColorUtils.blendARGB()方法来设置,为什么这样设计呢?有些app的状态栏并不是和标题栏颜色相同,稍微有些色差,所以在这里开发者只需要通过blendARGB()设置透明度就可以形成这种色差,而且还可以指定两种颜色之间的色差值,方便大家,android4.4上亦是如此。请看以下代码:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int uiFlags =View.SYSTEM_UI_FLAG_LAYOUT_STABLE //防止系统栏隐藏时内容区域大小发生变化
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; //Activity全屏显示,但状态栏不会被隐藏覆盖,状态栏依然可见,Activity顶端布局部分会被状态栏遮住。
if (mBarParams.fullScreen) {
uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; //Activity全屏显示,但导航栏不会被隐藏覆盖,导航栏依然可见,Activity底部布局部分会被导航栏遮住。
}
mWindow.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); //取消设置透明状态栏和导航栏
mWindow.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); //需要设置这个才能设置状态栏颜色
mWindow.setStatusBarColor(ColorUtils.blendARGB(mBarParams.statusBarColor,
mBarParams.statusBarColorTransform, mBarParams.statusBarAlpha)); //设置状态栏颜色
mWindow.setNavigationBarColor(ColorUtils.blendARGB(mBarParams.navigationBarColor,
mBarParams.navigationBarColorTransform, mBarParams.navigationBarAlpha)); //设置导航栏颜色
mWindow.getDecorView().setSystemUiVisibility(uiFlags); 把刚才设置的标记通过setSystemUiVisibility方法设置进去
}
-
android 4.4核心代码
在4.4里就没有5.0以上这些api了,只能设置透明状态栏和导航栏,而且设置透明导航栏之后,底部布局会被导航栏遮住,那怎么办呢?好吧,只能自己写代码去实现啦。再说之前,说说我的一个思路吧。现在是2.x.x版本,在1.x.x版本的时候,4.4中实现沉浸式是引用大家非常熟悉的一个库SystemBarTint(不推荐使用了,很久没人维护了)来实现的,但是后来发现一个严重的问题,对于有导航栏的手机,设置导航栏颜色的时候,底部布局会被导航栏遮住,除此之外还有一个小问题就是当用户设置状态栏为透明色的时候,不能时刻改变bar的颜色值,are you kidding?既然出现这样的问题,就想着怎么去解决吧!就这样,我乖乖去看看SystemBarTint的源代码,哦!原理如此,发现SystemBarTint库的实现就是在状态栏和导航栏的位置自定义了可以改变背景颜色的view,然后通过window.getDecorView()得到根布局,把刚才创建的view添加进去,既然这样,为何不自己也写一个,顺便把刚才说到导航栏的问题也解决一下呢。解决方法如下代码,在这里通过注释的方法向大家说明。这里代码只是片段,不可以直接拷贝到自己的项目中
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
mWindow.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);//透明状态栏
mWindow.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);//透明导航栏,设置这个,如果有导航栏,底部布局会被导航栏遮住
setupStatusBarView(); //在根节点创建一个可以自定义颜色的状态栏创建一个假的状态栏
if (mConfig.hasNavigtionBar()) //判断是否存在导航栏
setupNavBarView(); //在根节点创建一个可以自定义颜色的导航栏
// 解决android4.4有导航栏的情况下,activity底部被导航栏遮挡的问题
if (mConfig.hasNavigtionBar() && !mBarParams.fullScreenTemp && !mBarParams.fullScreen) {
if (mConfig.isNavigationAtBottom()) //判断导航栏是否在底部
mContentView.setPadding(0, 0, 0, mConfig.getNavigationBarHeight()); //有导航栏,获得当前布局的根节点,然后设置距离底部的padding值为导航栏的高度值
else
mContentView.setPadding(0, 0, mConfig.getNavigationBarWidth(), 0); //不在底部,设置距离右边的padding值为导航栏的宽度值
} else {
mContentView.setPadding(0, 0, 0, 0); //没有导航栏,什么都不做
}
}
/**
* 在根节点创建一个可以自定义颜色的状态栏
*/
private void setupStatusBarView() {
if (mBarParams.statusBarView == null) {
mBarParams.statusBarView = new View(mActivity);//创建一个view
}
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, getStatusBarHeight(mActivity));
params.gravity = Gravity.TOP; //把view设置在顶部
if (!isNavigationAtBottom(mActivity)) {
params.rightMargin = getNavigationBarWidth(mActivity); //横屏的时候,距离右边的距离
}
mBarParams.statusBarView.setLayoutParams(params);
mBarParams.statusBarView.setBackgroundColor(ColorUtils.blendARGB(mBarParams.statusBarColor,
mBarParams.statusBarColorTransform, mBarParams.statusBarAlpha));//设置view的颜色
mBarParams.statusBarView.setVisibility(View.VISIBLE);
ViewGroup viewGroup = (ViewGroup) mBarParams.statusBarView.getParent();
if (viewGroup != null)
viewGroup.removeView(mBarParams.statusBarView);
mViewGroup.addView(mBarParams.statusBarView);
}
/**
* 在根节点创建一个可以自定义颜色的导航栏
*/
private void setupNavBarView() {
if (mBarParams.navigationBarView == null) {
mBarParams.navigationBarView = new View(mActivity); //创建一个view
}
FrameLayout.LayoutParams params;
if (isNavigationAtBottom(mActivity)) { //判断导航栏是否在底部
params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, getNavigationBarHeight(mActivity));
params.gravity = Gravity.BOTTOM; //如果在底部把view设置在导航栏的位置
} else {
params = new FrameLayout.LayoutParams(getNavigationBarWidth(mActivity), FrameLayout.LayoutParams.MATCH_PARENT);
params.gravity = Gravity.END; //不在底部,把view设置到布局的结束位置
}
mBarParams.navigationBarView.setLayoutParams(params);
if (!mBarParams.fullScreen && (mBarParams.navigationBarColorTransform == Color.TRANSPARENT)) {
mBarParams.navigationBarView.setBackgroundColor(ColorUtils.blendARGB(mBarParams.navigationBarColor,
Color.BLACK, mBarParams.navigationBarAlpha));
} else {
mBarParams.navigationBarView.setBackgroundColor(ColorUtils.blendARGB(mBarParams.navigationBarColor,
mBarParams.navigationBarColorTransform, mBarParams.navigationBarAlpha));
}
mBarParams.navigationBarView.setVisibility(View.VISIBLE);
ViewGroup viewGroup = (ViewGroup) mBarParams.navigationBarView.getParent();
if (viewGroup != null)
viewGroup.removeView(mBarParams.navigationBarView);
mViewGroup.addView(mBarParams.navigationBarView);
}
-
状态栏字体颜色
沉浸式原理说完了,在看看状态栏字体颜色怎么去修改吧,在android 6.0以上系统为我们提供了相关的api来设置状态栏字体颜色,如下
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); }
但是对于一些第三方rom包来说,系统api就没办法实现了,还好小米和魅族公开了各自的实现方法,如下:
- flymeOS:
public static boolean FlymeSetStatusBarLightMode(Window window, boolean dark) { boolean result = false; if (window != null) { try { WindowManager.LayoutParams lp = window.getAttributes(); Field darkFlag = WindowManager.LayoutParams.class .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON"); Field meizuFlags = WindowManager.LayoutParams.class .getDeclaredField("meizuFlags"); darkFlag.setAccessible(true); meizuFlags.setAccessible(true); int bit = darkFlag.getInt(null); int value = meizuFlags.getInt(lp); if (dark) { value |= bit; } else { value &= ~bit; } meizuFlags.setInt(lp, value); window.setAttributes(lp); result = true; } catch (Exception e) { } } return result; }
- MIUI:
public static boolean MIUISetStatusBarLightMode(Window window, boolean dark) { boolean result = false; if (window != null) { Class clazz = window.getClass(); try { int darkModeFlag = 0; Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams"); Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE"); darkModeFlag = field.getInt(layoutParams); Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class); if(dark){ extraFlagField.invoke(window,darkModeFlag,darkModeFlag);//状态栏透明且黑色字体 }else{ extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体 } result=true; }catch (Exception e){ } } return result; }
- 状态栏和导航栏的隐藏
android 4.1以上支持状态栏和导航栏隐藏
private int hideBar(int uiFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
switch (mBarParams.barHide) {
case FLAG_HIDE_BAR: //隐藏状态栏和导航栏
uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.INVISIBLE;
break;
case FLAG_HIDE_STATUS_BAR: //隐藏状态栏
uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.INVISIBLE;
break;
case FLAG_HIDE_NAVIGATION_BAR://隐藏导航栏
uiFlags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
break;
case FLAG_SHOW_BAR://恢复显示
uiFlags |= View.SYSTEM_UI_FLAG_VISIBLE;
break;
}
}
return uiFlags | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
4、总结
至此,ImmersionBar库的用法与原理都讲完了。网上关于沉浸式的介绍铺天盖地,但是很少有人把它们封装起来,当开发者调用的时候还得自己去写大量代码,消耗大家时间。写这个库的目的就是方便大家的开发,解决大家在沉浸式方面出现的问题。如果还有不懂得地方可以去demo里看看,或者直接底下留言!
转载请注明:Android开发中文站 » 沉浸式管理:让你的APP更优雅