前面已經(jīng)實(shí)現(xiàn)過仿QQ的List抽屜效果以及仿QQ未讀消息拖拽效果皇筛,具體請見:
Android自定義控件:類QQ抽屜效果
Android自定義控件:類QQ未讀消息拖拽效果
趁熱打鐵答憔,這次我們實(shí)現(xiàn)QQ空間的主頁全效果蝶锋,先貼上我們最終的完成效果圖:
可以看到易稠,我們實(shí)現(xiàn)了如下效果:
- 下拉拖拽視差效果
- 透明狀態(tài)欄+TitleBar
- 狀態(tài)欄+TitleBar顏色動(dòng)態(tài)漸變
- 下拉加載更多
- 點(diǎn)擊按鈕∨彈出PopupWindow list選項(xiàng)+模糊背景效果
- 點(diǎn)擊按鈕+頂部彈出PopupWindow界面+模糊背景效果
下拉拖拽視差效果
第一步先實(shí)現(xiàn)拖拽視差效果脐帝,也就是下拉的時(shí)候,有一種阻滯感仆百,然后手抬起的時(shí)候厕隧,會(huì)稍微回彈一下。
在實(shí)現(xiàn)效果之前俄周,我們先看一下實(shí)現(xiàn)原理吁讨,我們看一下下面這張圖:
實(shí)際上呢,一整個(gè)視差效果界面峦朗,其實(shí)就是一個(gè)ListView建丧。我們給listView設(shè)置了一個(gè)headView,然后設(shè)置headView 布局的scaleType為centerCrop甚垦,取src圖片的中部也就是圖中綠色部分茶鹃,這部分是初始顯示區(qū)域。headView的src圖片上下部分實(shí)際上是處于界面之外沒有顯示出來艰亮,也就是圖中的棕色部分闭翩。
下面貼上頭布局代碼:
list_item_head.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="330dp"
android:scaleType="centerCrop"
android:id="@+id/iv_head"
android:src="@mipmap/parallax_img"/>
</LinearLayout>
然后在Activity中為listView添加頭布局:
private void init() {
for (int i = 0; i < 30; i++) {
list.add("user - " + i);
}
lvParallax = (ListView) findViewById(R.id.lv_parallax);
lvParallax.setOverScrollMode(ListView.OVER_SCROLL_NEVER);//滑到底部頂部不顯示藍(lán)色陰影
View headerView = View.inflate(this, R.layout.list_item_head, null);//添加header
ImageView ivHead = (ImageView) headerView.findViewById(R.id.iv_head);
lvParallax.initParallaxImageParams(ivHead);
lvParallax.addHeaderView(headerView);
lvParallax.setAdapter(new ParallaxAdapter(this, list));
}
item布局代碼就不貼了,我們看看現(xiàn)在運(yùn)行的效果:
注:為了方便截圖迄埃,后面的圖都是運(yùn)行在模擬器(480x800)上的效果截圖疗韵,所以顯示效果肯定跟最開始的真機(jī)(720x1280)效果有一定的區(qū)別,不過此處只是做演示侄非,這點(diǎn)小事就先忽略啦~ =蕉汪。=**
既然布局已經(jīng)完成了,那么我們接下來實(shí)現(xiàn)視差拖拽效果逞怨。
既然要拖拽者疤,我們肯定要自定義一個(gè)ListView并且重寫其onTouchEvent以及overScrollBy方法。
首先我們要思考的是叠赦,我們?nèi)绾卧谧远x控件中拿到我們headView的高度以及圖片的高度呢驹马?由于我們的headView參數(shù)是在Activity的onCreate中初始化的,但是在onCreate中無法通過getHeight()和getWidth()拿到headView的高度和寬度除秀,因?yàn)閂iew組件布局要在onResume回調(diào)后完成糯累。那么我們?nèi)绾卧趏nCreate中拿到headView的高度參數(shù)呢?這里我們通過getViewTreeObserver().addOnGlobalLayoutListener()來獲得寬度或者高度册踩。這是獲得一個(gè)view的寬度和高度的方法之一泳姐。
OnGlobalLayoutListener 是ViewTreeObserver的內(nèi)部類,當(dāng)一個(gè)視圖樹的布局發(fā)生改變時(shí)暂吉,可以被ViewTreeObserver監(jiān)聽到胖秒,這是一個(gè)注冊監(jiān)聽視圖樹的觀察者(observer),在視圖樹的全局事件改變時(shí)得到通知慕的。ViewTreeObserver不能直接實(shí)例化扒怖,而是通過getViewTreeObserver()獲得。
不多說业稼,上代碼:
/**
* 初始化ParallaxImage的初始參數(shù)
*
* @param imageView
*/
public void initParallaxImageParams(final ImageView imageView) {
this.ivHead = imageView;
//設(shè)定ImageView最大高度
imageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
imageView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
orignalHeight = imageView.getHeight();
//Log.e("tag", "orignalHeight = " + orignalHeight);
//獲取圖片的高度
int drawbleHeight = imageView.getDrawable().getIntrinsicHeight();
maxHeight = orignalHeight > drawbleHeight ? orignalHeight * 2 : drawbleHeight;
//Log.e("tag", "maxHeight = " + maxHeight);
}
});
}
在onGlobalLayout中增加一層判斷盗痒,當(dāng)headView初始高度大于圖片高度時(shí),我們?nèi)〉纳舷禄瑒?dòng)最大高度是headView*2低散。因?yàn)閺母旧蟻碇v俯邓,我們肯定是要保證headView上下部分肯定是超出界面之外的,所以這里的maxHeight肯定是要大于headView的高度的熔号。
然后重寫overScrollBy方法稽鞭,overScrollBy會(huì)在listview滑動(dòng)到頭的時(shí)候執(zhí)行,可以獲取到繼續(xù)滑動(dòng)的距離和方向引镊。當(dāng)滑動(dòng)到頭的時(shí)候朦蕴,我們通過繼續(xù)滾動(dòng)的距離篮条,動(dòng)態(tài)設(shè)置headView的高度,這樣達(dá)到一個(gè)拖動(dòng)顯示的效果吩抓。
/**
* 在listview滑動(dòng)到頭的時(shí)候執(zhí)行涉茧,可以獲取到繼續(xù)滑動(dòng)的距離和方向
* deltaX:繼續(xù)滑動(dòng)x方向的距離
* deltaY:繼續(xù)滑動(dòng)y方向的距離 負(fù):表示頂部到頭 正:表示底部到頭
* maxOverScrollX:x方向最大可以滾動(dòng)的距離
* maxOverScrollY:y方向最大可以滾動(dòng)的距離
* isTouchEvent: true: 是手指拖動(dòng)滑動(dòng) false:表示fling靠慣性滑動(dòng);
*/
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int
scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean
isTouchEvent) {
//Log.e("tag", "deltaY: " + deltaY + " isTouchEvent:" + isTouchEvent);
if (deltaY < 0 && isTouchEvent) {//頂部到頭,并且是手動(dòng)拖到頂部
if (ivHead != null) {
int newHeight = ivHead.getHeight() - deltaY / 3;
if (newHeight > maxHeight) {
newHeight = maxHeight;//限定拖動(dòng)最大高度范圍
}
ivHead.getLayoutParams().height = newHeight;//重新設(shè)置ivHead的高度值
//使布局參數(shù)生效
ivHead.requestLayout();
}
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
maxOverScrollX, maxOverScrollY, isTouchEvent);
}
最后重寫onTouchEvent方法疹娶,在這里檢測手抬起動(dòng)作伴栓,在手抬起的時(shí)候通過一個(gè)屬性動(dòng)畫回復(fù)headView原本的高度:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_UP) {
//放手的時(shí)候講imageHead的高度緩慢從當(dāng)前高度恢復(fù)到最初高度
final ValueAnimator animator = ValueAnimator.ofInt(ivHead.getHeight(), orignalHeight);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int animateValue = (int) animator.getAnimatedValue();
ivHead.getLayoutParams().height = animateValue;
//使布局參數(shù)生效
ivHead.requestLayout();
}
});
animator.setInterpolator(new OvershootInterpolator(3.f));//彈性插值器
animator.setDuration(350);
animator.start();
}
return super.onTouchEvent(ev);
}
最后的視差拖拽效果實(shí)現(xiàn)如下:
透明狀態(tài)欄+TitleBar
視差拖拽效果實(shí)現(xiàn)完成,當(dāng)然離我們最終要的漂漂的效果還有距離雨饺,距離在哪呢钳垮,首先我們沒有TitleBar,再接著呢额港,這個(gè)狀態(tài)欄饺窿,也太丑了!R普丁短荐!
下面首先實(shí)現(xiàn)透明狀態(tài)欄
在Activity setContentView(R.layout.activity_main)之后,我們執(zhí)行下面的代碼叹哭,要注意的是setStatusBarColor這個(gè)方法忍宋,也就是設(shè)置狀態(tài)欄顏色的方法,是API21也就是5.0以后才有的方法风罩,在5.0之前是無法實(shí)現(xiàn)的糠排,不過現(xiàn)在7.0都出來了,5.0之前的機(jī)型應(yīng)該也不多了超升。
/**
* 初始化狀態(tài)欄狀態(tài)
* 設(shè)置Activity狀態(tài)欄透明效果
* 隱藏ActionBar
*/
private void initState() {
//將狀態(tài)欄設(shè)置成透明色
UIUtils.setBarColor(this, Color.TRANSPARENT);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
}
/**
* 設(shè)置狀態(tài)欄背景色
* 4.4以下不處理
* 4.4使用默認(rèn)沉浸式狀態(tài)欄
* @param color
*/
public static void setBarColor(Activity activity, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window win = activity.getWindow();
View decorView = win.getDecorView();
win.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);//沉浸式狀態(tài)欄
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//android5.0及以上才有透明效果
win.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);//清除flag
//讓應(yīng)用的主體內(nèi)容占用系統(tǒng)狀態(tài)欄的空間
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | option);
win.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
win.setStatusBarColor(color);//設(shè)置狀態(tài)欄背景色
}
}
}
辣么我們現(xiàn)在的效果如何呢入宦?
嚯,已經(jīng)成功把狀態(tài)欄變透明了室琢。
接下來看看TitleBar乾闰,由于我們實(shí)際上是將整個(gè)應(yīng)用沾滿整個(gè)屏幕,也就是說App應(yīng)用主體實(shí)際上占用了狀態(tài)欄的空間并且狀態(tài)欄背景設(shè)置成了透明盈滴,所以實(shí)現(xiàn)了現(xiàn)在這種應(yīng)用作為狀態(tài)欄背景的效果涯肩。在應(yīng)用沒有占據(jù)全屏的情況下,布局應(yīng)該是從狀態(tài)欄之下開始布局的巢钓,但是現(xiàn)在應(yīng)用實(shí)際上是從屏幕(0,0)開始布局的病苗,所以在實(shí)際應(yīng)用中,TitleBar的高度應(yīng)該是設(shè)置為狀態(tài)欄高度+原本期望TitleBar的高度症汹。
下面貼上TitleBar代碼
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tb_title"
android:layout_width="match_parent"
android:layout_height="90dp">
<Button
android:id="@+id/btn_back"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:background="@mipmap/back"/>
<RelativeLayout
android:id="@+id/rl_title"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginTop="43dp"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_title"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_toLeftOf="@+id/tv_title"
android:layout_marginRight="5dp"
android:layout_marginTop="2dp"
android:src="@mipmap/refesh"
android:visibility="invisible"/>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="80dp"
android:background="@android:color/transparent"
android:text="Title"
android:layout_centerInParent="true"
android:textColor="@android:color/white"
android:textSize="20sp"/>
</RelativeLayout>
<Button
android:id="@+id/btn_add"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginRight="20dp"
android:layout_marginTop="10dp"
android:background="@mipmap/add_white"/>
</LinearLayout>
最后將TitleBar和ListView放在一個(gè)FrameLayout中硫朦,界面上的布局,基本完成背镇。
狀態(tài)欄+TitleBar顏色動(dòng)態(tài)漸變
基本界面已經(jīng)實(shí)現(xiàn)完成咬展,接下來我們看看怎么實(shí)現(xiàn)狀態(tài)欄和TitleBar顏色漸變泽裳。前面我們說了,TitleBar和ListView是放在一個(gè)FrameLayout中的破婆。所以思路應(yīng)該很明確了涮总,就是在這個(gè)FrameLayout中動(dòng)態(tài)的設(shè)置TitleBar的背景色,由于狀態(tài)欄實(shí)際是透明背景然后被TitleBar充滿的荠割,所以實(shí)際上我們這里說的狀態(tài)欄+TitleBar顏色動(dòng)態(tài)漸變其實(shí)單修改TitleBar的背景色就可以了。
首先我們實(shí)現(xiàn)一個(gè)自定義GradientLayout 旺矾,在GradientLayout中 給ParallaxListView設(shè)置一個(gè)OnScrollListener 蔑鹦,將根據(jù)ParallaxListView滑動(dòng)的距離和預(yù)設(shè)值求出一個(gè)fraction值,然后根據(jù)fraction和估值器計(jì)算出顏色值并且設(shè)置給TitleBar達(dá)到動(dòng)態(tài)更新TitleBar和狀態(tài)欄顏色的效果箕宙。
由于TitlBar右上角的添加按鈕需要根據(jù)滑動(dòng)距離更新背景嚎朽,所以這里我們增加一個(gè)接口OnGradientStateChangeListenr ,TitleBar實(shí)現(xiàn)這個(gè)接口柬帕,然后根據(jù)GradientLayout傳過去的fraction值以及關(guān)鍵值來更新按鈕"+"的狀態(tài):
public class GradientLayout extends FrameLayout implements OnScrollListener {
private TitleBar tb_title;
private ParallaxListView plv;
private static final float CRITICAL_VALUE = 0.5f;
private OnGradientStateChangeListenr onGradientStateChangeListenr;
private Context context;
/**
* 設(shè)置Gradient狀態(tài)監(jiān)聽
* @param onGradientStateChangeListenr
*/
public void setOnGradientStateChangeListenr(OnGradientStateChangeListenr onGradientStateChangeListenr){
this.onGradientStateChangeListenr = onGradientStateChangeListenr;
}
public GradientLayout(Context context) {
this(context, null);
}
public GradientLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GradientLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 2) {
throw new IllegalArgumentException("only can 2 child in this view");
} else {
if (getChildAt(0) instanceof ParallaxListView) {
plv = (ParallaxListView) getChildAt(0);
plv.setOnScrollListener(this);
} else {
throw new IllegalArgumentException("child(0) must be ParallaxListView");
}
tb_title = (TitleBar) getChildAt(1);
tb_title.setTitleBarListenr(this);
}
}
/**
* 設(shè)置title背景色
*
* @param color
*/
public void setTitleBackground(int color) {
tb_title.setBackgroundColor(color);
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int
totalItemCount) {
if (firstVisibleItem == 0) {
View headView = view.getChildAt(0);
if (headView != null) {
//如果上滑超過headView高度值一半+title高度哟忍,開啟伴隨動(dòng)畫
float slideValue = Math.abs(headView.getTop()) - headView.getHeight() / 2.f +
tb_title.getHeight();
if (slideValue < 0)
slideValue = 0;
float fraction = slideValue / (headView.getHeight() / 2.f);
if (fraction > 1) {
fraction = 1;
}
//Log.e("tag", "fraction = " + fraction);
excuteAnim(fraction);
}
} else {
float fraction = 1;
excuteAnim(fraction);
}
}
private void excuteAnim(float fraction) {
int color = (int) ColorUtil.evaluateColor(fraction, Color.parseColor("#0000ccff"), Color
.parseColor("#ff00ccff"));
setTitleBackground(color);
onGradientStateChangeListenr.onChange(fraction, CRITICAL_VALUE);
}
/**
* 設(shè)置TitleBar text
* @param msg
*/
public void setTitleText(String msg){
tb_title.setTitleText(msg);
}
/**
* Gradient變化臨界值監(jiān)聽
*/
public interface OnGradientStateChangeListenr{
/**
* 當(dāng)fraction超過臨界值時(shí)回調(diào)
* @param fraction
* @param criticalValue
*/
public void onChange(float fraction, float criticalValue);
}
}
TitleBar實(shí)現(xiàn)OnGradientStateChangeListenr
/**
* 設(shè)置Gradient臨界值監(jiān)聽
*
* @param gl
*/
public void setTitleBarListenr(GradientLayout gl) {
gl.setOnGradientStateChangeListenr(new OnGradientStateChangeListenr() {
@Override
public void onChange(float fraction, float criticalValue) {
/**
* 當(dāng)變化值超過臨界值
*/
if (fraction >= criticalValue) {
btn_add.setBackgroundResource(R.mipmap.add_trans);
} else {
btn_add.setBackgroundResource(R.mipmap.add_white);
}
}
})
}
至此我們的效果如下:
下拉加載更多
感覺現(xiàn)在基本已經(jīng)像一個(gè)比較靠譜的demo了,現(xiàn)在繼續(xù)增加下拉加載更多的功能陷寝。其實(shí)有了前面的鋪墊锅很,下拉加載實(shí)現(xiàn)起來其實(shí)非常簡單。
首先在ParallaxListView監(jiān)聽下拉拖拽的距離凤跑,然后在松手的時(shí)候根據(jù)拖拽距離計(jì)算出是否出發(fā)加載更多爆安,最后通過接口回調(diào)的方式將這個(gè)下拉刷新的狀態(tài)以及結(jié)果通知給GradientLayout,GradientLayout又通過接口回調(diào)的方式通知TitleBar更新界面仔引。不多說扔仓,直接上代碼,要注意的一點(diǎn)是咖耘,為了獨(dú)立開ParallaxListView和TitleBar翘簇,ParallaxListView和TitleBar的狀態(tài)更新全部通過父Layout GradientLayout。
ParallaxListView增加刷新接口以及模擬請求數(shù)據(jù)
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_UP) {
//如果松手時(shí)headView滑動(dòng)的距離大于預(yù)設(shè)值儿倒,回調(diào)onRefesh
//Log.e("tag", "ivHead.getHeight() = " + ivHead.getHeight());
//Log.e("tag", "orignalHeight = " + orignalHeight);
if (ivHead.getHeight() - orignalHeight > 60) {
if(onRefeshChangeListener != null){
onRefeshChangeListener.onListRefesh();
if(!isRefeshing){//當(dāng)前不是刷新狀態(tài)時(shí)
getData();
isRefeshing = true;
}
}
}
//放手的時(shí)候講imageHead的高度緩慢從當(dāng)前高度恢復(fù)到最初高度
final ValueAnimator animator = ValueAnimator.ofInt(ivHead.getHeight(), orignalHeight);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int animateValue = (int) animator.getAnimatedValue();
ivHead.getLayoutParams().height = animateValue;
//使布局參數(shù)生效
ivHead.requestLayout();
}
});
animator.setInterpolator(new OvershootInterpolator(3.f));//彈性插值器
animator.setDuration(350);
animator.start();
}
return super.onTouchEvent(ev);
}
/**
* 開啟一個(gè)線程模擬網(wǎng)絡(luò)請求操作
*/
private void getData(){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//test
onRefeshChangeListener.onListRefeshFinish(true);
isRefeshing = false;
//onRefeshChangeListener.onRefeshFinish(false);
}
}).start();
}
GradientLayout實(shí)現(xiàn)ParallaxListView.OnRefeshChangeListener并且新增一個(gè)OnRefeshChangeListener接口用于將狀態(tài)給TitleBar版保,實(shí)際上GradientLayout相當(dāng)于ParallaxListView和TitleBar的傳話者。
@Override
public void onListRefesh() {
onRefeshChangeListener.onListRefesh();
}
@Override
public void onListRefeshFinish(final boolean isRefeshSuccess) {
UIUtils.runOnUIThread(new Runnable() {
@Override
public void run() {
if(isRefeshSuccess){
//Toast.makeText(UIUtils.getContext(), "refesh success.", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(UIUtils.getContext(), "refesh failed.", Toast.LENGTH_SHORT).show();
}
}
});
//不論刷新成功還是失敗夫否,都要通知titleBar刷新完成
onRefeshChangeListener.onListRefeshFinish();
}
/**
* GradientLayout中的子list列表刷新狀態(tài)監(jiān)聽
*/
public interface OnRefeshChangeListener{
/**
* 開始刷新列表找筝,請求數(shù)據(jù)
*/
void onListRefesh();
/**
* 刷新列表完成
*/
void onListRefeshFinish();
}
TitleBar實(shí)現(xiàn)父Layout的接口,然后通過一個(gè)Tween動(dòng)畫實(shí)現(xiàn)刷新進(jìn)度圈圈的旋轉(zhuǎn):
/**
* 設(shè)置TitleBar監(jiān)聽
*
* @param gl
*/
public void setTitleBarListenr(GradientLayout gl) {
gl.setOnGradientStateChangeListenr(new OnGradientStateChangeListenr() {
@Override
public void onChange(float fraction, float criticalValue) {
/**
* 當(dāng)變化值超過臨界值
*/
if (fraction >= criticalValue) {
btn_add.setBackgroundResource(R.mipmap.add_trans);
} else {
btn_add.setBackgroundResource(R.mipmap.add_white);
}
}
});
gl.setOnRefeshChangeListener(new GradientLayout.OnRefeshChangeListener() {
@Override
public void onListRefesh() {
UIUtils.runOnUIThread(new Runnable() {
@Override
public void run() {
iv_title.setVisibility(View.VISIBLE);
//執(zhí)行動(dòng)畫
Animation anim = AnimationUtils.loadAnimation(context, R.anim.refesh_roate);
anim.setInterpolator(new LinearInterpolator());
iv_title.startAnimation(anim);
}
});
}
@Override
public void onListRefeshFinish() {
UIUtils.runOnUIThread(new Runnable() {
@Override
public void run() {
iv_title.setVisibility(View.INVISIBLE);
iv_title.clearAnimation();
}
});
}
});
}
現(xiàn)在再看看我們的效果慷吊,淚流滿面袖裕,終于實(shí)現(xiàn)大部分效果了!
點(diǎn)擊按鈕∨彈出PopupWindow list選項(xiàng)+模糊背景效果
接下來要實(shí)現(xiàn)的是QQ空間好友動(dòng)態(tài)列表選項(xiàng)彈出的效果溉瓶,QQ是彈出一個(gè)屏幕等寬的列表急鳄。我們這里實(shí)現(xiàn)的稍微跟QQ的有點(diǎn)不一樣谤民,我們這里實(shí)現(xiàn)的效果更像是3D touch的效果。
先來擼一擼思路疾宏,既然是彈出來张足,首相第一個(gè)想到的實(shí)現(xiàn)方法,當(dāng)然是PopupWindow坎藐,然后背景虛化为牍,其實(shí)網(wǎng)上也有很多的模糊虛化方法,然后再接著就是將我們要添加的View設(shè)到屏幕上岩馍。OK碉咆,思路很清晰簡單,然鵝蛀恩,真的辣么簡單嗎疫铜?
并沒有啊K弧?枪尽!一開始就出點(diǎn)了小意外顽馋,就是關(guān)于WindowManager.LayoutParams谓厘,由于這玩意的flag值實(shí)在是太多了,網(wǎng)上這類功能相關(guān)的資料又比較少寸谜,最后好一番折騰庞呕,總算是實(shí)現(xiàn)了我們要的效果,也就是虛化背景不滿全屏程帕,但是不知道為什么住练,模擬器狀態(tài)欄依然顯示的是半透明狀態(tài)欄,好在真機(jī)上運(yùn)行都一切正常愁拭,然后就妥妥的無視模擬器這個(gè)問題了讲逛。
先看看我們配置的WindowManager.LayoutParams,這里只列出來我們用到的幾個(gè)flag值岭埠,折騰了小半天盏混,最后也就用到這么幾個(gè),委屈的不行惜论,哈哈许赃。
private void initParams() {
params = new WindowManager.LayoutParams();
params.width = MATCH_PARENT;
params.height = MATCH_PARENT;
params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN //布滿屏幕,忽略狀態(tài)欄
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS //透明狀態(tài)欄
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; //透明虛擬按鍵欄
params.format = PixelFormat.TRANSLUCENT;//支持透明
params.gravity = Gravity.LEFT | Gravity.TOP;
}
接下來要思考的是將listView塞進(jìn)一個(gè)空layout馆类,這個(gè)地方要注意的是混聊,由于我們這里彈出的listView背景是一個(gè).9圖片峰搪,所以一定要記住將這個(gè).9圖片設(shè)置個(gè)listView做背景<致!!而不是設(shè)置給我們的空layout7钋骸J邸笋庄!
由于listView寬度我們希望是自適應(yīng)而不是充滿屏幕彤灶,所以我們要自定義一個(gè)listView,并且根據(jù)item的最大寬度設(shè)置listView的寬度展懈,下面貼上自定義listView的代碼销睁。
public class PopupListView extends ListView {
private Context context;
public PopupListView(Context context) {
this(context, null);
}
public PopupListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PopupListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int maxWidth = measureWidthByChilds() + getPaddingLeft() + getPaddingRight();
super.onMeasure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.UNSPECIFIED),
heightMeasureSpec);//注意,這個(gè)地方一定是MeasureSpec.UNSPECIFIED
}
public int measureWidthByChilds() {
int maxWidth = 0;
View view = null;
for (int i = 0; i < getAdapter().getCount(); i++) {
view = getAdapter().getView(i, view, this);
view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
if (view.getMeasuredWidth() > maxWidth) {
maxWidth = view.getMeasuredWidth();
}
view = null;
}
return maxWidth;
}
}
有一點(diǎn)比較重要存崖,我們在popupWindow彈出來的時(shí)候冻记,需要攔截返回鍵事件,點(diǎn)擊返回鍵時(shí)dismiss掉popupWindow金句,如何攔截返回鍵事件呢檩赢?我們這里通過一個(gè)自定義layout吕嘀,重寫這個(gè)layout的dispatchKeyEvent事件然后暴露一個(gè)接口违寞,實(shí)際上相當(dāng)于對dispatchKeyEvent事件做了一次傳遞,然后在popupWindow中實(shí)現(xiàn)setDispatchKeyEventListener的回調(diào)偶房。
/**
* 攔截WindowManager中view的按鍵事件趁曼,此處主要用于返回鍵事件攔截
* Created by Horrarndoo on 2017/3/28.
*/
public class PopupRootLayout extends FrameLayout{
private DispatchKeyEventListener mDispatchKeyEventListener;
public PopupRootLayout(Context context) {
super(context);
}
public PopupRootLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public PopupRootLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mDispatchKeyEventListener != null) {
return mDispatchKeyEventListener.dispatchKeyEvent(event);
}
return super.dispatchKeyEvent(event);
}
public DispatchKeyEventListener getDispatchKeyEventListener() {
return mDispatchKeyEventListener;
}
public void setDispatchKeyEventListener(DispatchKeyEventListener mDispatchKeyEventListener) {
this.mDispatchKeyEventListener = mDispatchKeyEventListener;
}
//監(jiān)聽接口
public static interface DispatchKeyEventListener {
boolean dispatchKeyEvent(KeyEvent event);
}
}
最后貼上PopupWindow的代碼,設(shè)置虛化背景和彈出/隱藏ListView都是通過屬性動(dòng)畫棕洋,比較簡單挡闰,代碼注釋也比較全,就不多做解釋了掰盘。
public class BlurPopupWindow {
/**
* 頂部彈出popupWindow關(guān)鍵字
*/
public static final int KEYWORD_LOCATION_TOP = 1;
/**
* 點(diǎn)擊處彈出popupWindow關(guān)鍵字
*/
public static final int KEYWORD_LOCATION_CLICK = 2;
private Activity activity;
private WindowManager.LayoutParams params;
private boolean isDisplay;
private WindowManager windowManager;
private PopupRootLayout rootView;
private ViewGroup contentLayout;
private final int animDuration = 250;//動(dòng)畫執(zhí)行時(shí)間
private boolean isAniming;//動(dòng)畫是否在執(zhí)行
/**
* BlurPopupWindow構(gòu)造函數(shù)
*
* @param activity 當(dāng)前彈出/消失BlurPopupWindow的Activity
* @param view 要彈出/消失的view內(nèi)容
* 默認(rèn)從點(diǎn)擊處彈出/消失popupWindow
*/
public BlurPopupWindow(Activity activity, View view) {
initBlurPopupWindow(activity, view, KEYWORD_LOCATION_CLICK);
}
/**
* BlurPopupWindow構(gòu)造函數(shù)
*
* @param activity 當(dāng)前彈出/消失BlurPopupWindow的Activity
* @param view 要彈出/消失的view內(nèi)容
* @param keyword 彈出/消失位置關(guān)鍵字 KEYWORD_LOCATION_TOP:頂部彈出
* KEYWORD_LOCATION_CLICK:點(diǎn)擊位置彈出
*/
public BlurPopupWindow(Activity activity, View view, int keyword) {
initBlurPopupWindow(activity, view, keyword);
}
/**
* BlurPopupWindow初始化
*
* @param activity 當(dāng)前彈出BlurPopupWindow的Activity
* @param view 要彈出/消失的view內(nèi)容
* @param keyword 彈出/消失位置關(guān)鍵字 KEYWORD_LOCATION_TOP:頂部彈出
* KEYWORD_LOCATION_CLICK:點(diǎn)擊位置彈出
*/
private void initBlurPopupWindow(Activity activity, View view, int keyword) {
this.activity = activity;
windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
switch (keyword) {
case KEYWORD_LOCATION_CLICK:
view.setPadding(5, 10, 5, 0);//由于.9圖片有部分是透明摄悯,往下padding 10個(gè)pix,左右padding 5個(gè)pix為了美觀
view.setBackgroundResource(R.drawable.popup_bg);
break;
case KEYWORD_LOCATION_TOP:
ImageView imageView = (ImageView) view;
imageView.setScaleType(ImageView.ScaleType.FIT_START);
imageView.setImageDrawable(activity.getResources().getDrawable(R.mipmap.popup_top_bg));
break;
default:
break;
}
initLayout(view, keyword);
}
private void initLayout(View view, final int keyword) {
rootView = (PopupRootLayout) View.inflate(activity, R.layout.popupwindow_layout, null);
contentLayout = (ViewGroup) rootView.findViewById(R.id.content_layout);
initParams();
contentLayout.addView(view);
//點(diǎn)擊根布局時(shí), 隱藏彈出的popupWindow
rootView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismissPopupWindow(keyword);
}
});
rootView.setDispatchKeyEventListener(new DispatchKeyEventListener() {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (rootView.getParent() != null) {
dismissPopupWindow(keyword);
}
return true;
}
return false;
}
});
}
private void initParams() {
params = new WindowManager.LayoutParams();
params.width = MATCH_PARENT;
params.height = MATCH_PARENT;
params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN //布滿屏幕愧捕,忽略狀態(tài)欄
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS //透明狀態(tài)欄
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; //透明虛擬按鍵欄
params.format = PixelFormat.TRANSLUCENT;//支持透明
params.gravity = Gravity.LEFT | Gravity.TOP;
}
/**
* 將bitmap模糊虛化并設(shè)置給view background
*
* @param view
* @param bitmap
* @return 虛化后的view
*/
private View getBlurView(View view, Bitmap bitmap) {
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth() / 3, bitmap
.getHeight() / 3, false);
Bitmap blurBitmap = UIUtils.getBlurBitmap(activity, scaledBitmap, 5);
view.setAlpha(0);
view.setBackgroundDrawable(new BitmapDrawable(null, blurBitmap));
alphaAnim(view, 0, 1, animDuration);
return view;
}
/**
* 彈出選項(xiàng)彈窗
* 默認(rèn)從點(diǎn)擊位置彈出
*
* @param locationView
*/
public void displayPopupWindow(View locationView) {
displayPopupWindow(locationView, KEYWORD_LOCATION_CLICK);
}
/**
* 彈出選項(xiàng)彈窗
*
* @param locationView 被點(diǎn)擊的view
* @param keyword 彈出位置關(guān)鍵字
*/
public void displayPopupWindow(View locationView, int keyword) {
if (!isAniming) {
isAniming = true;
try {
int[] point = new int[2];
float x = 0;
float y = 0;
contentLayout.measure(0, 0);
switch (keyword) {
case KEYWORD_LOCATION_CLICK:
//得到該view相對于屏幕的坐標(biāo)
locationView.getLocationOnScreen(point);
x = point[0] + locationView.getWidth() - contentLayout.getMeasuredWidth();
y = point[1] + locationView.getHeight();
break;
case KEYWORD_LOCATION_TOP:
x = 0;
y = 0;
break;
default:
break;
}
contentLayout.setX(x);
contentLayout.setY(y);
View decorView = activity.getWindow().getDecorView();
Bitmap bitmap = UIUtils.viewToBitmap(decorView);//將view轉(zhuǎn)成bitmap
View blurView = getBlurView(rootView, bitmap);//模糊圖片
windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
//將處理過的blurView添加到window
windowManager.addView(blurView, params);
//popupWindow動(dòng)畫
popupAnim(contentLayout, 0.f, 1.f, animDuration, keyword, true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 消失popupWindow
* 默認(rèn)從點(diǎn)擊處開始消失
*/
public void dismissPopupWindow() {
dismissPopupWindow(KEYWORD_LOCATION_CLICK);
}
/**
* 消失popupWindow
* @param keyword 消失位置關(guān)鍵字 KEYWORD_LOCATION_TOP:頂部彈出
* KEYWORD_LOCATION_CLICK:點(diǎn)擊位置彈出
*/
public void dismissPopupWindow(int keyword) {
if (!isAniming) {
isAniming = true;
if (isDisplay) {
popupAnim(contentLayout, 1.f, 0.f, animDuration, keyword, false);
}
}
}
/**
* 設(shè)置透明度屬性動(dòng)畫
*
* @param view 要執(zhí)行屬性動(dòng)畫的view
* @param start 起始值
* @param end 結(jié)束值
* @param duration 動(dòng)畫持續(xù)時(shí)間
*/
private void alphaAnim(final View view, int start, int end, int duration) {
ObjectAnimator.ofFloat(view, "alpha", start, end).setDuration(duration).start();
}
/**
* popupWindow屬性動(dòng)畫
*
* @param view
* @param start
* @param end
* @param duration
* @param keyword
* @param isToDisplay 顯示或消失 flag值
*/
private void popupAnim(final View view, float start, final float end, int duration, final int
keyword, final boolean isToDisplay) {
ValueAnimator va = ValueAnimator.ofFloat(start, end).setDuration(duration);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
switch (keyword) {
case KEYWORD_LOCATION_CLICK:
view.setPivotX(view.getMeasuredWidth());
view.setPivotY(0);
view.setScaleX(value);
view.setScaleY(value);
view.setAlpha(value);
break;
case KEYWORD_LOCATION_TOP:
view.setPivotX(0);
view.setPivotY(0);
view.setScaleY(value);
break;
default:
break;
}
}
});
va.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAniming = false;
if(isToDisplay) {//當(dāng)前為彈出popupWindow
isDisplay = true;
onPopupStateListener.onDisplay(isDisplay);
}else{//當(dāng)前為消失popupWindow
try {
if (isDisplay) {
windowManager.removeViewImmediate(rootView);
}
} catch (Exception e) {
e.printStackTrace();
}
isDisplay = false;
onPopupStateListener.onDismiss(isDisplay);
}
}
});
va.start();
}
}
現(xiàn)在看看實(shí)現(xiàn)效果奢驯。
點(diǎn)擊按鈕+頂部彈出PopupWindow界面+模糊背景效果
接下來的是最難的一個(gè)地方!4位妗瘪阁!
并不是!S寿恕管跺!騙你的!:探豁跑!哈哈,實(shí)際上前面的代碼也已經(jīng)寫的很清楚了泻云,我們這個(gè)頂部彈出的這個(gè)界面是個(gè)什么東西呢贩绕?沒錯(cuò);鸬摹!淑倾!就是一個(gè)ImageViewA蠛住!娇哆!鵝已E壤邸!碍讨!
OK治力,玩笑開完。要注意一點(diǎn)就是ImageView應(yīng)避免設(shè)置background而是應(yīng)該設(shè)置src勃黍,因?yàn)樵O(shè)置background可能會(huì)因?yàn)閳D片比例導(dǎo)致圖片拉伸失真宵统,當(dāng)然QQ頂部彈下來的肯定不是一個(gè)ImageView,這里也只是做一個(gè)效果覆获,實(shí)際應(yīng)用中自然可以根據(jù)需求去拓展马澈。
最后定義一個(gè)接口OnPopupStateListener 用于將PopupWindow狀態(tài)告知給TitleBar,然后TitleBar按鍵根據(jù)回調(diào)狀態(tài)給按鈕“+”設(shè)置屬性動(dòng)畫弄息。
/**
* popupWindow顯示和消失狀態(tài)變化接口
*/
public interface OnPopupStateListener {
/**
* popupWindow狀態(tài)變化
* @param isDisplay popupWindow當(dāng)前狀態(tài) true:顯示 false:消失
*/
// void onChange(boolean isDisplay);
/**
* popupWindow為顯示狀態(tài)
*/
void onDisplay(boolean isDisplay);
/**
* popupWindow為消失狀態(tài)
*/
void onDismiss(boolean isDisplay);
}
TitleBar 接口實(shí)現(xiàn)以及按鈕動(dòng)畫
private void initPopupWindow(final Activity context) {
ImageView iv_popup_top = new ImageView(context);
LayoutParams params = new LayoutParams(LayoutParams
.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
iv_popup_top.setLayoutParams(params);
blurPopupWindow = new BlurPopupWindow(context, iv_popup_top,
KEYWORD_LOCATION_TOP);
blurPopupWindow.setOnPopupStateListener(new BlurPopupWindow.OnPopupStateListener() {
@Override
public void onDisplay(boolean isDisplay) {
TitleBar.this.isDisplay = isDisplay;
}
@Override
public void onDismiss(boolean isDisplay) {
TitleBar.this.isDisplay = isDisplay;
dismissAnim();
}
});
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_add:
if (onBarClicklistener != null) {
if (isDisplay) {
dismissPopupWindow();
} else {
displayPopupWindow(v);
}
onBarClicklistener.onBarClick(R.id.btn_add);
}
break;
case R.id.btn_back:
if (onBarClicklistener != null) {
onBarClicklistener.onBarClick(R.id.btn_back);
}
break;
}
}
public void displayPopupWindow(View v) {
displayAnim();
blurPopupWindow.displayPopupWindow(v, KEYWORD_LOCATION_TOP);
}
public void dismissPopupWindow() {
dismissAnim();
blurPopupWindow.dismissPopupWindow(KEYWORD_LOCATION_TOP);
}
/**
* Add按鈕逆時(shí)針轉(zhuǎn)90度
*/
private void displayAnim() {
ObjectAnimator.ofFloat(btn_add, "rotation", 0.f, -90.f).setDuration(500).start();
}
/**
* Add按鈕瞬時(shí)間轉(zhuǎn)90度
*/
private void dismissAnim() {
ObjectAnimator.ofFloat(btn_add, "rotation", 0.f, 90.f).setDuration(500).start();
}
貼上最終模擬器上運(yùn)行的效果
最后附上完整demo地址:https://github.com/Horrarndoo/parallaxListView