編輯:關於Android編程
本文結構
1、功能介紹 2、總體設計 3、詳細設計 4、MaterialList自定義布局 5、總結MaterialList是一個幫助Android開發者獲取漂亮CardView的Android庫,通過這個庫你可以很容易實現具有Material Design風格的ListView,MaterialList中內置了7種類型的CardView,
首先,在你的layout中聲明一個MaterialListView:
接著,綁定MaterialListView到一個變量,並設置Item動畫
mListView = (MaterialListView) findViewById(R.id.material_listview);
mListView.setItemAnimator(new SlideInLeftAnimator());
mListView.getItemAnimator().setAddDuration(300);
mListView.getItemAnimator().setRemoveDuration(300);
然後設置dismiss監聽和ItemTouchListener
// Set the dismiss listener
mListView.setOnDismissCallback(new OnDismissCallback() {
@Override
public void onDismiss(@NonNull Card card, int position) {
// Show a toast
Toast.makeText(mContext, "You have dismissed a " + card.getTag(), Toast.LENGTH_SHORT).show();
}
});
// Add the ItemTouchListener
mListView.addOnItemTouchListener(new RecyclerItemClickListener.OnItemClickListener() {
@Override
public void onItemClick(@NonNull Card card, int position) {
Log.d("CARD_TYPE", "" + card.getTag());
}
@Override
public void onItemLongClick(@NonNull Card card, int position) {
Log.d("LONG_CLICK", "" + card.getTag());
}
});
最後添加Card
mListView.getAdapter().addAtStart(new Card.Builder(this)
.setTag("BASIC_IMAGE_BUTTONS_CARD")
.setDismissible()
.withProvider(new CardProvider())
.setLayout(R.layout.material_basic_image_buttons_card_layout)
.setTitle("Hi there")
.setDescription("I've been added on top!")
.addAction(R.id.left_text_button, new TextViewAction(this)
.setText("left")
.setTextResourceColor(R.color.black_button))
.addAction(R.id.right_text_button, new TextViewAction(this)
.setText("right")
.setTextResourceColor(R.color.orange_button))
.setDrawable(R.drawable.dog)
.endConfig()
.build());
通過給Provider設置不同的layout,從而獲取不同的CardView.
通過設置ListCardProvider和R.layout.material_list_card_layout,可以實現帶listView的CardView.
以上是 MaterialList的主要類的關系圖,跟總體設計中介紹的一樣大致分為四部分。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMyBpZD0="32-核心功能介紹">3.2 核心功能介紹
final CardProvider provider = new Card.Builder(this)
.setTag("WELCOME_CARD")
.setDismissible()
.withProvider(new CardProvider())
.setLayout(R.layout.material_welcome_card_layout)
.setTitle("Welcome Card")
.setTitleColor(Color.WHITE)
.setDescription("I am the description")
.setDescriptionColor(Color.WHITE)
.setSubtitle("My subtitle!")
.setSubtitleColor(Color.WHITE)
.setBackgroundColor(Color.BLUE)
.addAction(R.id.ok_button, new WelcomeButtonAction(this)
.setText("Okay!")
.setTextColor(Color.WHITE)
.setListener(new OnActionClickListener() {
@Override
public void onActionClicked(View view, Card card) {
Toast.makeText(mContext, "Welcome!", Toast.LENGTH_SHORT).show();
}
}));
我們通過觀察上面的代碼,首先我們通過new Card.Builder(this)獲取Builder實例,接著設置setDismissible(),將屬性設置為mDismissible,使它可以移除。
@NonNull
public Builder setDismissible() {
mDismissible = true;
return this;
}
然後我們通過withProvider(new CardProvider())注入CardProvider實例,此時將獲得CardProvider實例,我們通過這個實例設置ItemView的color,layout,title,Action,最後通過provider.endConfig().build()我們就能獲取實例Card,整個組合過程就是標准的Builder模式,不過Card就像一個空殼,真正持有ItemView核心屬性的是CardProvider。
接著我們回到withProvider()和setLayout()這兩個方法,我們知道獲取不同風格的CardView就是通過設置不同的layout和CardProvider來實現,但這個布局視圖控件是怎樣的添加和初始化的呢?
首先我們看MaterialListAdapter的內部類ViewHolder,ViewHolder在類中關聯了CardLayout。
public static class ViewHolder extends RecyclerView.ViewHolder {
private final CardLayout view;
public ViewHolder(@NonNull final View v) {
super(v);
view = (CardLayout) v;
}
public void build(Card card) {
view.build(card);//注冊觀察者
}
}
接著MaterialListAdapter繼承RecyclerView.Adapter並重寫它的兩個方法getItemViewType() onBindViewHolder(),分別綁定LayoutId和綁定holder,並對cardLayout進行初始化。
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.build(getCard(position));//綁定holder,並對cardLayout進行初始化
}
@Override
public int getItemViewType(final int position) {//綁定LayoutId
return mCardList.get(position).getProvider().getLayout();
}
我們看到onBindViewHolder()中調用了holder.build(),holder.build()內又調用了CardLayout的build(),build()內代碼如下:
public void build(@NonNull final Card card) {
mCard = card;
if (!mObserves) {
mCard.getProvider().addObserver(this);//添加觀察者
mObserves = true;
}
mCard.getProvider().render(this, card);//Card視圖布局初始化
}
我們可以看到,實際上build()是調用了CardProvider的render()方法來對Card視圖布局進行初始化,render()的核心代碼如下:
public void render(@NonNull final View view, @NonNull final Card card) {
// card的背景
final CardView cardView = findViewById(view, R.id.cardView, CardView.class);
if (cardView != null) {
cardView.setCardBackgroundColor(getBackgroundColor());
}
// 標題
final TextView title = findViewById(view, R.id.title, TextView.class);
if (title != null) {
title.setText(getTitle());
title.setTextColor(getTitleColor());
title.setGravity(getTitleGravity());
}
// 副標題
final TextView subtitle = findViewById(view, R.id.subtitle, TextView.class);
if (subtitle != null) {
subtitle.setText(getSubtitle());
subtitle.setTextColor(getSubtitleColor());
subtitle.setGravity(getSubtitleGravity());
if (getSubtitle() == null || getSubtitle().isEmpty()) {
subtitle.setVisibility(View.GONE);
} else {
subtitle.setVisibility(View.VISIBLE);
}
}
// 描述內容
final TextView supportingText = findViewById(view, R.id.supportingText, TextView.class);
if (supportingText != null) {
supportingText.setText(getDescription());
supportingText.setTextColor(getDescriptionColor());
supportingText.setGravity(getDescriptionGravity());
}
// 圖片
final ImageView imageView = findViewById(view, R.id.image, ImageView.class);
if (imageView != null) {
if (getDrawable() != null) {
imageView.setImageDrawable(getDrawable());
} else {
final RequestCreator requestCreator = Picasso.with(getContext())
.load(getImageUrl());
if (getOnImageConfigListenerListener() != null) {
getOnImageConfigListenerListener().onImageConfigure(requestCreator);
}
requestCreator.into(imageView);
}
}
// Divider
final View divider = findViewById(view, R.id.divider, View.class);
if (divider != null) {
divider.setVisibility(isDividerVisible() ? View.VISIBLE : View.INVISIBLE);
// 如果可見, 將設置分割線的 params
if (isDividerVisible()) {
final ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)
divider.getLayoutParams();
if (isFullWidthDivider()) {
params.setMargins(0, 0, 0, 0);
} else {
int dividerMarginPx = dpToPx(DIVIDER_MARGIN_DP);
params.setMargins(
dividerMarginPx,
0,
dividerMarginPx,
0
);
}
}
}
// Actions
for (final Map.Entry entry : mActionMapping.entrySet()) {
final View actionViewRaw = findViewById(view, entry.getKey(), View.class);
if (actionViewRaw != null) {
final Action action = entry.getValue();
action.setProvider(this);
action.onRender(actionViewRaw, card);
}
}
}
上面代碼中render()主要完成layout中控件的初始化,和初始化Action,並調用Action的抽象方法onRender(),而Action的子類TextAction,實現了onRender(),如下:
@Override
protected void onRender(@NonNull final View view, @NonNull final Card card) {
TextView textView = (TextView) view;
textView.setText(mActionText != null ? mActionText.toUpperCase(Locale.getDefault()) : null);
textView.setTextColor(mActionTextColor);
textView.setOnClickListener(new View.OnClickListener() {//設置單擊監聽
@Override
public void onClick(View v) {
if(mListener != null) {
mListener.onActionClicked(view, card);//回調OnActionClickListener的onActionClicked()
}
}
});
}
TextAction在onRender()中獲取textview,並設置了單擊事件監聽,在onClick()中回調OnActionClickListener的onActionClicked()方法,這樣使Layout上View只要設置了Action就能添加單擊事件回調。通過上面的流程CardLayout將作為MaterialListView的ItemView,並布局到界面上。
MaterialList可以通過左右滑動移除ItemView,這個功能是很實用的,這個效果是如何實現呢?
首先我們將MaterialListView作為切入口,MaterialListView繼承RecyclerView,它通過setOnTouchListener(mDismissListener)設置了TouchListener,這個mDismissListener就是SwipeDismissRecyclerViewTouchListener,它繼承View.OnTouchListener類,並重寫了onTouch(),以下是onTouch()的核心代碼:
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (mViewWidth < 2) {
mViewWidth = mRecyclerView.getWidth();
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
if (mPaused) {
return false;
}
// TODO: ensure this is a finger, and set a flag
// 找到被觸控的child view(執行命中測試)
Rect rect = new Rect();
int childCount = mRecyclerView.getChildCount();
int[] listViewCoords = new int[2];
mRecyclerView.getLocationOnScreen(listViewCoords);//一個控件在其整個屏幕上左上點的坐標,保存到listViewCoords中,以屏幕左上點為原點
int x = (int) motionEvent.getRawX() - listViewCoords[0];
int y = (int) motionEvent.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++) {//遍歷child view
child = mRecyclerView.getChildAt(i);
child.getHitRect(rect);//得到rect,它有child view的左上點坐標(left,top)和右下點坐標(right,bottom)
if (rect.contains(x, y)) {//判斷命中點(x,y)是否在ret中,如果是則說明該點落在這個child view上,
mDownView = child;
break;
}
}
if (mDownView != null) {
mDownX = motionEvent.getRawX();
mDownY = motionEvent.getRawY();
mDownPosition = mRecyclerView.getChildPosition(mDownView);
if (mCallbacks.canDismiss(mDownPosition)) {//設置該mDownView可以滑動移除
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(motionEvent);//對觸摸進行速度跟蹤
} else {
mDownView = null;
}
}
return false;
}
case MotionEvent.ACTION_CANCEL: {
if (mVelocityTracker == null) {
break;
}
if (mDownView != null && mSwiping) {
// cancel
animate(mDownView)
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
mDownX = 0;
mDownY = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
}
case MotionEvent.ACTION_UP: {
if (mVelocityTracker == null) {
break;
}
float deltaX = motionEvent.getRawX() - mDownX;
mVelocityTracker.addMovement(motionEvent);
mVelocityTracker.computeCurrentVelocity(1000);//1s內的速度
float velocityX = mVelocityTracker.getXVelocity();//x軸的1s內的速度
float absVelocityX = Math.abs(velocityX);//返回絕對值
float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());
boolean dismiss = false;
boolean dismissRight = false;
if (Math.abs(deltaX) > mViewWidth / 2 && mSwiping) {//x軸方向滑動的距離大於child寬同時mSwiping==true
dismiss = true;
dismissRight = deltaX > 0;
} else if (mMinFlingVelocity <= absVelocityX && absVelocityX <= mMaxFlingVelocity
&& absVelocityY < absVelocityX && mSwiping) {
//只有在同一個方向進行扔或拖是,dismiss才有效的
dismiss = (velocityX < 0) == (deltaX < 0);
dismissRight = mVelocityTracker.getXVelocity() > 0;
}
if (dismiss && mDownPosition != ListView.INVALID_POSITION) {
// 移除
final View downView = mDownView; // mDownView gets null'd before animation ends
final int downPosition = mDownPosition;
++mDismissAnimationRefCount;
animate(mDownView)
.translationX(dismissRight ? mViewWidth : -mViewWidth)
.alpha(0)
.setDuration(mAnimationTime)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
performDismiss(downView, downPosition);//在動畫結束時,移除downView
}
});
} else {
// 取消
animate(mDownView)
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
mDownX = 0;
mDownY = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (mVelocityTracker == null || mPaused) {
break;
}
mVelocityTracker.addMovement(motionEvent);
float deltaX = motionEvent.getRawX() - mDownX;
float deltaY = motionEvent.getRawY() - mDownY;
if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < Math.abs(deltaX) / 2) {//x軸方向滑動的距離大於最小有效滑動距離,同時x軸方向滑動的距離的二分之一大於Y軸方向滑動的距離,則對事件進行攔截
mSwiping = true;
mSwipingSlop = (deltaX > 0 ? mSlop : -mSlop);
mRecyclerView.requestDisallowInterceptTouchEvent(true);//解決滑動沖突,內部攔截法
// Cancel ListView's touch (un-highlighting the item)
MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL |(motionEvent.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT));
mRecyclerView.onTouchEvent(cancelEvent);
cancelEvent.recycle();
}
if (mSwiping) {//播放動畫,child view沿滑動x軸方向移動,並設置alpha
setTranslationX(mDownView, deltaX - mSwipingSlop);
setAlpha(mDownView, Math.max(0f, Math.min(1f,
1f - 2f * Math.abs(deltaX) / mViewWidth)));
return true;
}
break;
}
}
return false;
}
我們知道一個完整的滑動事件,是先ACTION_DOWN,接著ACTION_MOVE,最後才ACTION_UP,我們將從這三方面分析onTouch(),
首先在ACTION_DOWN中我們獲取觸控點坐標,接著我們遍歷mRecyclerView的childView,計算出這個觸控點落在哪一個childView上,從而獲取downView和position 然後在ACTION_MOVE中我們通過判斷觸控點在x軸方向滑動的距離大於最小有效滑動距離和x軸方向滑動的距離的二分之一大於Y軸方向滑動的距離,則設置mSwiping = true,並對事件進行攔截,同時播放屬性動畫,使mDownView沿滑動x軸方向移動,並設置alpha 最後在ACTION_UP中通過mVelocityTracker.getXVelocity()獲取觸控點1s內在x軸方向的速度absVelocityX,如果absVelocityX大於等於最小速度值和小於或等於最大速度值,absVelocityX大於y軸方向速度,且mSwiping為true,這些條件都滿足的話,則設置dismiss = true;或是如果x軸方向滑動的距離大於二分之一downView寬時,且mSwiping==true,則設置dismiss = true。接下來我們設置dismiss動畫,並在動畫結束時,調用performDismiss()。而我們downView移除的時候下面的view會慢慢的往上面頂,填補這個空白,這個是如何做到的呢?核心就是performDismiss()內設置的屬性動畫,其核心代碼如下:
private void performDismiss(final View dismissView, final int dismissPosition) {
final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
final int originalHeight = dismissView.getHeight();
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);//設置逐漸originalHeight到1
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//省略代碼
mCallbacks.onDismiss(mRecyclerView, dismissPositions);
//回調onDismiss()來將downView從adapter中移除
//省略代碼
// 發送一個 cancel event,來釋放資源
long time = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(time, time,
MotionEvent.ACTION_CANCEL, 0, 0, 0);
mRecyclerView.dispatchTouchEvent(cancelEvent);
mPendingDismisses.clear();
}
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();
dismissView.setLayoutParams(lp);
}
});
我們從上面的代碼可以看出通過ValueAnimator來逐漸將downView的高度從originalHeight到1,實現下面的view慢慢往上頂的動畫效果。在動畫結束時,會調用onDismiss()來將downView從adapter中移除,同時發送一個 cancel event,重置參數和釋放資源。
從3.1的類關系圖我們可以知道MaterialListAdapter和CardLayout都實現了Observer接口,CardProvider繼承Observable類,CardProvider注冊觀察者過程如下,首先在MaterialListAdapter 中的onBindViewHolder(),代碼如下:
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.build(getCard(position));
}
holder會調用build(),方法內又會調用CardLayout的build(),我們看下面CardLayout的build()的方法代碼
public void build(@NonNull final Card card) {
mCard = card;
if (!mObserves) {
mCard.getProvider().addObserver(this);//注冊CardLayout觀察者
mObserves = true;
}
mCard.getProvider().render(this, card);//Card視圖布局初始化
}
從上面代碼可以看出CardProvider注冊了CardLayout觀察者,而在MaterialListAdapter中的add(),同樣給CardProvider注冊了MaterialListAdapter觀察者。代碼如下:
public void add(final int position, @NonNull final Card card, final boolean scroll) {
mCardList.add(position, card);
card.getProvider().addObserver(this);//注冊MaterialListAdapter觀察者
mItemAnimation.onAddItem(position, scroll);
notifyItemInserted(position); // Triggers the animation!
}
這樣每當我們CardProvider調用setTitle()等設置屬性的方法時,就會調用notifyObservers()時,代碼如下:
public T setTitle(@NonNull final String title) {
mTitle = title;
notifyDataSetChanged();
return (T) this;
}
這樣會觸發CardLayout和MaterialListAdapter的回調函數update()
@Override
//CardLayout重寫的update()
public void update(final Observable observable, final Object data) {
if(data == null) {
build(mCard);//初始化card,接下來調用render()來初始化LayoutId內的控件
((CardProvider) observable).notifyDataSetChanged(getCard());
}
}
@Override
//MaterialListAdapter重寫的update()
public void update(final Observable observable, final Object data) {
if (data instanceof DismissEvent) {
remove(((DismissEvent) data).getCard(), true);//data為DismissEvent類型則移除這個card
}
if (data instanceof Card) {
notifyDataSetChanged();//通知adapter數據已經改變,重新布局繪制MaterialView內的view
}
}
通過這個觀察者模式,我們可以很輕松地在修改CardProvider的Title,TitleColor,Drawable後,實時的在MaterialListView顯示出剛剛的修改。
MaterialList庫有7個layout 的xml文件,已經足夠我們使用了,不過我們可不可以設置自己的XML文件進去呢?回答肯定是可以的,MaterialList就有很好的擴展性,通過本文的第三部分,我們知道了Card的布局控件初始化是在CardProvider的render()完成的,單擊事件的添加是通過Action,這樣我們要實現imageView的單擊事件,就需要通過繼承TextViewAction來事件,詳細文件如下:
layout_new.xml
<framelayout android:layout_height="wrap_content" android:layout_width="match_parent">
</framelayout>
ImageViewAction繼承於TextViewAction,使添加的ImageView可以添加監聽點擊事件
public class ImageViewAction extends TextViewAction{
@Nullable
private OnActionClickListener mListener;
public ImageViewAction(@NonNull Context context) {
super(context);
}
@Override
protected void onRender(@NonNull final View view, @NonNull final Card card) {
ImageView imageView = (ImageView) view;
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mListener != null) {
mListener.onActionClicked(view, card);
}
}
});
}
}
這樣我們就可以構建新的Card
final Card card =new Card.Builder(this)
.setTag("IMAGE_BUTTONS_CARD")
.setDismissible()
.withProvider(new CardProvider())
.setLayout(R.layout.layout_new)
.setTitle("Hi there")
.setDescription("I've been added on top!")
.addAction(R.id.share_text_button, new ImageViewAction(this))
.addAction(R.id.star_text_button, new ImageViewAction(this))
.addAction(R.id.mark_text_button, new ImageViewAction(this))
.setDrawable(R.drawable.photo).endConfig().build();
效果圖如下:
到此為止,整個MaterialList基本分析完了,從介紹使用到分析源碼設計,最後介紹如何擴展,這個過程就像一次次歷險,每一次發現都有不一樣的收獲,今後還會繼續寫這類型的博客,希望對大家有所幫助。
1.在移動設備訪問m.alipay.com時,如果本地安裝了支付寶客戶端,則浏覽器會調用本地客戶端,沒有安裝則會跳轉到下載頁面,提示安裝。剛好有這樣的需求,就分析了下支付
雖然很多同學已經順利入手了魅藍Note3,也根據網上的一些相關資料獲取到了魅藍Note3的Root權限,但是在使用一些修改類的軟件時候依舊會碰到提示該設備未
1、首先來創建一個Activity,在Activity的OnCreate函數裡面我們設置它為全屏,然後設置Activity的寬高為全屏*0.9,然後設置背景圖片為半透明的
本文實例講述了Android實現可使用自定義透明Dialog樣式的Activity。分享給大家供大家參考,具體如下:有時你需要一個對話框,但同時對話框中的內容有更多控制和