溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

海報分享功能實現(xiàn)詳解

發(fā)布時間:2020-07-10 10:13:14 來源:網(wǎng)絡 閱讀:339 作者:十年開發(fā) 欄目:移動開發(fā)

前言

由于業(yè)務需求,需要做一個卡片分享功能,前期做了一些預研,實現(xiàn)類似效果可以采用如下兩種方式:

  • 采用ViewPager實現(xiàn)
  • 采用RecyclerView實現(xiàn)

由于RecyclerView自帶復用設計,方便后期拓展,所以就采用RecyclerView這個方案,主要實現(xiàn)的細節(jié)效果和功能如下:

1.分頁,自動居中
2.卡片樣式及效果,陰影等
3.背景色漸變
4.切換卡片,卡片的縮放效果
5.指示器
6.卡片分享

效果圖:
海報分享功能實現(xiàn)詳解
RecyclerView這個方向的資料還是比較好查找,不過細節(jié)和想實現(xiàn)的效果還是有些許出入。針對這些問題,逐步探索,經(jīng)過多次改良后,得到了較為滿意的結(jié)果。

本文滑動是橫向滑動,如果讀者想要縱向的,可以使用RecyclerView的LinearLayoutManager設置方向,其他代碼大體相同。

下面我就根據(jù)效果逐一給讀者提供相關代碼實現(xiàn),并針對實現(xiàn)細節(jié)、難點,附上開發(fā)思路供大家參考。

難點:

  • 卡片比例適配
  • 滑動時卡片縮放動畫。
  • 滑動時距離計算、速度控制、頁碼計算。
  • 內(nèi)存控制

技術(shù)實現(xiàn)

分頁、自動居中
public class CardPagerSnapHelper extends PagerSnapHelper {
    public boolean mNoNeedToScroll = false;

    @Override
    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        if (mNoNeedToScroll) {
            return new int[]{0, 0};
        } else {
            return super.calculateDistanceToFinalSnap(layoutManager, targetView);
        }
    }
}

//使用.
mPageSnapHelp.attachToRecyclerView(mRecyclerView);

這里繼承PagerSnapHelper是因為想要的效果是一頁的滑動。如果想要的是可以滑動多頁,可以使用LinearSnapHelper,設置對應的朝向即可,另外繼承這個也可以設置阻尼大小,還可控制滑動速度。

卡片效果

我這里主要是根據(jù)要求做了如下方面的修改,讀者可以根據(jù)需求,增加動畫,列表,點擊反饋等。

1)陰影、圓角等

  • cardElevation 設置z軸陰影
  • cardCornerRadius 設置圓角大小
  • cardMaxElevation 設置z軸最大高度值
  • cardPreventCornerOverlap 是否添加內(nèi)邊距(避免內(nèi)容與邊緣重疊)
  • cardUseCompatPadding 設置內(nèi)邊距,V21+的版本和之前的版本仍舊具有一樣的計算方式

這是我用到的設置,讀者可以根據(jù)實際效果對比界面設計做調(diào)整:

<android.support.v7.widget.CardView 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="wrap_content"
    android:layout_height="match_parent"
    app:cardBackgroundColor="@color/white"
    app:cardElevation="6dp"
    app:cardMaxElevation="12dp"
    app:cardPreventCornerOverlap="true"
    app:cardUseCompatPadding="false">

2)卡片比例動態(tài)調(diào)整

卡片

要保持在不同屏幕下卡片比例保持不變,就需要根據(jù)屏幕的分辨率動態(tài)的設置卡片的寬高。

---- CardAdapter.java ----
  @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_poster, parent, false);
        mCardAdapterHelper.onCreateViewHolder(parent, itemView, 0.72f, (float) (17.0 / 25.0));
        return new ViewHolder(itemView);
    }

---- CardAdapterHelper.java ----
    /**
     * @param parent
     * @param itemView
     * @param cardPercentWidth 卡片占據(jù)屏幕寬度的百分比.
     * @param aspectRatio      寬高比.
     */
    public void onCreateViewHolder(ViewGroup parent, View itemView, float cardPercentWidth, float aspectRatio) {
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams();
        lp.width = (int) (DisplayUtil.getScreenWidth(parent.getContext()) * cardPercentWidth);
        lp.height = (int) (lp.width / aspectRatio);
        itemView.setLayoutParams(lp);
    }

二維碼

由于整個卡片都是按比例劃分的,為了展示盡可能大的二維碼區(qū)域,二維碼卡片也需要動態(tài)設置,按照底部欄的最大高度的80%作為寬高(二維碼是正方形)

//根據(jù)實際底部欄大小設置寬高.
    private void setQRCodeImageView(final ImageView imageView, final ViewGroup root) {
        if (imageView == null || root == null) {
            return;
        }
        imageView.post(new Runnable() {
            @Override
            public void run() {
                int height = root.getMeasuredHeight();
                int targetHeight = (int) (height * 0.8);
                if (height == 0) {
                    return;
                }

                ViewGroup.LayoutParams params = imageView.getLayoutParams();
                params.width = targetHeight;
                params.height = targetHeight;
                imageView.setLayoutParams(params);
            }
        });
    }

背景色漸變

這部分主要方法網(wǎng)上都有,就不重復造輪子了。這里是連貫步驟,就是根據(jù)當前卡片的底圖做一張模糊圖,列舉出來只是方便讀者快速實現(xiàn)。

----QRCodePosterActivity.java----
     private void initBlurBackground() {
            mBlurView = (ImageView) findViewById(R.id.blurView);
            mContentRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                        notifyBackgroundChange();
                        //指示器
                    }
                }
            });
            setDefaultBackground();
        }

        private void notifyBackgroundChange() {
            if (mPosterModule == null || mPosterModule.getBannerInfo().size() == 0) {
                setDefaultBackground();
                return;
        }

        /**
         * 延時設置說明,由于滑動距離會出現(xiàn)正好一頁的距離或偏離.
         * 所以滑動停止事件觸發(fā)會出現(xiàn)一次或兩次(偏離的時候,偏差.
         * 量將自動修正后再次停止),所以延時并取消上一次背景切換可以消除畫面閃爍。.
         */
        mBlurView.removeCallbacks(mBlurRunnable);
        mBlurRunnable = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = mCardScaleHelper.getCurrentBitmap();
                ViewSwitchUtils.startSwitchBackgroundAnim(mBlurView, BlurBitmapUtils.getBlurBitmap(mBlurView.getContext(), bitmap, 15));
            }
        };
        mBlurView.postDelayed(mBlurRunnable, 500);
    }

     private void setDefaultBackground() {
        if (mBlurView == null) {
            return;
        }
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_card_default);
        mBlurView.setImageBitmap(BlurBitmapUtils.getBlurBitmap(mBlurView.getContext(), bitmap, 15));
    }

---- CardScaleHelper.java ----
     public Bitmap getCurrentBitmap() {
        View view = mRecyclerView.getLayoutManager().findViewByPosition(getCurrentItemPos());
        if (view == null) {
            return null;
        }
        ImageView mBgIv = (ImageView) view.findViewById(R.id.iv_bg);
        final Bitmap bitmap = ((BitmapDrawable) mBgIv.getDrawable()).getBitmap();
        return bitmap;
    }

---- ViewSwitchUtils.java ----
     public static void startSwitchBackgroundAnim(ImageView view, Bitmap bitmap) {
        if (view == null || bitmap == null) {
            return;
        }
        Drawable oldDrawable = view.getDrawable();
        Drawable oldBitmapDrawable;
        TransitionDrawable oldTransitionDrawable = null;
        if (oldDrawable instanceof TransitionDrawable) {
            oldTransitionDrawable = (TransitionDrawable) oldDrawable;
            oldBitmapDrawable = oldTransitionDrawable.findDrawableByLayerId(oldTransitionDrawable.getId(1));
        } else if (oldDrawable instanceof BitmapDrawable) {
            oldBitmapDrawable = oldDrawable;
        } else {
            oldBitmapDrawable = new ColorDrawable(0xffc2c2c2);
        }

        if (oldTransitionDrawable == null) {
            oldTransitionDrawable = new TransitionDrawable(new Drawable[]{oldBitmapDrawable, new BitmapDrawable(view.getResources(), bitmap)});
            oldTransitionDrawable.setId(0, 0);
            oldTransitionDrawable.setId(1, 1);
            oldTransitionDrawable.setCrossFadeEnabled(true);
            view.setImageDrawable(oldTransitionDrawable);
        } else {
            oldTransitionDrawable.setDrawableByLayerId(oldTransitionDrawable.getId(0), oldBitmapDrawable);
            oldTransitionDrawable.setDrawableByLayerId(oldTransitionDrawable.getId(1), new BitmapDrawable(view.getResources(), bitmap));
        }
        oldTransitionDrawable.startTransition(1000);
    }

---- BlurBitmapUtils.java ----    
     /**
     * 得到模糊后的bitmap
     *
     * @param context
     * @param bitmap
     * @param radius
     * @return
     */
    public static Bitmap getBlurBitmap(Context context, Bitmap bitmap, int radius) {
        if (bitmap == null || context == null) {
            return null;
        }
        // 將縮小后的圖片做為預渲染的圖片。
        Bitmap inputBitmap = Bitmap.createScaledBitmap(bitmap, SCALED_WIDTH, SCALED_HEIGHT, false);
        // 創(chuàng)建一張渲染后的輸出圖片。
        Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);
        try {
            // 創(chuàng)建RenderScript內(nèi)核對象
            RenderScript rs = RenderScript.create(context);
            // 創(chuàng)建一個模糊效果的RenderScript的工具對象
            ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

            // 由于RenderScript并沒有使用VM來分配內(nèi)存,所以需要使用Allocation類來創(chuàng)建和分配內(nèi)存空間。
            // 創(chuàng)建Allocation對象的時候其實內(nèi)存是空的,需要使用copyTo()將數(shù)據(jù)填充進去。
            Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
            Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);

            // 設置渲染的模糊程度, 25f是最大模糊度
            blurScript.setRadius(radius);
            // 設置blurScript對象的輸入內(nèi)存
            blurScript.setInput(tmpIn);
            // 將輸出數(shù)據(jù)保存到輸出內(nèi)存中
            blurScript.forEach(tmpOut);

            // 將數(shù)據(jù)填充到Allocation中
            tmpOut.copyTo(outputBitmap);

        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            inputBitmap.recycle();
        }

        return outputBitmap;
    }

切換卡片,卡片的縮放效果

我們要實現(xiàn)如上效果,基本的滑動展示,RecyclerView都有實現(xiàn),需要解決是滑動過程中卡片的縮放問題、卡片透明度變化、滑動距離的判定、頁碼的計算、多張卡片的內(nèi)存問題等。

為了復用,主要的代碼都是通過幫助類實現(xiàn)。用法如下

---- QRCodePosterActivity.java ----
        // mRecyclerView綁定scale效果.
        mCardScaleHelper = new CardScaleHelper();
        mCardScaleHelper.setCurrentItemPos(0);//初始化指定頁面.
        mCardScaleHelper.setScale(0.8f);//兩側(cè)縮放比例.
        mCardScaleHelper.setCardPercentWidth(0.72f);//卡片占屏幕寬度比例.
        mCardScaleHelper.attachToRecyclerView(mContentRv);

下面我們來看看具體實現(xiàn)

初始化

我們從綁定開始初始化

---- CardScaleHelper.java ---- 
    private int mCardWidth; // 卡片寬度.
    private int mOnePageWidth; // 滑動一頁的距離.
    private int mCardGalleryWidth;
    private int mCurrentItemPos;
    private int mCurrentItemOffset;
    private float mScale = 0.9f; // 兩邊視圖scale.
    private float mCardPercentWidth = 0.60f;//卡片占據(jù)屏幕寬度的百分比,需要與CardAdapterHelper中的一致.
    private CardPagerSnapHelper mPageSnapHelp = new CardPagerSnapHelper();

     public void attachToRecyclerView(final RecyclerView mRecyclerView) {
        this.mRecyclerView = mRecyclerView;
        mContext = mRecyclerView.getContext();
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    mPageSnapHelp.mNoNeedToScroll = mCurrentItemOffset == 0 || mCurrentItemOffset == getDestItemOffset(mRecyclerView.getAdapter().getItemCount() - 1);
                } else {
                    mPageSnapHelp.mNoNeedToScroll = false;
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (dx == 0) {
                    initWidth();
                    return;
                }
                // dx>0則表示右滑, dx<0表示左滑, dy<0表示上滑, dy>0表示下滑
                mCurrentItemOffset += dx;
                computeCurrentItemPos();
                onScrolledChangedCallback();
            }

        });
        mPageSnapHelp.attachToRecyclerView(mRecyclerView);
    }

    /** 初始化卡片寬度**/
    private void initWidth() {
        mCardGalleryWidth = mRecyclerView.getWidth();
        mCardWidth = (int) (mCardGalleryWidth * mCardPercentWidth);
        mOnePageWidth = mCardWidth;
        mRecyclerView.smoothScrollToPosition(mCurrentItemPos);
        onScrolledChangedCallback();
    }

計算當前卡片索引

---- CardScaleHelper.java ---- 
    private void computeCurrentItemPos() {
        if (mOnePageWidth <= 0) return;
        boolean pageChanged = false;
        // 滑動超過一頁說明已翻頁.
        if (Math.abs(mCurrentItemOffset - mCurrentItemPos * mOnePageWidth) >= (mOnePageWidth)) {
            pageChanged = true;
        }
        if (pageChanged) {
            int tempPos = mCurrentItemPos;
            mCurrentItemPos = mCurrentItemOffset / (mOnePageWidth);
        }
    }

卡片滑動切換計算

下面的這個方法是比較核心,包含了所有卡片的縮放比計算,透明度計算,為了達到平滑過度,這里用到了三角函數(shù),也包含了一些適配問題的解決。由于水平有限,如下方法可能還是存在優(yōu)化的空間或細節(jié)修正,僅供參考,感興趣的朋友可以自行研究。

---- CardScaleHelper.java ---- 
    /**
     * RecyclerView位移事件監(jiān)聽, view大小隨位移事件變化.
     */
    public void onScrolledChangedCallback() {
        for (int i = 0; i < mRecyclerView.getAdapter().getItemCount(); i++) {
            LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
            final View view = layoutManager.getChildAt(i);
            if (view == null) {
                continue;
            }
            //計算當前這個view相對于中間View的偏移頁碼量.
            //(view相對的X的起始位置-當前scrollview滾動的位置)/每頁大小.
            // = 0 為居中頁.
            // = 1 為下一頁 2 為下下頁.
            // = -1 為上一頁 -2 為上上頁.
            double offsetPage = ((int) view.getTag() * (double) mOnePageWidth - mCurrentItemOffset) / (double) mOnePageWidth;
            double scale = (float) Math.cos(offsetPage);
            if (Math.abs(scale) < mScale)
                scale = mScale;
            view.setScaleX((float) scale);
            view.setScaleY((float) scale);

            BigDecimal bd = new BigDecimal((scale * 0.8)).setScale(1, RoundingMode.UP);
            if (scale > 0.99f) {
                view.setAlpha(1);
            } else {
                view.setAlpha((bd.floatValue()));
                //解決透明顯示異常的問題,強制重新繪制.
                view.invalidate();
            }
        }

    }

Tag值,及滑動時卡片間隙計算。

---- CardAdapter.java ----   
    @Override
    public void onBindViewHolder(final ViewHolder holder, final int position) {
        holder.itemView.setTag(position);
        mCardAdapterHelper.onBindViewHolder(holder.itemView, position, getItemCount());
        setQRCodeImageView(holder.mQRCodeIv, holder.mBottomLl);
        //業(yè)務代碼.
    }

---- CardScaleHelper.java ----  
    private int mPagePadding = 15;

    public void onBindViewHolder(View itemView, final int position, int itemCount) {
        int mOneSideWidth = (int) ((DisplayUtil.getScreenWidth(itemView.getContext()) - itemView.getLayoutParams().width) / 2.0);
        int leftMarin = position == 0 ? mOneSideWidth : 0;
        int rightMarin = position == itemCount - 1 ? mOneSideWidth : 0;
        setViewMargin(itemView, leftMarin, 0, rightMarin, 10);
    }

    private void setViewMargin(View view, int left, int top, int right, int bottom) {
        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        if (lp.leftMargin != left || lp.topMargin != top || lp.rightMargin != right || lp.bottomMargin != bottom) {
            lp.setMargins(left, top, right, bottom);
            view.setLayoutParams(lp);
        }
    }

多張卡片內(nèi)存控制

  • 方案一:利用第三方框架去顯示,如glide,pessones等,最簡單,如果對內(nèi)存沒有極細的要求的話推薦使用這個方案。
  • 方案二:可以考慮回收不顯示卡片的那部分內(nèi)存,然后利用LruCache進行緩存管理。
    指示器

    由于指示器比較簡單,這里簡述一種實現(xiàn)思路, 可以直接用LinearLayout動態(tài)添加包含指示器圖案的view,每次滑動結(jié)束后更新指示器位置。

卡片分享

在銅板街的應用上,卡片最終是要分享出去,所以我們繼續(xù)分析下,如何在分享前做好準備,由于分享有需要文件,也有需要Bitmap的.

 @Override
    public void onClick(View v) {
        if (v.getId() == R.id.iv_back) {
            finish();
            return;
        }
        createBitmap(v);
    }

    public void createBitmap(final View clickView) {
        showLoadingCustomDialog();
        ThreadPoolManager.getInstance().addTask(new Runnable() {
            @Override
            public void run() {
                View view = linearLayoutManager.findViewByPosition(mCardScaleHelper.getCurrentItemPos());
                View mContentRl = view.findViewById(R.id.rl_content);
                mContentRl.setDrawingCacheEnabled(true);
                mContentRl.buildDrawingCache();  //啟用DrawingCache并創(chuàng)建位圖.
                final Bitmap bitmap = Bitmap.createBitmap(mContentRl.getDrawingCache()); //創(chuàng)建一個DrawingCache的拷貝,因為DrawingCache得到的位圖在禁用后會被回收.
                mContentRl.setDrawingCacheEnabled(false);  //禁用DrawingCahce否則會影響性能.
                mContentRl.destroyDrawingCache();
                file = FileUtil.saveImage(Constant.IMAGE_CACHE_PATH, "share" + System.currentTimeMillis(), bitmap);
                dismissLoadingCustomDialog();
                clickView.post(new Runnable() {
                    @Override
                    public void run() {
                        //分享.
                    }
                });
            }
        });
    }

注意幾個細節(jié),一個是bitmap的回收,第二個是文件的處理,由于QQ分享的問題,我們并不能分享完成后立馬刪除原文件,所以我的做法是關閉當前頁面時,會清理(文件有最后修改時間方法:lastModified)過期的文件緩存。

總結(jié)

本文總結(jié)了在開發(fā)畫廊型卡片分享的一些心得和體會,對于一個復雜的程序來說,算法往往是最關鍵的,整個功能的開發(fā)可以說一半的時間都是在調(diào)試滑動時卡片的縮放效果。而工作中多數(shù)應用開發(fā)用到的算法往往比較簡單,所以如果想提升,就必須自己去專研。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI