Android下拉阻尼效果實現(xiàn)原理及簡單實例
前言
本文將通過代碼講解下拉阻尼效果的實現(xiàn)原理。
實現(xiàn)靈感來源于這篇博客,但是這篇博客的代碼并不能讓我滿意,或者說是糟糕的,不過還是非常感謝作者帶給我的啟發(fā)。
現(xiàn)在大部分資訊類安卓APP都有一個下拉刷新的功能,又如微信聯(lián)系人列表頂部的小程序入口,也使用了這種下拉阻尼的效果。
我的代碼主要是解釋其實現(xiàn)原理,為方便讀者理解,所以代碼邏輯非常簡單,但如果想要實現(xiàn)例如下拉刷新轉(zhuǎn)動的進(jìn)度圈,還需要修改代碼中的MoveHeaderTask類中的onProgressUpdate方法;如果要實現(xiàn)滑動列表頂部加入這種下拉阻尼效果,則需要修改代碼中的onTouch方法,通過判斷是否到達(dá)列表頂部來決定是否觸發(fā)下拉阻尼效果的邏輯代碼。
最新的微信版本還實現(xiàn)了一個具有慣性的滑動列表(不清楚這樣表述是否正確),滑動的速度大小和小程序入口的下拉阻尼效果會形成互動,但這已不是本文討論的重點,這需要感興趣的讀者自行對我的代碼進(jìn)行迭代。
運行效果如下:

如圖,拉動"可見主體"到達(dá)一定高度,"隱藏頭部"就會彈出,反之,向上滑動到一定高度,"隱藏頭部"則會收回,如果未到達(dá)指定高度,則恢復(fù)原狀。
實際運行效果其實很流暢,也不會出現(xiàn)上圖中,頭部無法完全隱藏的情況,只是AS自帶的錄屏工具比較差勁。我不建議把這個自定義控件用在對話框類型的activity上,因為前一個activity處于可見狀態(tài),可能會占用大量算力,導(dǎo)致動畫效果不流暢,親測。
原理
這種效果是通過自定義控件的方式來實現(xiàn)的,我自定義了一個控件類型,這個自定義控件(PullDownDumperLayout)繼承自線性布局(LinearLayout) 。
用戶可以下拉彈出的那個視圖,例如微信的小程序列表,開發(fā)者只是將這個視圖移出了父元素之外,所以不可見,我們暫且稱之為隱藏頭部,只有下拉到一定程度才會彈出,而主體,例如微信的聯(lián)系人列表,則是可見的,布局見下圖。

實現(xiàn)這個效果需要我們做三件工作:
1.隱藏作為頭部的控件
2.監(jiān)聽用戶對屏幕的操作事件
3.實現(xiàn)下拉回彈的動畫效果
我們這個自定義控件會自動獲取內(nèi)部第一個子元素充當(dāng)頭部,其余的元素則是充當(dāng)可見的主體(詳見代碼中的注釋)。
基本的布局原理差不多就這樣了,但是我們還需要讓自定義控件監(jiān)聽用戶的手勢操作,例如上下滑動等。這里我和靈感來源的那篇博客一樣,讓自定義控件實現(xiàn)View.OnTouchListener接口,實現(xiàn)內(nèi)部的onTouch方法可以監(jiān)聽來自屏幕的所有觸摸操作。代碼中我讓頭部和第二個子元素(可見的主體)注冊了這個監(jiān)聽器,這是為了方便讀者理解,讀者可根據(jù)自己的需求進(jìn)行修改。
注意,對于不能監(jiān)聽屏幕觸摸事件的控件需要添加:
android:clickable="true"
至此,我們已經(jīng)可以進(jìn)行布局和監(jiān)聽用戶手勢了,但是還需要實現(xiàn)一個頭部展開和隱藏的動畫效果。當(dāng)用戶將隱藏頭部下拉或上滑到一定高度時,這個效果就會被觸發(fā),這需要依賴上面所述的onTouch方法。動畫效果的實現(xiàn)需要另開一個線程進(jìn)行操作,線程的啟動方式我們可以采用繼承AsyncTask類來實現(xiàn)。
除此之外,我們可能會多次復(fù)用這個控件,所以在自定義控件類的最后還需要一些調(diào)整參數(shù)的set方法。
這里提個醒,在接下來的代碼中,我們的自定義控件因為繼承自LinearLayout,里面需要重寫onLayout方法,而onLayout方法顧名思義就是布局,這個方法在Activity中的onCreate方法執(zhí)行之后才會被調(diào)用,所以我們可以在Activity的onCreate方法中利用findViewById獲取實例,調(diào)用上面提到的set方法進(jìn)行參數(shù)的初始化。
LinearLayout中不止onLayout一個方法,詳細(xì)解析請讀者移步其他關(guān)于XML標(biāo)簽加載過程的文章,這里不做贅述。
代碼
PullDownDumperLayout .java:
public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener {
/**
* 取布局中的第一個子元素為下拉隱藏頭部
*/
private View mHeadLayout;
/**
* 隱藏頭部布局的高的負(fù)值
*/
private int mHeadLayoutHeight;
/**
* 隱藏頭部的布局參數(shù)
*/
private MarginLayoutParams mHeadLayoutParams;
/**
* 判斷是否為第一次初始化,第一次初始化需要把headView移出界面外
*/
private boolean mOnLayoutIsInit=false;
/**
* 移動時,前一個坐標(biāo)
*/
private float mMoveY;
/**
* 如果為false,會退出頭部展開或隱藏動畫
*/
private boolean mChangeHeadLayoutTopMargin;
/**
* 觸發(fā)動畫的分界線,由mRatio計算得到
*/
private int mBoundary;
/**
* 頭部布局的隱藏和展開速度,以及單次執(zhí)行時間
*/
private int mHeadLayoutHideSpeed;
private int mHeadLayoutUnfoldSpeed;
private long mSleepTime;
/**
* 觸發(fā)動畫的分界線,頭部布局上半部分和整體高度的比例
*/
private double mRatio;
public PullDownDumperLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化參數(shù),根據(jù)自己的需求調(diào)整
mHeadLayoutHideSpeed=-20;
mHeadLayoutUnfoldSpeed=20;
mSleepTime=10;
mRatio=0.5;
}
/**
* 布局開始設(shè)置每一個控件
* 在activity的onCreate執(zhí)行之后才會執(zhí)行
* 因此可以在onCreate中調(diào)用set方法設(shè)置參數(shù)
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(!mOnLayoutIsInit && changed) {
//將第一個子元素作為頭部移出界面外
mHeadLayout = this.getChildAt(0);
mHeadLayoutHeight=-mHeadLayout.getHeight();
mBoundary=(int)(mRatio*mHeadLayoutHeight);//計算觸發(fā)動畫分界線
mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams();
mHeadLayoutParams.topMargin=mHeadLayoutHeight;
mHeadLayout.setLayoutParams(mHeadLayoutParams);
//TODO 設(shè)置手勢監(jiān)聽器,不能觸碰的控件需要添加android:clickable="true"
getChildAt(1).setOnTouchListener(this);
mHeadLayout.setOnTouchListener(this);
//標(biāo)記已被初始化
mOnLayoutIsInit=true;
}
}
/**
* 屏幕觸摸操作監(jiān)聽器
* @return false則注冊本監(jiān)聽器的控件將不會對事件做出響應(yīng),true則相反
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mMoveY=event.getRawY();//捕獲按下時的坐標(biāo),初始化mMoveY
mChangeHeadLayoutTopMargin=false;
break;
case MotionEvent.ACTION_MOVE:
float currY=event.getRawY();
int vector=(int)(currY-mMoveY);//向量,用于判斷手勢的上滑和下滑
mMoveY=currY;
//判斷是否為滑動
if(Math.abs(vector)==0){
return false;
}
//頭部完全隱藏時不再向上滑動
if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) {
return false;
}
//頭部完全展開時不再向下滑動
if (vector > 0 && mHeadLayoutParams.topMargin >= 0) {
return false;
}
//對增量進(jìn)行修正,對滑動距離進(jìn)行減半
int topMargin = mHeadLayoutParams.topMargin + (vector/2);//阻尼值
if(topMargin>0){
// 瞬間拉動的距離超過了頭部高度,因為這一瞬間很短,這里采用直接賦值的方式
// 如需平滑過渡,要另開線程,并且監(jiān)聽到ACTION_DOWN時線程可被打斷
topMargin = 0;
}
else if(topMargin<mHeadLayoutHeight){
// 瞬間拉動的距離超過了頭部高度,因為這一瞬間很短,這里采用直接賦值的方式
// 如需平滑過渡,要另開線程,并且監(jiān)聽ACTION_DOWN時線程可被打斷
topMargin = mHeadLayoutHeight;
}
//用戶對屏幕的滑動將會改變控件的TopMargin
mHeadLayoutParams.topMargin = topMargin ;
mHeadLayout.setLayoutParams(mHeadLayoutParams);
break;
default:
//TODO 出現(xiàn)其他觸碰事件,如MotionEvent.ACTION_UP時,根據(jù)閾值判斷此時頭部應(yīng)該彈出還是隱藏
mChangeHeadLayoutTopMargin=true;
if(mHeadLayoutParams.topMargin<=mBoundary){
//隱藏
new MoveHeaderTask().execute(true);
}
else{
//展開
new MoveHeaderTask().execute(false);
}
break;
}
return false;
}
/**
* 新線程,隱藏或者展開頭部布局,線程可被ACTION_DOWN打斷
*/
class MoveHeaderTask extends AsyncTask<Boolean, Integer, Integer> {
/**
*
* @param opt true為隱藏動畫,false為展開動畫
* @return
*/
@Override
protected Integer doInBackground(Boolean... opt) {
int topMargin=mHeadLayoutParams.topMargin;
//true為隱藏,false為展開
int speed=(opt[0])?mHeadLayoutHideSpeed:mHeadLayoutUnfoldSpeed;
while(mChangeHeadLayoutTopMargin){
topMargin += speed;
if (topMargin <= mHeadLayoutHeight||topMargin>=0) {
topMargin=(opt[0])?mHeadLayoutHeight:0;
publishProgress(topMargin);
break;
}
publishProgress(topMargin);
sleep(mSleepTime);
}
return null;
}
//調(diào)用publishProgress后會執(zhí)行
@Override
protected void onProgressUpdate(Integer... topMargin) {
mHeadLayoutParams.topMargin=topMargin[0];
mHeadLayout.setLayoutParams(mHeadLayoutParams);
}
}
//調(diào)整參數(shù)
public void setHeadLayoutHideSpeed(int speed){
this.mHeadLayoutHideSpeed=speed;
}
public void setHeadLayoutUnfoldSpeed(int speed){
this.mHeadLayoutUnfoldSpeed=speed;
}
public void setSleepTime(long time){
this.mSleepTime=time;
}
public void setRatio(double ratio){
this.mRatio=ratio;
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.pulldowndumpertest.PullDownDumperLayout
android:tag="記得將這個標(biāo)簽修改為自己的包名"
android:id="@+id/PullDownDumper"
android:layout_width="900px"
android:layout_height="1920px"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@null"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="500px"
android:orientation="vertical"
android:background="@color/colorPrimary"
android:clickable="true">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="隱藏頭部"
android:textSize="100px"
android:gravity="center"
android:textColor="#FFFFFF"
android:background="@null"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="1700px"
android:background="@color/colorPrimaryDark"
android:clickable="true">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="可見主體"
android:textSize="100px"
android:gravity="center"
android:textColor="#FFFFFF"
android:background="@null"/>
</LinearLayout>
</com.example.pulldowndumpertest.PullDownDumperLayout>
</android.support.constraint.ConstraintLayout>
MainActivity.java:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//TODO 讀者可在這里初始化參數(shù)
PullDownDumperLayout pddl=findViewById(R.id.PullDownDumper);
}
}
下面是筆者正在使用的自定義控件,比上述的控件多了一個效果:
頭部處于隱藏或展開的不同狀態(tài)時,觸發(fā)動畫效果的分界線可以隨狀態(tài)不同而改變。
還是拿最新版的微信小程序入口來講,用戶在下拉時,小程序界面會占用整個屏幕,如果觸發(fā)動畫的分界線太低,這樣導(dǎo)致的結(jié)果是用戶可能無法通過上滑重新返回聯(lián)系人列表,但由于微信沒有對滑動距離進(jìn)行減半處理,所以不存在上述問題,可能是出于防止誤觸的原因,從小程序界面返回聯(lián)系人列表的方式改用點擊底部的一個按鈕。而我的控件可以通過改變觸發(fā)動畫效果的分界線來解決這一問題,感興趣的讀者可以研究一下。
public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener {
/**
* 取布局中的第一個子元素為下拉隱藏頭部
*/
private View mHeadLayout;
/**
* 隱藏頭部布局的高的負(fù)值
*/
private int mHeadLayoutHeight;
/**
* 隱藏頭部的布局參數(shù)
*/
private MarginLayoutParams mHeadLayoutParams;
/**
* 判斷是否為第一次初始化,第一次初始化需要把headView移出界面外
*/
private boolean mOnLayoutIsInit=false;
/**
* 從配置獲取的滾動判斷閾值,為兩點間的距離,超過此閾值判斷為滾動
*/
// private int mScaledTouchSlop;
/**
* 按下時的y軸坐標(biāo)
*/
// private float mDownY;
/**
* 移動時,前一個坐標(biāo)
*/
private float mMoveY;
/**
* 如果為false,會退出頭部展開或隱藏動畫
*/
private boolean mChangeHeadLayoutTopMargin;
/**
* 頭部布局的隱藏和展開速度,以及單次執(zhí)行時間
*/
private int mHeadLayoutHideSpeed;
private int mHeadLayoutUnfoldSpeed;
private long mSleepTime;
/**
* 初始化頭部布局的偏移值,數(shù)值越大,頭部可見部分越多,預(yù)設(shè)值為0,即初始時頭部完全不可見
*/
private int mTopMarginOffset;
/**
* 觸發(fā)動畫的分界線,頭部布局上半部分和整體高度的比例
*/
private double mUnfoldRatio;
private double mHideRatio;
/**
* 觸發(fā)動畫的分界線,初始值由mRatio計算得到
* 頭部處于隱藏時等于mUnfoldBoundary
* 頭部處于展開時等于mHideBoundary
* mBoundary在onTouch的ACTION_DOWN中變化
*/
private int mBoundary;
private int mUnfoldBoundary;
private int mHideBoundary;
/**
* 阻尼值,越大越難拖動,呈線性趨勢
*/
private int mDumper;
public PullDownDumperLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// mScaledTouchSlop= ViewConfiguration.get(context).getScaledTouchSlop();
mHeadLayoutHideSpeed=-30;
mHeadLayoutUnfoldSpeed=30;
mSleepTime=10;
mUnfoldRatio=0.6;
mHideRatio=mUnfoldRatio;
mDumper=2;
mTopMarginOffset=-200;
}
/**
* 布局開始設(shè)置每一個控件
* 在activity的onCreate執(zhí)行之后才會執(zhí)行
* 因此可以在onCreate中調(diào)用set方法設(shè)置參數(shù)
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//只初始化一次
if(!mOnLayoutIsInit && changed) {
//將第一個子元素作為頭部移出界面外
mHeadLayout = this.getChildAt(0);
mHeadLayoutHeight=-mHeadLayout.getHeight();
mUnfoldBoundary=(int)(mUnfoldRatio*mHeadLayoutHeight);//計算觸發(fā)展開動畫分界線
mHideBoundary=(int)(mHideRatio*mHeadLayoutHeight);//計算觸發(fā)隱藏動畫分界線
mBoundary=mUnfoldBoundary;//觸發(fā)動畫的分界線初始為mUnfoldBoundary
mHeadLayoutHeight-=mTopMarginOffset;//頭部隱藏布局可見的部分
mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams();
mHeadLayoutParams.topMargin=mHeadLayoutHeight;
mHeadLayout.setLayoutParams(mHeadLayoutParams);
//TODO 設(shè)置手勢監(jiān)聽器,不能觸碰的控件需要添加android:clickable="true"
getChildAt(1).setOnTouchListener(this);
mHeadLayout.setOnTouchListener(this);
//標(biāo)記已被初始化
mOnLayoutIsInit=true;
}
}
/**
* 屏幕觸摸操作監(jiān)聽器
* @return false: 注冊本監(jiān)聽器的控件將不會對事件做出響應(yīng),true則相反
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//根據(jù)此時處于完全展開或完全隱藏決定mBoundary的值,如果兩種情況都不滿足則不做改變
if(mHeadLayoutParams.topMargin==mHeadLayoutHeight)
mBoundary=mUnfoldBoundary;
else if(mHeadLayoutParams.topMargin==0)
mBoundary=mHideBoundary;
// mDownY=event.getRawY();//獲取按下的屏幕y坐標(biāo)
mMoveY=event.getRawY();
mChangeHeadLayoutTopMargin=false;//false會打斷隱藏或展開頭部布局的動畫
break;
case MotionEvent.ACTION_MOVE:
float currY=event.getRawY();
int vector=(int)(currY-mMoveY);//向量,用于判斷手勢的上滑和下滑
mMoveY=currY;
//判斷是否為滑動
if(Math.abs(vector)==0){
return false;
}
//頭部完全隱藏時不再向上滑動
if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) {
return false;
}
//頭部完全展開時不再向下滑動
else if (vector > 0 && mHeadLayoutParams.topMargin >= 0) {
return false;
}
//對增量進(jìn)行修正
int topMargin = mHeadLayoutParams.topMargin + (vector/mDumper);
if(topMargin>0){
// 瞬間拉動的距離超過了頭部高度,因為這一瞬間很短,這里采用直接賦值的方式
// 如需實現(xiàn)平滑過渡,要另開線程,并且監(jiān)聽到ACTION_DOWN時線程可被打斷
topMargin = 0;
}
else if(topMargin<mHeadLayoutHeight){
// 瞬間拉動的距離超過了頭部高度,因為這一瞬間很短,這里采用直接賦值的方式
// 如需實現(xiàn)平滑過渡,要另開線程,并且監(jiān)聽ACTION_DOWN時線程可被打斷
topMargin = mHeadLayoutHeight;
}
//使參數(shù)生效
mHeadLayoutParams.topMargin = topMargin ;
mHeadLayout.setLayoutParams(mHeadLayoutParams);
break;
default:
//出現(xiàn)其他觸碰事件,如MotionEvent.ACTION_UP時,根據(jù)閾值mBoundary判斷此時頭部應(yīng)該彈出還是隱藏
mChangeHeadLayoutTopMargin=true;//允許執(zhí)行動畫
if(mHeadLayoutParams.topMargin<=mBoundary){
//隱藏
new MoveHeaderTask().execute(true);
}
else{
//展開
new MoveHeaderTask().execute(false);
}
break;
}
return false;
}
/**
* 新線程,隱藏或者展開頭部布局,線程可被ACTION_DOWN打斷
*/
private class MoveHeaderTask extends AsyncTask<Boolean, Integer, Integer> {
/**
*
* @param opt true為隱藏動畫,false為展開動畫
* @return
*/
@Override
protected Integer doInBackground(Boolean... opt) {
int topMargin=mHeadLayoutParams.topMargin;
//true為隱藏,false為展開
int speed=(opt[0])?mHeadLayoutHideSpeed:mHeadLayoutUnfoldSpeed;
while(mChangeHeadLayoutTopMargin){
topMargin += speed;
if (topMargin <= mHeadLayoutHeight||topMargin>=0) {
topMargin=(opt[0])?mHeadLayoutHeight:0;
publishProgress(topMargin);
break;
}
publishProgress(topMargin);
sleep(mSleepTime);
}
return null;
}
//調(diào)用publishProgress后會執(zhí)行
@Override
protected void onProgressUpdate(Integer... topMargin) {
mHeadLayoutParams.topMargin=topMargin[0];
mHeadLayout.setLayoutParams(mHeadLayoutParams);
}
}
//調(diào)整參數(shù)
public void setHeadLayoutHideSpeed(int speed){
this.mHeadLayoutHideSpeed=speed;
}
public void setHeadLayoutUnfoldSpeed(int speed){
this.mHeadLayoutUnfoldSpeed=speed;
}
public void setSleepTime(long time){
this.mSleepTime=time;
}
public void setDumper(int dumper){
this.mDumper=dumper;
}
public void setTopMarginOffset(int offset){
this.mTopMarginOffset=-offset;
}
/**
* 頭部處于隱藏狀態(tài)時,觸發(fā)展開動畫的分界線
* @param ratio 頭部布局上部分與下部分的分界線
*/
public void setUnfoldRatio(double ratio){
this.mUnfoldRatio=ratio;
}
/**
* 頭部處于展開狀態(tài)時,觸發(fā)隱藏動畫的分界線
* @param ratio 頭部布局上部分與下部分的分界線
*/
public void setHideRatio(double ratio){
this.mHideRatio=ratio;
}
}```
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android實現(xiàn)簡單的下拉阻尼效應(yīng)示例代碼
- Android下拉刷新完全解析,教你如何一分鐘實現(xiàn)下拉刷新功能(附源碼)
- Android實現(xiàn)聯(lián)動下拉框 下拉列表spinner的實例代碼
- Android中Spinner(下拉框)控件的使用詳解
- Android下拉刷新ListView——RTPullListView(demo)
- Android PullToRefreshLayout下拉刷新控件的終結(jié)者
- Android中使用RecyclerView實現(xiàn)下拉刷新和上拉加載
- Android下拉刷新上拉加載控件(適用于所有View)
- Android官方下拉刷新控件SwipeRefreshLayout使用詳解
- Android屬性動畫實現(xiàn)布局的下拉展開效果
相關(guān)文章
Android 三種實現(xiàn)定時器詳解及實現(xiàn)方法
本文主要介紹 Android 定時器的知識資料,這里整理了三種方法來實現(xiàn)定時器的方法,有需要的小伙伴可以參考下2016-09-09
Android中將Bitmap對象以PNG格式保存在內(nèi)部存儲中的方法
在Android中進(jìn)行圖像處理的任務(wù)時,有時我們希望將處理后的結(jié)果以圖像文件的格式保存在內(nèi)部存儲空間中,本文以此為目的,介紹將Bitmap對象的數(shù)據(jù)以PNG格式保存下來的方法2017-08-08
Gradle屬性設(shè)置及環(huán)境變量全面教程
這篇文章主要為大家介紹了Gradle屬性設(shè)置及環(huán)境變量的全面教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
Win10下android studio開發(fā)環(huán)境配置圖文教程
這篇文章主要為大家詳細(xì)介紹了Win10下android studio開發(fā)環(huán)境配置圖文教程,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07
Android中使用Matrix控制圖形變換和制作倒影效果的方法
這篇文章主要介紹了Android中使用Matrix控制圖形變換和制作倒影效果的方法,用Matrix來作矩陣變化十分強(qiáng)大,文中的制作倒影的例子便是一個十分巧妙的運用,需要的朋友可以參考下2016-04-04
Android開發(fā)手冊shape屬性和子屬性使用說明
這篇文章主要為大家介紹了Android開發(fā)手冊shape屬性和子屬性使用說明,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06

