海报分享功能实现详解
来源:10年Android开发     阅读:649
源码超市
发布于 2019-06-11 02:18
查看主页

前言

因为业务需求,需要做一个卡片分享功能,前期做了少量预研,实现相似效果可以采用如下两种方式:

因为RecyclerView自带复用设计,方便后期拓展,所以就采用RecyclerView这个方案,主要实现的细节效果和功能如下:

1.分页,自动居中
2.卡片样式及效果,阴影等
3.背景色渐变
4.切换卡片,卡片的缩放效果
5.指示器
6.卡片分享

效果图:


RecyclerView这个方向的资料还是比较好查找,不过细节和想实现的效果还是有些许出入。针对这些问题,逐渐探究,经过屡次改良后,得到了较为满意的结果。

本文滑动是横向滑动,假如读者想要纵向的,可以使用RecyclerView的LinearLayoutManager设置方向,其余代码大体相同。

下面我就根据效果逐一给读者提供相关代码实现,并针对实现细节、难点,附上开发思路供大家参考。

难点:

技术实现

分页、自动居中

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,设置对应的朝向就可,另外继承这个也可以设置阻尼大小,还可控制滑动速度。

卡片效果

我这里主要是根据要求做了如下方面的修改,读者可以根据需求,添加动画,列表,点击反馈等。

1)阴影、圆角等

这是我用到的设置,读者可以根据实际效果比照界面设计做调整:

<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)卡片比例动态调整

卡片

要保持在不同屏幕下卡片比例保持不变,就需要根据屏幕的分辨率动态的设置卡片的宽高。

---- 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 卡片占据屏幕宽度的百分比.     * @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);    }

二维码

因为整个卡片都是按比例划分的,为了展现尽可能大的二维码区域,二维码卡片也需要动态设置,按照底部栏的最大高度的80%作为宽高(二维码是正方形)

//根据实际底部栏大小设置宽高.    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);            }        });    }

背景色渐变

这部分主要方法网上都有,就不重复造轮子了。这里是连贯步骤,就是根据当前卡片的底图做一张模糊图,列举出来只是方便读者快速实现。

----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;        }        /**         * 延时设置说明,因为滑动距离会出现正好一页的距离或者偏离.         * 所以滑动中止事件触发会出现一次或者两次(偏离的时候,偏差.         * 量将自动修正后再次中止),所以延时并取消上一次背景切换可以消除画面闪烁。.         */        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);        // 创立一张渲染后的输出图片。        Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);        try {            // 创立RenderScript内核查象            RenderScript rs = RenderScript.create(context);            // 创立一个模糊效果的RenderScript的工具对象            ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));            // 因为RenderScript并没有使用VM来分配内存,所以需要使用Allocation类来创立和分配内存空间。            // 创立Allocation对象的时候其实内存是空的,需要使用copyTo()将数据填充进去。            Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);            Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);            // 设置渲染的模糊程度, 25f是最大模糊度            blurScript.setRadius(radius);            // 设置blurScript对象的输入内存            blurScript.setInput(tmpIn);            // 将输出数据保存到输出内存中            blurScript.forEach(tmpOut);            // 将数据填充到Allocation中            tmpOut.copyTo(outputBitmap);        } catch (Exception e) {            e.printStackTrace();        }finally {            inputBitmap.recycle();        }        return outputBitmap;    }

切换卡片,卡片的缩放效果

我们要实现如上效果,基本的滑动展现,RecyclerView都有实现,需要处理是滑动过程中卡片的缩放问题、卡片透明度变化、滑动距离的判定、页码的计算、多张卡片的内存问题等。

为了复用,主要的代码都是通过帮助类实现。用法如下

---- QRCodePosterActivity.java ----        // mRecyclerView绑定scale效果.        mCardScaleHelper = new CardScaleHelper();        mCardScaleHelper.setCurrentItemPos(0);//初始化指定页面.        mCardScaleHelper.setScale(0.8f);//两侧缩放比例.        mCardScaleHelper.setCardPercentWidth(0.72f);//卡片占屏幕宽度比例.        mCardScaleHelper.attachToRecyclerView(mContentRv);

下面我们来看看具体实现

初始化

我们从绑定开始初始化

---- 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;//卡片占据屏幕宽度的百分比,需要与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);        }    }

卡片滑动切换计算

下面的这个方法是比较核心,包含了所有卡片的缩放比计算,透明度计算,为了达到平滑过度,这里用到了三角函数,也包含了少量适配问题的处理。因为水平有限,如下方法可能还是存在优化的空间或者细节修正,仅供参考,感兴趣的朋友可以自行研究。

---- CardScaleHelper.java ----     /**     * RecyclerView位移事件监听, 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);        //业务代码.    }---- 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);        }    }

多张卡片内存控制

指示器

因为指示器比较简单,这里简述一种实现思路, 可以直接用LinearLayout动态增加包含指示器图案的view,每次滑动结束后升级指示器位置。

卡片分享

在铜板街的应用上,卡片最终是要分享出去,所以我们继续分析下,如何在分享前做好准备,因为分享有需要文件,也有需要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并创立位图.                final Bitmap bitmap = Bitmap.createBitmap(mContentRl.getDrawingCache()); //创立一个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() {                        //分享.                    }                });            }        });    }

注意几个细节,一个是bitmap的回收,第二个是文件的解决,因为QQ分享的问题,我们并不能分享完成后立马删除原文件,所以我的做法是关闭当前页面时,会清除(文件有最后修改时间方法:lastModified)过期的文件缓存。

总结

本文总结了在开发画廊型卡片分享的少量心得和体会,对于一个复杂的程序来说,算法往往是最关键的,整个功能的开发可以说一半的时间都是在调试滑动时卡片的缩放效果。而工作中多数应用开发用到的算法往往比较简单,所以假如想提升,就必需自己去专研。

最后详情个QQ群:979045005,点击加群 ,Android 开发的朋友可以加一下,有什么新技术大家一起交流学习一下,整理了少量干货,需要的话,可以进群找管理免费领取,不多说直接上图吧!

免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境 服务器应用
相关推荐
「nginx」六、nginx配置文件说明
马云:2020年是互联网爆发最赚钱的一年,抓住机遇成就未来
让你提高工作效率的Git的技巧
web开发中该用 em 还是 rem 呢?
Nginx web服务器
首页
搜索
订单
购物车
我的