android 近段時間專案開發總結
原有設計
這段時間接手了公司使用者端首頁的版本迭代任務.產品設計與上一個版本有非常大的差異.
首先想到的肯定是想通過在原有基礎上進行修改. 原有設計是 頂部搜尋欄+SlidingTabLayout+ViewPager
實現頂部tab,內容頁為RecycleView 頭部為banner圖加店鋪列表+某種商品列表 ,
列表內容為有分類頭部的item.CoordinatorLayout+AppBarLayout實現頂部懸浮.
現有的設計
在搜尋欄上增加一條公司標語,搜尋和viewPager+tab不變 ,當列表滾動到第一個分類item時顯示分類tab並實
現根據滾動對分類tab進行切換.實現的功能就是一個滾動與點選切換分類的聯動效果.遇到的問題時如何計算第
一個可見item距離頂部的距離, 內容頁 我設計成一個多佈局item的recycleView .非聯動相關的頭部 banner+活動選項
+其它組成頭部item , 然後是店鋪列表item ,某種特殊商品列表item ,一個宮格樣式商品列表item,最後是相同樣式的的
商品item 頂部分類圖示+內容列表 樣式相同 只是進行了分類 ,這裡共用一個item.
遇到的問題
由於之前沒有接手過,看別人程式碼還是痛苦的,開始在原有的程式碼上進行修改,首先當然是修改佈局,進去xml中簡直看懵了,我問了之前做的同事,他說裡面有兩套佈局,一套顯示定位失敗的情況,一套顯示正常然後顯示首頁內容的佈局.這樣的寫法對於接手的人當然是痛苦的, 反覆嘗試了幾次最後發現還不如自己重寫,因為專案進度很趕,佈局就讓我浪費了兩天時間,決定自己重寫. 對兩套佈局以及頭部 進行了抽取,通過includ標籤進行引入.立馬整個首頁的佈局結構就清晰了,之後遇到多次佈局調整都能夠快速的進行調整,提高了可維護性,所以希望看到我文章的人也能夠有這種意識.
佈局設計完成接下來就是要根據UI設計互動的要求實現相應的設計效果
從上到下一個個功能實現進行介紹實現
1 SlidingTabLayout tab分類實現頂部懸浮, 公司標語,搜尋欄隱藏
實現方式為系統提供的配置方式CoordinatorLayout+AppBarLayout +內容
CoordinatorLayout需要包裹 AppBarLayout +內容,CoordinatorLayout必須要能佔滿整個螢幕高度,假如CoordinatorLayout上方還有控制元件將其頂下去了,那就加上margain_top =-xxdp,AppBarLayout中為頂部懸浮會隱藏的內容.
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:elevation="0dp"
tools:visibility="visible">
<LinearLayout
android:id="@+id/top_rl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:layout_scrollFlags="enterAlways|scroll">
<ImageView
android:id="@+id/iv_one_hour"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:src="@drawable/index_caichengoneh_ic" />
<LinearLayout
android:id="@+id/ll_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="@dimen/dimen_5">
<ImageView
android:id="@+id/iv_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dimen_12"
android:adjustViewBounds="true"
android:src="@drawable/index_add_ic" />
<ImageView
android:id="@+id/iv_cai_cheng"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dimen_5"
android:adjustViewBounds="true"
android:src="@drawable/index_nav_caicheng_ic"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/rl_market"
android:layout_width="0dp"
android:layout_height="27dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="@dimen/dimen_6"
android:layout_weight="1"
android:background="@drawable/home_search_bg"
android:paddingLeft="@dimen/dimen_12"
android:paddingRight="@dimen/dimen_12">
<TextView
android:id="@+id/tv_market"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_gravity="center"
android:gravity="center_vertical"
android:maxLines="1"
android:minHeight="30dp"
android:text="市場"
android:textColor="@color/color_666666"
android:textSize="@dimen/xh_size14" />
<ImageView
android:id="@+id/iv_market"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="@dimen/dimen_6"
android:layout_toRightOf="@id/tv_market"
android:src="@drawable/home_down_ic"
android:visibility="gone" />
</RelativeLayout>
<ImageView
android:id="@+id/iv_search"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:layout_marginRight="@dimen/dimen_10"
android:scaleType="fitXY"
android:src="@drawable/index_serach_ic" />
</LinearLayout>
</LinearLayout>
</android.support.design.widget.AppBarLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
< [email protected]/appbar_scrolling_view_behavior 必須加入實現頂部懸浮-->
<!-- 首頁內容-->
<include
layout="@layout/home_data_show"
android:visibility="gone" />
<!-- 定位失敗-->
<include
layout="@layout/home_have_market"
android:visibility="gone" />
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
2 分類聯動列表 相當於商城app的左右分類聯動切換,只是換成了頂部.開始也只是簡簡單單的將聯動程式碼遷移過來,之前的商品分類的聯動功能是我實現的,完成後發現了很多問題,
1,點選頂部聯動tab出現滾動過頭,也是出現分類圖示被遮擋.
2,列表滾動到頂部實現對頂部tab的切換,但分類圖示被遮擋了一部分.
解決第一個問題比較簡單,因為有大神提供瞭解決方法 ,替換自己的LinearLayoutManager即可
滾動到指定位置mRecyclerView.smoothScrollToPosition(position );並置頂.
public class TopLayoutManager extends LinearLayoutManager {
public TopLayoutManager(Context context) {
super(context);
}
public TopLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public TopLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
RecyclerView.SmoothScroller smoothScroller = new TopSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private static class TopSmoothScroller extends LinearSmoothScroller {
TopSmoothScroller(Context context) {
super(context);
}
/**
* 以下引數以LinearSmoothScroller解釋
*
* @param viewStart RecyclerView的top位置
* @param viewEnd RecyclerView的bottom位置
* @param boxStart Item的top位置
* @param boxEnd Item的bottom位置
* @param snapPreference 判斷滑動方向的標識
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
* {@link #SNAP_TO_END}.)
* @return 移動偏移量
*/
@Override
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
CLog.e("tag",boxStart+"---boxStart-------viewStart-------"+viewStart);
return boxStart - viewStart + 120;// 這裡是關鍵,得到的就是置頂的偏移量
}
}
}
第二個問題處理起來就複雜了
要實現的就是恰好滾到到聯動tab的高度就進行切換,既然是滾動,那就得對recycleView的滾動進行監聽
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
refreshQuickTop(true);//dy 正值手指滑動方向向上 反之向下
isUp = true;
} else {
refreshQuickTop(false);
isUp = false;
}
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
// 只有LinearLayoutManager才有查詢第一個和最後一個可見view位置的方法
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager;
//獲取第一個可見view的位置
int firstItemPosition = linearManager.findFirstVisibleItemPosition();
if (otherAdapter.getData().size() == 0) {
return;
}
//根據索引來獲取對應的itemView
View firstVisiableChildView = linearManager.findViewByPosition(firstItemPosition);
//獲取當前顯示條目的高度
int itemHeight = firstVisiableChildView.getHeight();
//獲取當前Recyclerview 偏移量
int flag = firstVisiableChildView.getTop();// 向上滾動 flag 絕對值減小 反之絕對值增大 (關鍵) // DisplayUtils.dip2px(getActivity(), 40))聯動tab高度
if (firstItemPosition == 0) {
if (isVisible && Math.abs(flag) > (itemHeight - DisplayUtils.dip2px(getActivity(), 40))) {
rvTypeTab.setVisibility(View.VISIBLE);
isVisible = false;
} else if (!isVisible && Math.abs(flag) < itemHeight - DisplayUtils.dip2px(getActivity(), 40)){
isVisible = true;
rvTypeTab.setVisibility(View.INVISIBLE);
otherAdapter.setListTabComeBack(0);
}
}
if (isOnItemClick) {//點選聯動tab時不進行滾動時的操作
//內容滾動到條目
isOnItemClick = false;
rvTypeTab.scrollToPosition(position);
if (isScroll) {
//改變聯動tab選擇的顏色和背景
tabAdapter.setSelected(position);
}
isScroll = true;
} else {
if (isUp && Math.abs(flag) > itemHeight - DisplayUtils.dip2px(getActivity(), 44)) {
//獲取到第一個條目的型別
if (firstItemPosition != -1) {
String catId = otherAdapter.getData().get(firstItemPosition + 1).getCatName();
//遍歷左邊列表資料集合,獲取到當前型別的索引
for (int i = 0; i < tabAdapter.getData().size(); i++) {
if (catId.equals(tabAdapter.getData().get(i))) {
position = i;
break;
}
}
}
} else if (!isUp && itemHeight - Math.abs(flag) > DisplayUtils.dip2px(getActivity(), 44)) {
// 底部可見
if (firstItemPosition >= 0) {
String catId = otherAdapter.getData().get(firstItemPosition).getCatName();
//遍歷左邊列表資料集合,獲取到當前型別的索引
for (int i = 0; i < tabAdapter.getData().size(); i++) {
if (catId.equals(tabAdapter.getData().get(i))) {
position = i;
break;
}
}
}
}
isOnItemClick = false;
//聯動tab滾動到指定條目
rvTypeTab.scrollToPosition(position);
if (isScroll) {
//改變選擇的顏色和背景
tabAdapter.setSelected(position);
}
isScroll = true;
}
}
}
});
3 圖文混排遇到的問題
圖文混排導致文字被圖片替換,以及圖片和文字不能居中對齊
處理結果:
int count = 0;
SpannableStringBuilder span = new SpannableStringBuilder(item.productName + "/" + item.weightDesc);
CustomImageSpan image = new CustomImageSpan(mContext, R.drawable.lab_recommend_ic, CustomImageSpan.ALIGN_BASELINE);
span.insert(count, "圖 ");
count = count + 1;
span.setSpan(image, count-1, count, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
圖文居中對齊
public class CustomImageSpan extends ImageSpan {
//自定義對齊方式--與文字中間線對齊
private int ALIGN_FONTCENTER = 2;
public CustomImageSpan(Context context, int resourceId) {
super(context, resourceId);
}
public CustomImageSpan(Context context, int resourceId, int verticalAlignment) {
super(context, resourceId, verticalAlignment);
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom,
Paint paint) {
//draw 方法是重寫的ImageSpan父類 DynamicDrawableSpan中的方法,在DynamicDrawableSpan類中,雖有getCachedDrawable(),
// 但是私有的,不能被呼叫,所以呼叫ImageSpan中的getrawable()方法,該方法中 會根據傳入的drawable ID ,獲取該id對應的
// drawable的流物件,並最終獲取drawable物件
Drawable drawable = getDrawable(); //呼叫imageSpan中的方法獲取drawable物件
canvas.save();
//獲取畫筆的文字繪製時的具體測量資料
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
//系統原有方法,預設是Bottom模式)
int transY = bottom - drawable.getBounds().bottom;
if (mVerticalAlignment == ALIGN_BASELINE) {
transY -= fm.descent;
} else if (mVerticalAlignment == ALIGN_FONTCENTER) { //此處加入判斷, 如果是自定義的居中對齊
//與文字的中間線對齊(這種方式不論是否設定行間距都能保障文字的中間線和圖片的中間線是對齊的)
// y+ascent得到文字內容的頂部座標,y+descent得到文字的底部座標,(頂部座標+底部座標)/2=文字內容中間線標
transY = ((y + fm.descent) + (y + fm.ascent)) / 2 - drawable.getBounds().bottom / 2;
}
canvas.translate(x, transY+5);
drawable.draw(canvas);
canvas.restore();
}
/**
* 重寫getSize方法,只有重寫該方法後,才能保證不論是圖片大於文字還是文字大於圖片,都能實現中間對齊
*/
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
Drawable d = getDrawable();
Rect rect = d.getBounds();
if (fm != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.bottom - fmPaint.top;
int drHeight = rect.bottom - rect.top;
int top = drHeight / 2 - fontHeight / 4;
int bottom = drHeight / 2 + fontHeight / 4;
fm.ascent = -bottom;
fm.top = -bottom;
fm.bottom = top;
fm.descent = top;
}
return rect.right;
}
}
購物車商品新增拋物線動畫
public class ThrowCartAnimUtil {
private static final int THROW_DURATION = 1000;
private static final int SCALE_DURATION = 200;
private static final int OFFY = 200;// dp
private Activity mActivity;
private Animator mAnimator;
private View mAnimView;
private View mEndView;
private final SimpleDraweeView mGenericDraweeView;
public ThrowCartAnimUtil(Activity activity, @NonNull View endView) {
mActivity = activity;
mGenericDraweeView = new SimpleDraweeView(mActivity);
// mGenericDraweeView.setBackgroundResource(R.drawable.red_oval);
final int size = DisplayUtils.dip2px(mActivity, 30);
mGenericDraweeView.setLayoutParams(new ViewGroup.LayoutParams(size, size));
mGenericDraweeView.measure(0, 0);
setAnimView(mGenericDraweeView);
mEndView = endView;
}
public void setAnimView(View view) {
mAnimView = view;
}
public void start(View startView,String url) {
start(startView,url, null);
}
public void start(View startView,String url, Animator.AnimatorListener listener) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.end();
}
FrescoUtils.setRequest(mGenericDraweeView,url);
final int animViewWidth = mAnimView.getMeasuredWidth();
final int animViewHeight = mAnimView.getMeasuredHeight();
//獲取檢視在視窗的位置
int[] startViewLoc = new int[2];
int[] endViewLoc = new int[2];
startView.getLocationInWindow(startViewLoc);
mEndView.getLocationInWindow(endViewLoc);
//將動畫view新增到decorView
ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
if (mAnimView.getParent() != decorView) {
ViewUtils.removeFromParent(mAnimView);
decorView.addView(mAnimView);
}
mAnimView.setVisibility(View.VISIBLE);
mAnimView.setX(startViewLoc[0]);
mAnimView.setY(startViewLoc[1]);
//扔出動畫
final Point startPot = new Point(startViewLoc[0] + startView.getWidth() / 2, startViewLoc[1] + startView.getHeight() / 2);
final Point endPot = new Point(endViewLoc[0] + mEndView.getWidth() / 2, endViewLoc[1] mEndView.getHeight() / 2);
int offY = DisplayUtils.dip2px(mActivity, OFFY);
ScaleAnimation scaleAnimation = new ScaleAnimation(0.6f, 0.1f,0.6f, 0.1f);
scaleAnimation.setInterpolator(new AccelerateInterpolator());
scaleAnimation.setRepeatCount(0);
scaleAnimation.setFillAfter(true);
ValueAnimator throwAnimator = ObjectAnimator.ofObject(new BezierEvaluator(offY), startPot, endPot);
throwAnimator.setInterpolator(new AccelerateInterpolator(0.8f));
throwAnimator.setDuration(THROW_DURATION);
throwAnimator.addUpdateListener(animation -> {
Point point = (Point) animation.getAnimatedValue();
mAnimView.setX(point.x - animViewWidth / 2);
mAnimView.setY(point.y - animViewHeight / 2);
});
throwAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mAnimView.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mAnimView.setVisibility(View.GONE);
}
});
//購物車放大縮小動畫
AnimatorSet scaleAnimator = new AnimatorSet();
mEndView.setPivotX(mEndView.getWidth() / 2);
mEndView.setPivotY(mEndView.getHeight() / 2);
//縮小
PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1, 0.8f);
PropertyValuesHolder valuesHolder2 = PropertyValuesHolder.ofFloat("scaleY", 1, 0.8f);
ValueAnimator zoomIn = ObjectAnimator.ofPropertyValuesHolder(mEndView, valuesHolder1, valuesHolder2);
zoomIn.setDuration(SCALE_DURATION / 2);
//放大
PropertyValuesHolder valuesHolder11 = PropertyValuesHolder.ofFloat("scaleX", 0.8f, 1);
PropertyValuesHolder valuesHolder22 = PropertyValuesHolder.ofFloat("scaleY", 0.8f, 1);
ValueAnimator zoomOut = ObjectAnimator.ofPropertyValuesHolder(mEndView, valuesHolder11,valuesHolder22);
zoomOut.setDuration(SCALE_DURATION / 2);
zoomOut.setInterpolator(new OvershootInterpolator());
scaleAnimator.playSequentially(zoomIn, zoomOut);
AnimatorSet animatorSet = new AnimatorSet();
mAnimator = animatorSet;
// animatorSet.
animatorSet.playSequentially(throwAnimator, scaleAnimator);
if (listener != null) {
animatorSet.addListener(listener);
}
animatorSet.start();
}
public boolean isRunning() {
return mAnimator != null && mAnimator.isRunning();
}
public static <T> ObservableTransformer<T, T> requestWhitAnim(Context context, View view,String url, ThrowCartAnimUtil anim) {
return upstream -> {
Dialog dialog = Dialogs.progress(context);
return upstream.doOnSubscribe(disposable -> {
if (anim != null) {
anim.start(view,url, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//動畫結束請求還未完成,彈出等待框
if (!disposable.isDisposed()) {
if (context instanceof Activity && !((Activity) context).isFinishing()) {
dialog.show();
}
dialog.setOnCancelListener(dialog1 -> {
disposable.dispose();
});
}
}
});
}
}).doOnTerminate(dialog::dismiss);
};
}
private static class BezierEvaluator implements TypeEvaluator<Point> {
private final Point mPoint = new Point();
private int offsetY;
public BezierEvaluator(int offsetY) {
this.offsetY = offsetY;
}
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
final int centerX = (startValue.x + endValue.x) / 2;
final int curX = (int) (startValue.x * Math.pow(1 - fraction, 2) + 2 * fraction * (1 - fraction) * centerX + fraction * fraction * endValue.x);
final int curY = (int) (startValue.y * Math.pow(1 - fraction, 2) + 2 * fraction * (1 - fraction) * (startValue.y - offsetY) + fraction * fraction * endValue.y);
mPoint.set(curX, curY);
return mPoint;
}
}
}