仿购物类APP秒杀活动页面

作者: IAM四十二 | 来源:发表于2016-03-23 21:42 被阅读2090次

    在淘宝和京东都可以看到一类秒杀活动,即在特定的时间段内商品有着“貌似十分实惠”的价格,诱惑众亲剁手。
    本着学习的态度,模仿了一下整个页面的内容,发现主要难点有两个方面:

    顶部指示器内容及位置的动态调整
    各个页面计时器的实现

    这里就我所遇见的问题做一下分析,首先看一下效果图。

    效果图

    为避免打广告嫌疑,特在图中加入马赛克,请忽略列表内容

    由于动图使用GifCam录制,限制在2M内之后,效果不看起来是很流畅,实际中在ViewPager中切换fragment是很流畅的

    效果图可以看到,实际实现内容不多,主要是顶部Tab切换,并始终将选中项保持在中间位置,在一点就是右上角计时器的实现。
    好了,这里就这两个问题说一下。

    指示器-ViewPagerIndicator

    其实ViewPager的Indicator(指示器),官方也有提供(如TabPageIndicator)使用起来也是很方便,但是在这里使用有些不太合适,主要是每个Indicator的内容之间距离过宽,indicator的背景色也是无法修改。所以,这里参考网络各位大神自定义过的ViewPagerIndicator后,实现如下:

    public class ViewPagerIndicator extends LinearLayout {
    
        private Paint mPaint;
        private Path mPath;
        private int mIndicatorWidth;
        private int mIndicatorHeight;
    
        /**
         * 三角形的宽度为单个Tab的1/6
         */
        private static final float RADIO_TRIANGEL = 1.0f / 6;
        /**
         * 三角形的最大宽度
         */
        private final int DIMENSION_TRIANGEL_WIDTH = (int) (getScreenWidth() / 3 * RADIO_TRIANGEL);
    
        /**
         * 初始时,三角形指示器的偏移量
         */
        private int mInitTranslationX;
        /**
         * 手指滑动时的偏移量
         */
        private float mTranslationX;
    
        /**
         * 默认的Tab数量
         */
        private static final int COUNT_DEFAULT_TAB = 4;
        /**
         * tab数量
         */
        private int mTabVisibleCount = COUNT_DEFAULT_TAB;
    
        /**
         * tab上的内容
         */
        private List<String> mTabTitles;
        /**
         * 与之绑定的ViewPager
         */
        public ViewPager mViewPager;
    
        /**
         * 标题正常时的颜色
         */
        private static final int COLOR_TEXT_NORMAL = 0x77FFFFFF;
        private static final int COLOR_BG_NORMAL = 0xFF252a2e;
        /**
         * 标题选中时的颜色
         */
        private static final int COLOR_TEXT_HIGHLIGHTCOLOR = 0xFFFFFFFF;
        private static final int COLOR_BG_HIGHLIGHTCOLOR = 0xFFE5004F;
    
        private Context mContext;
    
        public ViewPagerIndicator(Context context) {
            this(context, null);
            mContext = context;
        }
    
        public ViewPagerIndicator(Context context, AttributeSet attrs) {
            super(context, attrs);
            mContext = context;
    
            // 获得自定义属性,tab的数量
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPagerIndicator);
            mTabVisibleCount = a.getInt(R.styleable.ViewPagerIndicator_item_count, COUNT_DEFAULT_TAB);
            if (mTabVisibleCount < 0)
                mTabVisibleCount = COUNT_DEFAULT_TAB;
            a.recycle();
    
            // 初始化画笔
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setColor(Color.parseColor("#ffffffff"));
            mPaint.setStyle(Style.FILL);
            mPaint.setPathEffect(new CornerPathEffect(3));
    
        }
    
        /**
         * 绘制指示器
         */
        @Override
        protected void dispatchDraw(Canvas canvas) {
            canvas.save();
            canvas.drawPath(mPath, mPaint);
            canvas.restore();
    
            super.dispatchDraw(canvas);
        }
    
        /**
         * 初始化指示器的宽度
         */
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mIndicatorWidth = (int) (w / mTabVisibleCount * RADIO_TRIANGEL);
            mIndicatorWidth = Math.min(DIMENSION_TRIANGEL_WIDTH, mIndicatorWidth);
    
            initTriangle();
    
            // 初始时的偏移量
            mInitTranslationX = 0 + mIndicatorWidth * 2;
        }
    
        /**
         * 设置可见的tab的数量
         *
         * @param count
         */
        public void setVisibleTabCount(int count) {
            this.mTabVisibleCount = count;
        }
    
        /**
         * @param datas
         */
        public void setTabItemTitles(List<String> datas) {
            // 如果传入的list有值,则移除布局文件中设置的view
            if (datas != null && datas.size() > 0) {
                this.removeAllViews();
                this.mTabTitles = datas;
    
                for (String title : mTabTitles) {
                    // 添加view
                    addView(generateTextView(title));
                }
                // 设置item的click事件
                setItemClickEvent();
            }
    
        }
    
        /**
         * 对外的ViewPager的回调接口
         */
        public interface PageChangeListener {
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
    
            public void onPageSelected(int position);
    
            public void onPageScrollStateChanged(int state);
        }
    
        // 对外的ViewPager的回调接口
        private PageChangeListener onPageChangeListener;
    
        // 对外的ViewPager的回调接口的设置
        public void setOnPageChangeListener(PageChangeListener pageChangeListener) {
            this.onPageChangeListener = pageChangeListener;
        }
    
        // 设置关联的ViewPager
        public void setViewPager(ViewPager mViewPager, int pos) {
            this.mViewPager = mViewPager;
    
            mViewPager.addOnPageChangeListener(new OnPageChangeListener() {
                @Override
                public void onPageSelected(int position) {
                    // 设置字体颜色高亮
                    resetTextViewColor();
                    highLightTextView(position);
    
                    // 回调
                    if (onPageChangeListener != null) {
                        onPageChangeListener.onPageSelected(position);
                    }
                }
    
                @Override
                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                    // 滚动
                    scroll(position, positionOffset);
    
                    // 回调
                    if (onPageChangeListener != null) {
                        onPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
                    }
    
                }
    
                @Override
                public void onPageScrollStateChanged(int state) {
                    // 回调
                    if (onPageChangeListener != null) {
                        onPageChangeListener.onPageScrollStateChanged(state);
                    }
    
                }
            });
            // 设置当前页
            mViewPager.setCurrentItem(pos);
            // 高亮
            highLightTextView(pos);
        }
    
        /**
         * 高亮文本
         *
         * @param position
         */
        protected void highLightTextView(int position) {
            View view = getChildAt(position);
            if (view instanceof TextView) {
                ((TextView) view).setTextColor(COLOR_TEXT_HIGHLIGHTCOLOR);
                ((TextView) view).setBackgroundColor(COLOR_BG_HIGHLIGHTCOLOR);
    
            }
    
        }
    
        /**
         * 重置文本颜色
         */
        private void resetTextViewColor() {
            for (int i = 0; i < getChildCount(); i++) {
                View view = getChildAt(i);
                if (view instanceof TextView) {
                    ((TextView) view).setTextColor(COLOR_TEXT_NORMAL);
                    ((TextView) view).setBackgroundColor(COLOR_BG_NORMAL);
                }
            }
        }
    
        /**
         * 设置点击事件
         */
        public void setItemClickEvent() {
            int cCount = getChildCount();
            for (int i = 0; i < cCount; i++) {
                final int j = i;
                View view = getChildAt(i);
                view.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mViewPager.setCurrentItem(j);
                    }
                });
            }
        }
    
        /**
         * 根据标题生成我们的TextView
         *
         * @param text
         * @return
         */
        private TextView generateTextView(String text) {
            TextView tv = new TextView(getContext());
            LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
                    LayoutParams.MATCH_PARENT);
            lp.width = getScreenWidth() / mTabVisibleCount;
            tv.setGravity(Gravity.CENTER);
            tv.setTextColor(COLOR_TEXT_NORMAL);
            tv.setText(text);
            tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
            tv.setLayoutParams(lp);
            return tv;
        }
    
        /**
         * 初始化指示器
         */
        private void initTriangle() {
            mPath = new Path();
            mIndicatorHeight = (int) (mIndicatorWidth / 2 / Math.sqrt(2));
            mPath.moveTo(0, 0);
            mPath.lineTo(mIndicatorWidth, 0);
            mPath.lineTo(mIndicatorWidth / 2, -mIndicatorHeight);
            mPath.close();
        }
    
        /**
         * 指示器跟随手指滚动,以及容器滚动
         *
         * @param position
         * @param offset
         */
        public void scroll(int position, float offset) {
            /**
             * <pre>
             *  0-1:position=0 ;1-0:postion=0;
             * </pre>
             */
            // 不断改变偏移量,invalidate
    
            int tabWidth = getScreenWidth() / mTabVisibleCount;
            int movex = (getScreenWidth() - tabWidth) / 2;
    
            if (mTabVisibleCount != 1) {
    
                this.scrollTo((position - (mTabVisibleCount - 2)) * tabWidth + tabWidth / 2, 0);
    
            } else
            // 为count为1时 的特殊处理
            {
                this.scrollTo(position * tabWidth + (int) (tabWidth * offset), 0);
            }
            // }
    
            invalidate();
        }
    
        /**
         * 设置布局中view的一些必要属性;如果设置了setTabTitles,布局中view则无效
         */
        @Override
        protected void onFinishInflate() {
            Log.e("TAG", "onFinishInflate");
            super.onFinishInflate();
    
            int cCount = getChildCount();
    
            if (cCount == 0)
                return;
    
            for (int i = 0; i < cCount; i++) {
                View view = getChildAt(i);
                LayoutParams lp = (LayoutParams) view.getLayoutParams();
                lp.weight = 0;
                lp.width = getScreenWidth() / mTabVisibleCount;
                view.setLayoutParams(lp);
            }
            // 设置点击事件
            setItemClickEvent();
    
        }
    
        /**
         * 获得屏幕的宽度
         *
         * @return
         */
        public int getScreenWidth() {
            WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
            DisplayMetrics outMetrics = new DisplayMetrics();
            wm.getDefaultDisplay().getMetrics(outMetrics);
            return outMetrics.widthPixels;
        }
    
    }
    

    这个类在很大程度上参考了鸿洋大神的博客,并因此处所需就scroll方法做了调整,这里在再一次庆幸我们可以站在巨人的肩膀上前行。

    代码可以看到,这个scroll方法是在ViewPager的onPageScrolled方法中调用,也就是随着ViewPager的滑动,同时实现顶部Indicator的滑动,这里要确保当前点击某项或者是ViewPager滑动后对应的Indicator应该始终处在水平中间的位置,明白这点结合当前所需显示tab的个数,即可确定scroll滑动距离的计算方法;这个其实很简单,多做几次尝试就可以总结出规律来,这里就不再详细分析。

    计时器的实现

    第一次见到这个抢购页面时,感觉很简单,不就是一个计时器做倒计时吗?而自己尝试后却发现有很多问题。首先这里使用计时器必然要用到线程,那么这个线程什么时候开始呢?又在什么时候结束呢?而且就算知道了,能确保每一次都能顺利的结束线程吗?(结束一个线程应该是一件很niubility的事情)

    所以经过各种尝试后,实现如下

    public class VpSimpleFragment extends Fragment {
        public static final String BUNDLE_DATA = "bean";
        private TimeBean bean;
        private View rootView;
        private TextView TimerH, TimerM, TimerS;
        private TextView statusTv;
    
        //计时器
        private int TotalCount = 360;
        private ScheduledExecutorService scheduledExecutorService;
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState)
    
        {
    
            rootView = inflater.inflate(R.layout.fragment_vs, null);
            Bundle arguments = getArguments();
            if (arguments != null) {
                bean = (TimeBean) arguments.getSerializable(BUNDLE_DATA);
            }
            InitView();
            setData();
            return rootView;
    
        }
    
        /**
         * 这里针对不同的场次,随机设定一个计数值(实际开发中可从服务器获取这个计数值)
         */
        private void setData() {
            String temp="";
            switch (bean.getStatus()) {
                case 0:
                    temp = "距离下场开始";
                    TotalCount=2490;
                    break;
                case 1:
                    temp = "距离本次结束";
                    TotalCount=1189;
                    break;
                case 2:
                    temp = "距离本场开始";
                    TotalCount=3311;
                    break;
                default:
                    break;
            }
            statusTv.setText(temp);
        }
    
        private void InitView() {
            TimerH = V.f(rootView, R.id.TimerH);
            TimerM = V.f(rootView, R.id.TimerM);
            TimerS = V.f(rootView, R.id.TimerS);
            statusTv = V.f(rootView, R.id.statuTv);
    
            scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
            // 每隔一秒执行一次task任务
            scheduledExecutorService.scheduleAtFixedRate(task, 1, 1, TimeUnit.SECONDS);
    
        }
    
        final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 1:
                        int STv = TotalCount % 60;
                        int MTv = TotalCount / 60 % 60;
                        int Htv = TotalCount / 3600;
    
                        if (STv < 10) {
                            TimerS.setText("0" + STv);
                        } else {
                            TimerS.setText("" + STv);
                        }
    
                        if (MTv < 10) {
                            TimerM.setText("0" + MTv);
                        } else {
                            TimerM.setText("" + MTv);
    
                        }
    
                        if (Htv < 10) {
                            TimerH.setText("0" + Htv);
                        } else {
                            TimerH.setText("" + Htv);
    
                        }
    
                        if (TotalCount <= 0) {
                            task.cancel();
                            scheduledExecutorService.shutdown();
                        }
                }
            }
        };
    
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                TotalCount--;
                Message message = new Message();
                message.what = 1;
                handler.sendMessage(message);
            }
        };
    
        public static VpSimpleFragment newInstance(TimeBean bean) {
            Bundle bundle = new Bundle();
            bundle.putSerializable(BUNDLE_DATA, bean);
            VpSimpleFragment fragment = new VpSimpleFragment();
            fragment.setArguments(bundle);
            return fragment;
        }
    
        @Override
        public void onPause() {
            super.onPause();
            //这一句很关键,确保在fragment之间进行切换时,定时器不会出现混乱
            scheduledExecutorService.shutdown();
        }
    }
    

    这里有些同学可能会说,何必用计时器自己算,获取手机当前时间减去活动时间的,不就是剩下的时间了吗?理论上是这样,但是这种做法是有问题的。首先,如果秒杀活动结束时间为22:00,有人刻意将手机时间又20:50改为20:00,那么这个时候活动倒计时怎么向这类用户解释呢?很有可能就被恶意用户钻了空子;另一方面,就算没有人恶意更改手机时间,那也无法保证手机时间和服务器的时间一致,这也会导致许多说不清的问题。所以,由服务器返回值,做倒计时计数是比较可取的方法。

    • 倒计时更新时间

    这里的倒计时实现很好理解,开启一个线程不断的将总计数值做减法(直到归零为止),然后通过handler更新TextView的内容,并根据当前页面的内容,显示不同的指示信息。

    • findViewByID的封装

    上面代码里中:

    TimerH = V.f(rootView, R.id.TimerH);
    

    这个的作用其实就是findViewByID,只不过是对其做了一下封装,免得每次写那么一长串,还得加上强制类型转换(真不知道官方这么做的意义是什么,为什么不在findViewByID内部自己实现强制类型转换),这里也分享一下这个方法

    V.class

    public class V {
    
        /**
         * activity.findViewById()
         * 
         * @param context
         * @param id
         * @return
         */
        public static <T extends View> T f(Activity context, int id) {
            return (T) context.findViewById(id);
        }
    
        /**
         * rootView.findViewById()
         * 
         * @param rootView
         * @param id
         * @return
         */
    
        public static <T extends View> T f(View rootView, int id) {
            return (T) rootView.findViewById(id);
        }
    

    Mainactivity中使用

    public class MainActivity extends FragmentActivity {
        private List<Fragment> mTabContents = new ArrayList<Fragment>();
        private FragmentPagerAdapter mAdapter;
        private ViewPager mViewPager;
        private List<String> mDatas = new ArrayList<>();
        private List<TimeBean> jsonbean = new ArrayList<>();
    
        private ViewPagerIndicator mIndicator;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            requestWindowFeature(Window.FEATURE_NO_TITLE);
            setContentView(R.layout.vp_indicator);
    
            initView();
            getDataFromServer();
            initDatas();
    
        }
    
        /**
         * 此处设定虚拟数据(实际应该从服务器获取)
         */
        private void getDataFromServer() {
            TimeBean time1=new TimeBean();
            time1.setStatus(0);
            time1.setTime("08:00");
            TimeBean time2=new TimeBean();
            time2.setStatus(0);
            time2.setTime("10:00");
            TimeBean time3=new TimeBean();
            time3.setStatus(1);
            time3.setTime("12:00");
            TimeBean time4=new TimeBean();
            time4.setStatus(2);
            time4.setTime("18:00");
            TimeBean time5=new TimeBean();
            time5.setStatus(2);
            time5.setTime("22:00");
    
            jsonbean.add(time1);
            jsonbean.add(time2);
            jsonbean.add(time3);
            jsonbean.add(time4);
            jsonbean.add(time5);
        }
    
        private void initDatas() {
            int postion=0;
            for(int i=0;i<jsonbean.size();i++){
                String str="";
                switch (jsonbean.get(i).getStatus()){
                    case 0:
                        str=jsonbean.get(i).getTime()+"\n已结束";
                        break;
                    case 1:
                        str=jsonbean.get(i).getTime()+"\n进行中";
                        break;
                    case 2:
                        str=jsonbean.get(i).getTime()+"\n即将开始";
                        break;
                    default:
                        break;
                }
                mDatas.add(str);
    
                //将正在进行的活动位置进行标记
                if(jsonbean.get(i).getStatus()==1){
                    postion=i;
                }
            }
    
            for (TimeBean data : jsonbean) {
                VpSimpleFragment fragment = VpSimpleFragment.newInstance(data);
                mTabContents.add(fragment);
            }
    
            mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
                @Override
                public int getCount() {
                    return mTabContents.size();
                }
    
                @Override
                public Fragment getItem(int position) {
                    return mTabContents.get(position);
                }
            };
    
            //设置Tab上的标题
            mIndicator.setTabItemTitles(mDatas);
            mViewPager.setAdapter(mAdapter);
            //设置关联的ViewPager,并且显示为当前正在进行的fragment页面,确保每次进入页面,正好显示正在进行的活动
            mIndicator.setViewPager(mViewPager, postion);
        }
    
        private void initView() {
            mViewPager = (ViewPager) findViewById(R.id.id_vp);
            mIndicator = (ViewPagerIndicator) findViewById(R.id.id_indicator);
        }
    
    }
    

    这里vp_indicator这个布局文件很简单,就是Indicator下面放一个ViewPager,很典型的一种使用方法,这里就不再列出。


    好了,这个就是对秒杀活动页面的一些理解,当然实际应用中比这里要复杂很多,参与秒杀的时间段、计时数值的获取是动态的,因此这里只是就所遇到的问题做一些浅显的记录,方便日后使用。

    相关文章

      网友评论

      本文标题:仿购物类APP秒杀活动页面

      本文链接:https://www.haomeiwen.com/subject/hdfrlttx.html