1. 程式人生 > >【騰訊Bugly乾貨分享】Android動態佈局入門及NinePatchChunk解密

【騰訊Bugly乾貨分享】Android動態佈局入門及NinePatchChunk解密

作者:黃進——QQ音樂團隊

擺脫XML佈局檔案

相信每一個Android開發者,在接觸“Hello World”的時候,就形成了一個觀念:Android UI佈局是通過layout目錄下的XML檔案定義的。使用XML定義佈局的方式,有著結構清晰、可預覽等優勢,因而極為通用。可是,偏偏在某些場景下,佈局是需要根據執行時的狀態變化的,無法使用XML預先定義。這時候,我們只能通過JavaCode控制,在程式執行時,動態的實現對應的佈局。

所以,作為入門,將從給三個方面給大家介紹一些動態佈局相關的基礎知識和經驗。

  • 動態新增view到介面上,擺脫layout資料夾下的XML檔案。
  • 熟悉Drawable子類,擺脫drawable資料夾下的XML檔案。
  • 解密NinePatchChunk,解析如何實現後臺下發.9圖片給客戶端使用。

動態新增View

這一步,顧名思義,就是把我們要的View新增到介面上去。這是動態佈局中最基礎最常用的步驟。

Android開發中,我們用到的ButtonImageViewRelativeLayoutLinearLayout等等元素最終都是繼承於View這個類的。按照我自己的理解,可以將它們分為兩類,控制元件和容器(這兩個名字純屬作者自己編的,並非官方定義)。ButtonImageView這類直接繼承於View的就是控制元件,控制元件一般是用來呈現內容和與使用者互動的;RelativeLayout

LinearLayout這類繼承於ViewGroup的就是容器,容器就是用來裝東西的。Android是巢狀式佈局的設計,因此,容器裝的既可以是容器,也可以是控制元件。

更直接的,還是通過一段demo程式碼來看吧。

首先,因為不能setContentView(R.layout.xxx)了,我們需要先新增一個root作為整個的容器,

RelativeLayout root = new RelativeLayout(this);
root.setBackgroundColor(Color.WHITE);
setContentView(root, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

然後,我們嘗試在螢幕正中間新增一個按鈕,

Button button1 = new Button(this);
button1.setId(View.generateViewId());
button1.setText("Button1");
button1.setBackgroundColor(Color.RED);
LayoutParams btnParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
btnParams.addRule(RelativeLayout.CENTER_IN_PARENT, 1);
root.addView(button1, btnParams);

到這裡可以發現,只需要三步,就可以新增一個view(以按鈕為例)到相應的容器root裡面了,

  • new Button(this),並初始化控制元件相關的屬性。
  • 根據root的型別,new LayoutParams,這個引數主要用來描述要新增的view在容器中的定位資訊,包括高寬,居中對齊,margin等等屬性。特別地,對於上面的例子,相對於父容器居中的實現是,btnParams.addRule(RelativeLayout.CENTER_IN_PARENT, 1),這裡對應XML的程式碼則是android:centerInParent='true'
  • 最後一步,新增到容器中, root.addView(button1, btnParams)就行了。

接下來,搞的稍微複雜點,繼續在按鈕的右下方新增一個線性佈局,向其中新增一個TextViewButton,而且各自佔的寬度比例為2:3(對於android:layout_weight屬性),demo程式碼如下,

// 在按鈕右下方新增一個線性佈局
LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
LayoutParams lParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
lParams.addRule(RelativeLayout.BELOW, button1.getId());
lParams.addRule(RelativeLayout.RIGHT_OF, button1.getId());
root.addView(linearLayout, lParams);

// 線上性佈局中,新增一個TextView和一個Button,寬度按2:3的比例
TextView textView = new TextView(this);
textView.setText("TextView");
textView.setTextSize(28);
textView.setBackgroundColor(Color.BLUE);
LinearLayout.LayoutParams tParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT);
tParams.weight = 2; // 定義寬度的比例
linearLayout.addView(textView, tParams);

Button button2 = new Button(this);
button2.setText("Button2");
button2.setBackgroundColor(Color.RED);
LinearLayout.LayoutParams bParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT);
bParams.weight = 3; // 定義寬度的比例
linearLayout.addView(button2, bParams);

需要注意的是,上面程式碼中的lParams.addRule(RelativeLayout.BELOW, button1.getId())XML對應android:layout_below

規則如果定義的是一個view相對於另一個view的,一定要初始化另一個view(button1)的id不為0,否則規則會失效。通常,為了防止id重複,建議使用系統方法來生成id,也就是第二段程式碼中的button1.setId(View.generateViewId())

最終,這一段程式碼執行下來,我們得到的效果就是,

但是,新增view作者也遇到過一個小小坑。

如下圖左邊部分,作者曾經遇到一個場景,需要在RelativeLayout右邊新增一個ImageView,同時,這個ImageView的右邊部分在RelativeLayout的外面。

一開始,作者的程式碼如下,卻只能得到上圖右邊的效果,

ImageView imageView = new ImageView(this);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height);
params.leftMargin = x;  // 到左邊的距離
params.topMargin = y;   // 到上邊的距離
parent.addView(imageView, params);

後來本人猜測,這是因為onMeasureonLayout的時候,受到了rightMargin 預設為0的限制。

後來,經過本人驗證,要跳過這個坑,加一行params.rightMargin = -1*width就可以了。(有興趣的同學可以去看看原始碼,這裡就不詳解了)

Drawable子類

上一節,我們只是擺脫了layout目錄的XML檔案。可是還有一類XML檔案,頻繁的被layout目錄的XML檔案引用,那就是drawable目錄的XML檔案。drawable目錄的下檔案,通常是定義了一些,selectorshape等等。可是,考慮到一個場景:selector裡面引用的圖片,不是打包時res目錄的資源,而是後臺下發的圖片呢?類似場景下,我們能不能擺脫這類XML檔案呢?

根據上一節的經驗,要相信,XML定義能實現的,Java程式碼一定能夠實現。從drawable的目錄名就可以看出,不管是selectorshape或是其他,總歸都應該是drawable。因此,在Java程式碼中,總應該有一個Drawable的子類來對應他們。下面,就介紹幾個常用的Drawable的子類給大家。

StateListDrawable:對應selector,主要用來描述按鈕等的點選態。

StateListDrawable selector = new StateListDrawable();
btnSelectorDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePress);
btnSelectorDrawable.addState(new int[]{android.R.attr.state_enabled}, drawableEnabel);
btnSelectorDrawable.addState(new int[]{android.R.attr.state_selected}, drawableSelected);
btnSelectorDrawable.addState(new int[]{android.R.attr.state_focused}, drawableFocused);
btnSelectorDrawable.addState(new int[]{}, drawableNormal);

GradientDrawable:對應漸變色

GradientDrawable drawable = new GradientDrawable();
drawable.setOrientation(Orientation.TOP_BOTTOM); //定義漸變的方向
drawable.setColors(colors); //colors為int[],支援2個以上的顏色

最後,說一個比較複雜的Drawable,是進度條相關的。

LayerDrawable:對應Seekbar android:progressDrawable

通常,我們用XML定義一個進度條的ProgressDrawable是這樣的,

<!--ProgressDrawable-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background" android:drawable="@drawable/background"/>
    <item android:id="@android:id/secondaryProgress" android:drawable="@drawable/secondary_progress"/>
    <item android:id="@android:id/progress" android:drawable="@drawable/progress"/>
</layer-list>

而對於其中的,@drawable/progress@drawable/secondary_progress也不是普通的drawable,

<[email protected]/progress 定義-->
<clip xmlns:android="http://schemas.android.com/apk/res/android"
      android:clipOrientation="horizontal"
      android:drawable="@drawable/progress_drawable"
      android:gravity="left" >
</clip>

也就是說,通過XML要定義進度條的ProgressDrawable,我們需要定義多個XML檔案的,還是比較複雜的。那麼JavaCode實現呢?

其實,理解了XML實現的方式,下面的JavaCode就很好理解了。

LayerDrawable layerDrawable = (LayerDrawable) getProgressDrawable();

//背景
layerDrawable.setDrawableByLayerId(android.R.id.background, backgroundDrawable);

//進度條
ClipDrawable clipProgressDrawable = new ClipDrawable(progressDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
layerDrawable.setDrawableByLayerId(android.R.id.progress, clipProgressDrawable);

//緩衝進度條
ClipDrawable clipSecondaryProgressDrawable = new ClipDrawable(secondaryProgressDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
layerDrawable.setDrawableByLayerId(android.R.id.secondaryProgress, clipSecondaryProgressDrawable);

更多的Drawable的子類,大家可以根據自己需求去官方文件上查詢就行了。

“蛋疼.9.PNG”

.9.png圖片對Android開發來說,都不陌生。通常情況下,我們對於.9.png圖片的使用,只需要簡單的放到resource目錄下,然後,當做普通圖片來用就可以了。然而,以本人的經驗,如果要動態下發’.9.png’圖片給客戶端使用就很蛋疼了。

一開始,當我想當然以為可以直接載入本地.9.png圖片,用的飛起的時候,發現了Android Nine Patch的一個大坑!!!

“說好的自動拉昇了???”(隱隱約約感覺到某需求的工作量又少評估了一天。。。。。。。)

通過查閱資料發現,原來,工程裡面用的.9.png在打包的時候,經過了aapt的處理,成為了一張包含有特殊資訊的.png圖片。而不是直接載入的.9.png這種圖片。

那麼第一個思路就來了(參考引用),首先,我們先對.9.png執行一個aapt命令。

aapt.exe s -i xx.9.png -o xx.png

然後,後臺下發這種處理過的.png,客戶端通過如下程式碼,就可以載入這張圖片,得到一個有區域性拉伸效果的NinePatchDrawable了。

Bitmap bitmap = BitmapFactory.decodeFile(filePath);
NinePatchDrawable npd = new NinePatchDrawable(context.getResource(), bitmap, bitmap.getNinePatchChunk(), new Rect(), null);

可是,這個初級方式並不是太完美,每次後臺配置新的圖片,都需要aapt處理一遍,後臺需要針對iOS和Android區分平臺下發不同圖片。總之,不太科學!那麼有沒有更加徹底的方式呢?

徹底理解.9.png

回顧NinePatchDrawable的構造方法第三個引數bitmap.getNinePatchChunk(),作者猜想,aapt命令其實就是在bitmap圖片中,加入了NinePatchChunk的資訊,那麼我們是不是隻要能自己構造出這個東西,就可以讓任何圖片按照我們想要的方式拉昇了呢?

可是查了一堆官方文件,似乎並找不到相應的方法來獲得這個byte[]型別的chunk引數。

既然無法知道這個chunk如何生成,那麼能不能從解析的角度逆向得出這個NinePatchChunk的生成方法呢?

下面就需要從原始碼入手了。

public static NinePatchChunk deserialize(byte[] data) {
    ByteBuffer byteBuffer =
            ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
    byte wasSerialized = byteBuffer.get();
    if (wasSerialized == 0) return null;
    NinePatchChunk chunk = new NinePatchChunk();
    chunk.mDivX = new int[byteBuffer.get()];
    chunk.mDivY = new int[byteBuffer.get()];
    chunk.mColor = new int[byteBuffer.get()];
    checkDivCount(chunk.mDivX.length);
    checkDivCount(chunk.mDivY.length);
    // skip 8 bytes
    byteBuffer.getInt();
    byteBuffer.getInt();
    chunk.mPaddings.left = byteBuffer.getInt();
    chunk.mPaddings.right = byteBuffer.getInt();
    chunk.mPaddings.top = byteBuffer.getInt();
    chunk.mPaddings.bottom = byteBuffer.getInt();
    // skip 4 bytes
    byteBuffer.getInt();
    readIntArray(chunk.mDivX, byteBuffer);
    readIntArray(chunk.mDivY, byteBuffer);
    readIntArray(chunk.mColor, byteBuffer);
    return chunk;
}

其實從這部分解析byte[] chunk的原始碼,我們已經可以反推出來大概的結構了。如下圖,

按照上圖中的猜想以及對.9.png的認識,直覺感受到,mDivX,mDivY,mColor這三個陣列是最關鍵的,但是具體是什麼,就要繼續看原始碼了。

/**
 * This chunk specifies how to split an image into segments for
 * scaling.
 *
 * There are J horizontal and K vertical segments.  These segments divide
 * the image into J*K regions as follows (where J=4 and K=3):
 *
 *      F0   S0    F1     S1
 *   +-----+----+------+-------+
 * S2|  0  |  1 |  2   |   3   |
 *   +-----+----+------+-------+
 *   |     |    |      |       |
 *   |     |    |      |       |
 * F2|  4  |  5 |  6   |   7   |
 *   |     |    |      |       |
 *   |     |    |      |       |
 *   +-----+----+------+-------+
 * S3|  8  |  9 |  10  |   11  |
 *   +-----+----+------+-------+
 *
 * Each horizontal and vertical segment is considered to by either
 * stretchable (marked by the Sx labels) or fixed (marked by the Fy
 * labels), in the horizontal or vertical axis, respectively. In the
 * above example, the first is horizontal segment (F0) is fixed, the
 * next is stretchable and then they continue to alternate. Note that
 * the segment list for each axis can begin or end with a stretchable
 * or fixed segment.
 * /

正如原始碼中,註釋的一樣,這個NinePatch Chunk把圖片從x軸和y軸分成若干個區域,F區域代表了固定,S區域代表了拉伸。mDivX,mDivY描述了所有S區域的位置起始,而mColor描述了,各個Segment的顏色,通常情況下,賦值為原始碼中定義的NO_COLOR = 0x00000001就行了。就以原始碼註釋中的例子來說,mDivX,mDivY,mColor如下:

mDivX = [ S0.start, S0.end, S1.start, S1.end];
mDivY = [ S2.start, S2.end, S3.start, S3.end];
mColor = [c[0],c[1],...,c[11]]

對於mColor這個陣列,長度等於劃分的區域數,是用來描述各個區域的顏色的,而如果我們這個只是描述了一個bitmap的拉伸方式的話,是不需要顏色的,即原始碼中NO_COLOR = 0x00000001

說了這麼多,我們還是通過一個簡單例子來說明如何構造一個按中心點拉伸的NinePatchDrawable吧,

Bitmap bitmap = BitmapFactory.decodeFile(filepath);
int[] xRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};
int[] yRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};
int NO_COLOR = 0x00000001;
int colorSize = 9;
int bufferSize = xRegions.length * 4 + yRegions.length * 4 + colorSize * 4 + 32;

ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize).order(ByteOrder.nativeOrder());
// 第一個byte,要不等於0
byteBuffer.put((byte) 1);

//mDivX length
byteBuffer.put((byte) 2);
//mDivY length
byteBuffer.put((byte) 2);
//mColors length
byteBuffer.put((byte) colorSize);

//skip
byteBuffer.putInt(0);
byteBuffer.putInt(0);

//padding 先設為0
byteBuffer.putInt(0);
byteBuffer.putInt(0);
byteBuffer.putInt(0);
byteBuffer.putInt(0);

//skip
byteBuffer.putInt(0);

// mDivX
byteBuffer.putInt(xRegions[0]);
byteBuffer.putInt(xRegions[1]);

// mDivY
byteBuffer.putInt(yRegions[0]);
byteBuffer.putInt(yRegions[1]);

// mColors
for (int i = 0; i < colorSize; i++) {
    byteBuffer.putInt(NO_COLOR);
}

return byteBuffer.array();

後來也在github上找到了一個現成的Library,有興趣的同學可以直接去學習和使用。

參考資料:

更多精彩內容歡迎關注bugly的微信公眾賬號:

騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智慧合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的程式碼行,實時上報可以在釋出後快速的瞭解應用的質量情況,適配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!

相關推薦

Bugly乾貨分享Android動態佈局入門NinePatchChunk解密

作者:黃進——QQ音樂團隊 擺脫XML佈局檔案 相信每一個Android開發者,在接觸“Hello World”的時候,就形成了一個觀念:Android UI佈局是通過layout目錄下的XML檔案定義的。使用XML定義佈局的方式,有著結構清

Bugly乾貨分享Android程序保活招式大全

【騰訊Bugly乾貨分享】Android程序保活招式大全 本文來自於騰訊bugly開發者社群,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/57ac4a0ea374c75371c08ce8 作者:騰訊——張興華 目前市面上的應用,貌似除了微信和手Q都會

Bugly乾貨分享Android減包 - 減少APK大小

本文是對Google官方文件 Reduce APK Size 的翻譯,點選“閱讀原文”可以檢視英文原文。 譯者簡介:damonxia(夏正冬),天天P圖Android工程師 使用者經常會避免下載看起來體積較大的應用,特別是在不穩定的2G、3G

Bugly乾貨分享Android ListView與RecyclerView對比淺析--快取機制

作者:黃寧源 一,背景 RecyclerView是谷歌官方出的一個用於大量資料展示的新控制元件,可以用來代替傳統的ListView,更加強大和靈活。 最近,自己負責的業務,也遇到這樣的一個問題,關於是否要將ListView替換為Recycl

Bugly乾貨分享Android 新一代多渠道打包神器

關於作者: 李濤,騰訊Android工程師,14年加入騰訊SNG增值產品部,期間主要負責手Q動漫、企鵝電競等專案的功能開發和技術優化。業務時間喜歡折騰新技術,寫一些技術文章,個人技術部落格:www.ltlovezh.com 。 ApkChanne

Bugly乾貨分享Android 程序保活招式大全

本文來自於騰訊bugly開發者社群, ,原文地址:https://segmentfault.com/a/1190000006251859作者:騰訊——張興華目前市面上的應用,貌似除了微信和手Q都會比較擔心被使用者或者系統(廠商)殺死問題。本文對 Android 程序拉活進行一

Bugly乾貨分享移動網際網路測試到質量的轉變

Dev Club 是一個交流移動開發技術,結交朋友,擴充套件人脈的社群,成員都是經過稽核的移動開發工程師。每週都會舉行嘉賓分享,話題討論等活動。 本期,我們邀請了 TesterHome 測試技術社群聯合創始人“陳曄”,為大家分享《移動網際網路測試到質量的轉

Bugly乾貨分享聊聊蘋果的Bug

作者:張三華 導語 精神哥最近發現, 很多開發者在 iOS10 上遇到了一類堆疊為nano_free字樣的Crash,也有很多人向我們Bugly客服反饋遇到了這類問題,但並沒有好的解決方案。正當大家都束手無策的時候,微信強大的技術團隊針對這類

Bugly乾貨分享總結一個技術總監的教訓和經驗

導語 2017年來了,新年開篇,就不跟大家聊技術啦,給大家分享一篇鵝廠技術總監在多年工作中總結出的教訓和經驗。 這篇文章自從在騰訊內部論壇發表後,精神哥每年都會拿出來重新研讀一番,每次都有新的感悟和收穫,所以強烈推薦給大家。 正文 資深程式設

Bugly乾貨分享打造“微信小程式”元件化開發框架

作者:Gcaufy 導語 Bugly 之前發了一篇關於微信小程式的開發經驗分享(點選閱讀),小夥伴們在公眾賬號後臺問了很多關於小程式開發方面的問題,精神哥在查閱相關內容的時候,發現了龔澄同學自己寫了一個小程式開發框架,真的怒贊,趕緊安利給大家

Bugly乾貨分享美團大眾點評 Hybrid 化建設

本期 T 沙龍探討了移動端熱更新相關的話題。由於沙龍時間的限制,本期我們選取了美團的 Hybrid 化建設、去哪兒的跨平臺 ListView 效能優化、微博 Android 端熱更新踩過的坑話題。還期待熱更新、熱修復哪些話題?歡迎留言給我們。也歡迎報名參加

Bugly乾貨分享跨平臺 ListView 效能優化

導語 精神哥前陣子去參加了好友小青在北京辦的T沙龍,探討移動端熱更新相關的話題。Bugly 曾為大家介紹過不少騰訊內部的熱更新的框架,正好這次看到了美團,去哪兒以及微博同學在應用熱更新方面的實踐。 上週為大家整理了《美團大眾點評 Hybrid 化建設

Bugly乾貨分享WKWebView 那些坑

導語 WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView 元件,用以替代 UIKit 中笨重難用、記憶體洩漏的 UIWebView, 擁有60fps滾動重新整理率、和 safari 相同的 JavaScript 引擎。簡單

Bugly乾貨分享你為什麼需要 Kotlin

一、往事 曾經你有段時間研究 Intellij 的外掛開發,企圖編譯 Intellij Idea Community Edition (ICE)的原始碼,結果發現有個奇怪的東西讓你的程式碼無法編譯。。什麼鬼,kt 是什麼玩意兒? 怎麼又有

bugly乾貨分享精神哥手把手教你如何智鬥ANR

上帝說要有ANR,於是Bugly就有了ANR上報,那麼ANR到底是什麼? 最近很多童鞋問起精神哥ANR的問題,那麼這次就來聊一下,雞爪怎麼泡才好吃,噢不,是如何快速定位ANR。 ANR是什麼 簡單說,通常就是App執行的時候,duang~卡住了,怎麼搞都動不

bugly乾貨分享解耦---Hybrid H5跨平臺性思考

跨平臺,是H5最重要的能力之一。而 Hybrid H5 因強依賴於具體 app,往往不具有跨平臺性。這時,將強依賴關係解耦,即可恢復 H5 的跨平臺能力。近期本人負責 手Q 紅包打賞專案的前端開發,因專案涉及到多 app 跨平臺相容,對 hybrid H5

Bugly乾貨分享WebSocket 淺析

前言 在WebSocket API尚未被眾多瀏覽器實現和釋出的時期,開發者在開發需要接收來自伺服器的實時通知應用程式時,不得不求助於一些“hacks”來模擬實時連線以實現實時通訊,最流行的一種方式是長輪詢 。 長輪詢主要是發出一個HTTP請求到伺服器,

Bugly乾貨分享基於RxJava的一種MVP實現

Dev Club 是一個交流移動開發技術,結交朋友,擴充套件人脈的社群,成員都是經過稽核的移動開發工程師。每週都會舉行嘉賓分享,話題討論等活動。 本期,我們邀請了騰訊IEG Android 開發工程師——戴俊,為大家分享《基於RxJava的一種MVP實現》

Bugly乾貨分享RecyclerView 必知必會

導語 RecyclerView是Android 5.0提出的新UI控制元件,可以用來代替傳統的ListView。 Bugly之前也發過一篇相關文章,講解了 RecyclerView 與 ListView 在快取機制上的一些區別: 今天精神哥來給

Bugly乾貨分享深入理解 ButterKnife,讓你的程式學會寫程式碼

0、引子 話說我們做程式設計師的,都應該多少是個懶人,我們總是想辦法驅使我們的電腦幫我們幹活,所以我們學會了各式各樣的語言來告訴電腦該做什麼——儘管,他們有時候也會誤會我們的意思。 突然有一天,我覺得有些程式碼其實,可以按照某種規則生成,但你又不能不