Android仿京东、天猫商品详情页

前言

前面在介绍控件TabLayout控件和CoordinatorLayout使用的时候说了下实现京东、天猫详情页面的效果,今天要说的是优化版,是我们线上实现的效果,首先看一张效果:


项目结构分析

首先我们来分析一下要实现上面的效果,我们需要怎么做。顶部是一个可以滑动切换Tab,可以用ViewPager+Fragment实现,也可以使用系统的TabLayout控件实现;而下面的 View是一个可以滑动拖动效果的View,可以采用网上一个叫做DragLayout的控件,我这里是自己实现了一个,主要是通过对View的事件分发的一些处理;然后滑动到下面就是一个图文详情的View(Fragment),本页面包含两个界面:详情页面和参数页面;最后是评价的View(Fragment)。经过上面的分析,我们的界面至少需要4个Fragement,首先来看一下项目结构:

代码讲解

代码比较多,这里只讲解几个核心的方法类。首先我们来看一下我们自己是的这个具有阻尼效果的View,我们知道要实现的效果,我们需要对View的事件做一个全面的实现。这里首先说一下View的事件分发的流程:

onInterceptTouchEvent()–>dispatchTouchEvent()–>onTouchEvent();

首先我们需要对View传过来的事件做一个拦截:

ensureTarget();
  if (null == mTarget) {
   return false;
  }
  if (!isEnabled()) {
   return false;
  }
  final int aciton = MotionEventCompat.getActionMasked(ev);
  boolean shouldIntercept = false;
  switch (aciton) {
   case MotionEvent.ACTION_DOWN: {
    mInitMotionX = ev.getX();
    mInitMotionY = ev.getY();
    shouldIntercept = false;
    break;
   }
   case MotionEvent.ACTION_MOVE: {
    final float x = ev.getX();
    final float y = ev.getY();

    final float xDiff = x - mInitMotionX;
    final float yDiff = y - mInitMotionY;

    if (canChildScrollVertically((int) yDiff)) {
     shouldIntercept = false;
    } else {
     final float xDiffabs = Math.abs(xDiff);
     final float yDiffabs = Math.abs(yDiff);

     if (yDiffabs > mTouchSlop && yDiffabs >= xDiffabs
       && !(mStatus == Status.CLOSE && yDiff > 0
       || mStatus == Status.OPEN && yDiff < 0)) {
      shouldIntercept = true;
     }
    }
    break;
   }
   case MotionEvent.ACTION_UP:
   case MotionEvent.ACTION_CANCEL: {
    shouldIntercept = false;
    break;
   }
  }
  return shouldIntercept;

最后转发给onTouchEvent

ensureTarget();
  if (null == mTarget) {
   return false;
  }
  if (!isEnabled()) {
   return false;
  }
  boolean wantTouch = true;
  final int action = MotionEventCompat.getActionMasked(ev);
  switch (action) {
   case MotionEvent.ACTION_DOWN: {
    if (mTarget instanceof View) {
     wantTouch = true;
    }
    break;
   }

   case MotionEvent.ACTION_MOVE: {
    final float y = ev.getY();
    final float yDiff = y - mInitMotionY;
    if (canChildScrollVertically(((int) yDiff))) {
     wantTouch = false;
    } else {
     processTouchEvent(yDiff);
     wantTouch = true;
    }
    break;
   }
   case MotionEvent.ACTION_UP:
   case MotionEvent.ACTION_CANCEL: {
    finishTouchEvent();
    wantTouch = false;
    break;
   }
  }
  return wantTouch;

滑动事件完了之后我们需要调用request方法对View做一个重绘:

final int left = l;
  final int right = r;
  int top;
  int bottom;
  final int offset = (int) mSlideOffset;
  View child;
  for (int i = 0; i < getChildCount(); i++) {
   child = getChildAt(i);
   if (child.getVisibility() == GONE) {
    continue;
   }
   if (child == mBehindView) {
    top = b + offset;
    bottom = top + b - t;
   } else {
    top = t + offset;
    bottom = b + offset;
   }
   child.layout(left, top, right, bottom);
  }

上下滑动也是涉及到两个界面:mFrontView和mBehindView,然后通过判断滑动事件来显示哪一个View。具体看代码:

package com.xzh.gooddetail.view;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;

import com.xzh.gooddetail.R;

public class SlideDetailsLayout extends ViewGroup {

 public interface OnSlideDetailsListener {
  void onStatusChanged(Status status);
 }

 public enum Status {
  CLOSE,
  OPEN;

  public static Status valueOf(int stats) {
   if (0 == stats) {
    return CLOSE;
   } else if (1 == stats) {
    return OPEN;
   } else {
    return CLOSE;
   }
  }
 }

 private static final float DEFAULT_PERCENT = 0.2f;
 private static final int DEFAULT_DURATION = 300;

 private View mFrontView;
 private View mBehindView;

 private float mTouchSlop;
 private float mInitMotionY;
 private float mInitMotionX;

 private View mTarget;
 private float mSlideOffset;
 private Status mStatus = Status.CLOSE;
 private boolean isFirstShowBehindView = true;
 private float mPercent = DEFAULT_PERCENT;
 private long mDuration = DEFAULT_DURATION;
 private int mDefaultPanel = 0;

 private OnSlideDetailsListener mOnSlideDetailsListener;

 public SlideDetailsLayout(Context context) {
  this(context, null);
 }

 public SlideDetailsLayout(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
 }

 public SlideDetailsLayout(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);

  TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlideDetailsLayout, defStyleAttr, 0);
  mPercent = a.getFloat(R.styleable.SlideDetailsLayout_percent, DEFAULT_PERCENT);
  mDuration = a.getInt(R.styleable.SlideDetailsLayout_duration, DEFAULT_DURATION);
  mDefaultPanel = a.getInt(R.styleable.SlideDetailsLayout_default_panel, 0);
  a.recycle();

  mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
 }

 public void setOnSlideDetailsListener(OnSlideDetailsListener listener) {
  this.mOnSlideDetailsListener = listener;
 }

 public void smoothOpen(boolean smooth) {
  if (mStatus != Status.OPEN) {
   mStatus = Status.OPEN;
   final float height = -getMeasuredHeight();
   animatorSwitch(0, height, true, smooth ? mDuration : 0);
  }
 }

 public void smoothClose(boolean smooth) {
  if (mStatus != Status.CLOSE) {
   mStatus = Status.CLOSE;
   final float height = -getMeasuredHeight();
   animatorSwitch(height, 0, true, smooth ? mDuration : 0);
  }
 }

 @Override
 protected LayoutParams generateDefaultLayoutParams() {
  return new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT);
 }

 @Override
 public LayoutParams generateLayoutParams(AttributeSet attrs) {
  return new MarginLayoutParams(getContext(), attrs);
 }

 @Override
 protected LayoutParams generateLayoutParams(LayoutParams p) {
  return new MarginLayoutParams(p);
 }

 @Override
 protected void onFinishInflate() {
  final int childCount = getChildCount();
  if (1 >= childCount) {
   throw new RuntimeException("SlideDetailsLayout only accept childs more than 1!!");
  }
  mFrontView = getChildAt(0);
  mBehindView = getChildAt(1);
  if (mDefaultPanel == 1) {
   post(new Runnable() {
    @Override
    public void run() {
     smoothOpen(false);
    }
   });
  }
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  final int pWidth = MeasureSpec.getSize(widthMeasureSpec);
  final int pHeight = MeasureSpec.getSize(heightMeasureSpec);

  final int childWidthMeasureSpec =
    MeasureSpec.makeMeasureSpec(pWidth, MeasureSpec.EXACTLY);
  final int childHeightMeasureSpec =
    MeasureSpec.makeMeasureSpec(pHeight, MeasureSpec.EXACTLY);

  View child;
  for (int i = 0; i < getChildCount(); i++) {
   child = getChildAt(i);
   if (child.getVisibility() == GONE) {
    continue;
   }
   measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);
  }
  setMeasuredDimension(pWidth, pHeight);
 }

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
  final int left = l;
  final int right = r;
  int top;
  int bottom;
  final int offset = (int) mSlideOffset;
  View child;
  for (int i = 0; i < getChildCount(); i++) {
   child = getChildAt(i);
   if (child.getVisibility() == GONE) {
    continue;
   }
   if (child == mBehindView) {
    top = b + offset;
    bottom = top + b - t;
   } else {
    top = t + offset;
    bottom = b + offset;
   }
   child.layout(left, top, right, bottom);
  }
 }

 @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
  ensureTarget();
  if (null == mTarget) {
   return false;
  }
  if (!isEnabled()) {
   return false;
  }
  final int aciton = MotionEventCompat.getActionMasked(ev);
  boolean shouldIntercept = false;
  switch (aciton) {
   case MotionEvent.ACTION_DOWN: {
    mInitMotionX = ev.getX();
    mInitMotionY = ev.getY();
    shouldIntercept = false;
    break;
   }
   case MotionEvent.ACTION_MOVE: {
    final float x = ev.getX();
    final float y = ev.getY();

    final float xDiff = x - mInitMotionX;
    final float yDiff = y - mInitMotionY;

    if (canChildScrollVertically((int) yDiff)) {
     shouldIntercept = false;
    } else {
     final float xDiffabs = Math.abs(xDiff);
     final float yDiffabs = Math.abs(yDiff);

     if (yDiffabs > mTouchSlop && yDiffabs >= xDiffabs
       && !(mStatus == Status.CLOSE && yDiff > 0
       || mStatus == Status.OPEN && yDiff < 0)) {
      shouldIntercept = true;
     }
    }
    break;
   }
   case MotionEvent.ACTION_UP:
   case MotionEvent.ACTION_CANCEL: {
    shouldIntercept = false;
    break;
   }
  }
  return shouldIntercept;
 }

 @Override
 public boolean onTouchEvent(MotionEvent ev) {
  ensureTarget();
  if (null == mTarget) {
   return false;
  }
  if (!isEnabled()) {
   return false;
  }
  boolean wantTouch = true;
  final int action = MotionEventCompat.getActionMasked(ev);
  switch (action) {
   case MotionEvent.ACTION_DOWN: {
    if (mTarget instanceof View) {
     wantTouch = true;
    }
    break;
   }

   case MotionEvent.ACTION_MOVE: {
    final float y = ev.getY();
    final float yDiff = y - mInitMotionY;
    if (canChildScrollVertically(((int) yDiff))) {
     wantTouch = false;
    } else {
     processTouchEvent(yDiff);
     wantTouch = true;
    }
    break;
   }
   case MotionEvent.ACTION_UP:
   case MotionEvent.ACTION_CANCEL: {
    finishTouchEvent();
    wantTouch = false;
    break;
   }
  }
  return wantTouch;
 }

 private void processTouchEvent(final float offset) {
  if (Math.abs(offset) < mTouchSlop) {
   return;
  }

  final float oldOffset = mSlideOffset;
  if (mStatus == Status.CLOSE) {
   // reset if pull down
   if (offset >= 0) {
    mSlideOffset = 0;
   } else {
    mSlideOffset = offset;
   }

   if (mSlideOffset == oldOffset) {
    return;
   }

  } else if (mStatus == Status.OPEN) {
   final float pHeight = -getMeasuredHeight();
   if (offset <= 0) {
    mSlideOffset = pHeight;
   } else {
    final float newOffset = pHeight + offset;
    mSlideOffset = newOffset;
   }

   if (mSlideOffset == oldOffset) {
    return;
   }
  }
  requestLayout();
 }

 private void finishTouchEvent() {
  final int pHeight = getMeasuredHeight();
  final int percent = (int) (pHeight * mPercent);
  final float offset = mSlideOffset;

  boolean changed = false;

  if (Status.CLOSE == mStatus) {
   if (offset <= -percent) {
    mSlideOffset = -pHeight;
    mStatus = Status.OPEN;
    changed = true;
   } else {
    mSlideOffset = 0;
   }
  } else if (Status.OPEN == mStatus) {
   if ((offset + pHeight) >= percent) {
    mSlideOffset = 0;
    mStatus = Status.CLOSE;
    changed = true;
   } else {
    mSlideOffset = -pHeight;
   }
  }

  animatorSwitch(offset, mSlideOffset, changed);
 }

 private void animatorSwitch(final float start, final float end) {
  animatorSwitch(start, end, true, mDuration);
 }

 private void animatorSwitch(final float start, final float end, final long duration) {
  animatorSwitch(start, end, true, duration);
 }

 private void animatorSwitch(final float start, final float end, final boolean changed) {
  animatorSwitch(start, end, changed, mDuration);
 }

 private void animatorSwitch(final float start,
        final float end,
        final boolean changed,
        final long duration) {
  ValueAnimator animator = ValueAnimator.ofFloat(start, end);
  animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
   @Override
   public void onAnimationUpdate(ValueAnimator animation) {
    mSlideOffset = (float) animation.getAnimatedValue();
    requestLayout();
   }
  });
  animator.addListener(new AnimatorListenerAdapter() {
   @Override
   public void onAnimationEnd(Animator animation) {
    super.onAnimationEnd(animation);
    if (changed) {
     if (mStatus == Status.OPEN) {
      checkAndFirstOpenPanel();
     }

     if (null != mOnSlideDetailsListener) {
      mOnSlideDetailsListener.onStatusChanged(mStatus);
     }
    }
   }
  });
  animator.setDuration(duration);
  animator.start();
 }

 private void checkAndFirstOpenPanel() {
  if (isFirstShowBehindView) {
   isFirstShowBehindView = false;
   mBehindView.setVisibility(VISIBLE);
  }
 }

 private void ensureTarget() {
  if (mStatus == Status.CLOSE) {
   mTarget = mFrontView;
  } else {
   mTarget = mBehindView;
  }
 }

 protected boolean canChildScrollVertically(int direction) {
  if (mTarget instanceof AbsListView) {
   return canListViewSroll((AbsListView) mTarget);
  } else if (mTarget instanceof FrameLayout ||
    mTarget instanceof RelativeLayout ||
    mTarget instanceof LinearLayout) {
   View child;
   for (int i = 0; i < ((ViewGroup) mTarget).getChildCount(); i++) {
    child = ((ViewGroup) mTarget).getChildAt(i);
    if (child instanceof AbsListView) {
     return canListViewSroll((AbsListView) child);
    }
   }
  }

  if (android.os.Build.VERSION.SDK_INT < 14) {
   return ViewCompat.canScrollVertically(mTarget, -direction) || mTarget.getScrollY() > 0;
  } else {
   return ViewCompat.canScrollVertically(mTarget, -direction);
  }
 }

 protected boolean canListViewSroll(AbsListView absListView) {
  if (mStatus == Status.OPEN) {
   return absListView.getChildCount() > 0
     && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
     .getTop() <
     absListView.getPaddingTop());
  } else {
   final int count = absListView.getChildCount();
   return count > 0
     && (absListView.getLastVisiblePosition() < count - 1
     || absListView.getChildAt(count - 1)
     .getBottom() > absListView.getMeasuredHeight());
  }
 }

 @Override
 protected Parcelable onSaveInstanceState() {
  SavedState ss = new SavedState(super.onSaveInstanceState());
  ss.offset = mSlideOffset;
  ss.status = mStatus.ordinal();
  return ss;
 }

 @Override
 protected void onRestoreInstanceState(Parcelable state) {
  SavedState ss = (SavedState) state;
  super.onRestoreInstanceState(ss.getSuperState());
  mSlideOffset = ss.offset;
  mStatus = Status.valueOf(ss.status);

  if (mStatus == Status.OPEN) {
   mBehindView.setVisibility(VISIBLE);
  }

  requestLayout();
 }

 static class SavedState extends BaseSavedState {

  private float offset;
  private int status;

  public SavedState(Parcel source) {
   super(source);
   offset = source.readFloat();
   status = source.readInt();
  }

  public SavedState(Parcelable superState) {
   super(superState);
  }

  @Override
  public void writeToParcel(Parcel out, int flags) {
   super.writeToParcel(out, flags);
   out.writeFloat(offset);
   out.writeInt(status);
  }

  public static final Creator<SavedState> CREATOR =
    new Creator<SavedState>() {
     public SavedState createFromParcel(Parcel in) {
      return new SavedState(in);
     }

     public SavedState[] newArray(int size) {
      return new SavedState[size];
     }
    };
 }
}

接下来就是一些Fragment等的页面填充,也没啥好讲的,代码又很多可以优化的地方,在优化的地方,笔者也列出了优化的方案,大家可以根据自己的实际情况做页面级的优化。

源码下载:http://xiazai.jb51.net/201701/yuanma/AndriodGoodDetail(jb51.net).rar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android 仿京东、拼多多商品分类页的示例代码

    最近接了一个项目,要仿照京东写一个商品分类页,但需要滑动右边子分类,左边的主分类也跟着变换,写了个demo,需要的同学可以自取. 先放一个写完之后的样子: 写这个需求的思路也很清晰,首先左边肯定是一个listView,右边也是一个listView,这两个listView要达到一个联动的效果.右边的listView再嵌套一个GridView即可.如下图所示. 所以,我们需要的数据结构也就确定了,应该是数组套数组,也就说护肤大分类下又有子分类商品,类似于这个样子: ok,数据和UI结构确定了,就可以

  • 安卓(android)仿电商app商品详情页按钮浮动效果

    1.效果图如下: 这效果用户体验还是很酷炫,今天我们就来讲解如何实现这个效果. 2.分析 为了方便理解,作图分析 如图所示,整个页面分为四个部分: 1.悬浮内容,floatView 2.顶部内容,headView 3.中间内容,与悬浮内容相同,middleView 4.商品详情展示页面,detailView 因为页面内容高度会超出屏幕,所以用Scrollview实现滚动,悬浮view与scrollview同级,都在一个帧布局或者相对布局中. 当y方向的滚动距离小于中间的内容middleView到

  • Android实现商品展示效果

    一. 创建手机界面布局 创建一个activity_main.xml文件代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" androi

  • Android仿淘宝切换商品列表布局效果的示例代码

    最近电商项目中有这样一个需求,就是在进入商品列表界面,有一个按钮可以切换商品列表的布局(网格或者垂直列表排列). 效果图: 上面两幅图分别是点击右上角按钮后显示两种不同布局的效果.简单的流程可以概括为:第一次进入页面,有个默认的布局(网格布局),点击按钮,由网格布局切换到竖直的线性布局,再次点击切换到网格布局. 分析: 可以看到商品展示的形式都是以列表的方式来展现,我用的是RecyclerView,这种列表并不复杂,配合Adapter数据适配器就实现了. 提出这个需求时,问了朋友,他说使用了两个

  • Android 仿淘宝商品属性标签页

    需求 1.动态加载属性,如尺码,颜色,款式等 由于每件商品的属性是不确定的,有的商品的属性是颜色和尺码,有的是口味,有的是大小,所以这些属性不能直接写死到页面上. 2.动态加载属性下的标签 每个属性下的标签个数也不是一定的,比如有的商品的尺码是是S,M,XL,有的是均码,也就是每种属性的具体的内容是不一定的. 技术点 自定义ViewGroup,使其中的TextView可以依据内容长短自动换行,如下图所示 实现 布局 通过ListView来显示商品所有属性,每种属性作为ListView的Item.

  • Android仿淘宝商品浏览界面图片滚动效果

    用手机淘宝浏览商品详情时,商品图片是放在后面的,在第一个ScrollView滚动到最底下时会有提示,继续拖动才能浏览图片.仿照这个效果写一个出来并不难,只要定义一个Layout管理两个ScrollView就行了,当第一个ScrollView滑到底部时,再次向上滑动进入第二个ScrollView.效果如下: 需要注意的地方是: 1.如果是手动滑到底部需要再次按下才能继续往下滑,自动滚动到底部则不需要 2.在由上一个ScrollView滑动到下一个ScrollView的过程中多只手指相继拖动也不会导

  • Android仿淘宝商品详情页效果

    本文实例为大家分享了Android仿淘宝商品详情页的具体代码,供大家参考,具体内容如下 Demo地址:先上效果图 效果就是上面图片的效果 接下来看看如何实现 首先我们来看下布局文件 <LinearLayout android:id="@+id/header" android:layout_width="match_parent" android:layout_height="72dp" android:paddingTop="24

  • Android仿淘宝商品详情页

    看到有人在问如何实现淘宝商品详情页效果,献上效果图 大致梳理一下思路,这里不提供源码 状态栏透明使用开源库StatusBarCompat,为了兼容手机4.4 dependencies { compile ('com.github.niorgai:StatusBarCompat:2.1.4', { exclude group: 'com.android.support' }) } allprojects { repositories { ... maven { url "https://jitpa

  • Android仿京东手机端类别页

    京东手机端的类别标签页, 是一个左侧滑动可选择类别, 右侧一个类别明细的列表联动页面. 当用户选择左侧选项, 可在右侧显示更多选项来选择. 实现方式也不少. 最常见的当然是左侧和右侧各一个Fragment, 左侧Fragment放置ListView, 右侧放显示类别明细的Fragment. 如果觉得页面包含的Fragment太多, 左侧直接给一个ListView就可以了.不影响效果. 效果图: 例子中值得注意的三点: 左侧列表点击某个Item可以自动上下滑动,使所点击的item自动移至列表中间

  • Android仿京东、天猫商品详情页

    前言 前面在介绍控件TabLayout控件和CoordinatorLayout使用的时候说了下实现京东.天猫详情页面的效果,今天要说的是优化版,是我们线上实现的效果,首先看一张效果: 项目结构分析 首先我们来分析一下要实现上面的效果,我们需要怎么做.顶部是一个可以滑动切换Tab,可以用ViewPager+Fragment实现,也可以使用系统的TabLayout控件实现:而下面的 View是一个可以滑动拖动效果的View,可以采用网上一个叫做DragLayout的控件,我这里是自己实现了一个,主要

  • Android 仿淘宝、京东商品详情页向上拖动查看图文详情控件DEMO详解

    一.淘宝商品详情页效果 我们的效果 二.实现思路 使用两个scrollView,两个scrollView 竖直排列,通过自定义viewGroup来控制两个scrollView的竖直排列,以及滑动事件的处理.如下图 三.具体实现 1.继承viewGroup自定义布局View 重写onMeasure()和onLayout方法,在onLayout方法中完成对两个子ScrollView的竖直排列布局,代码如下: 布局文件: <RelativeLayout xmlns:android="http:/

  • android仿京东商品属性筛选功能

    筛选和属性选择是目前非常常用的功能模块:几乎所有的APP中都会使用: 点击筛选按钮会弹出一个自己封装好的popupWindow,实用方法非常简单:两行代码直接显示:(当然初始化数据除外) 这里和以前用到的流式布局有些不一样:流式布局 以前使用的是单个分类,而且也没有在项目中大量实用:这个筛选功能除了数据外几乎都是从项目中Copy出来的: 整个popupWindow布局就是一个自定义的ListView,这个自定义的listview主要是控制listview的高度: 如果数据少的话就是自适应,如果数

  • Android自定义ViewGroup实现淘宝商品详情页

    最近公司在新版本上有一个需要,要在首页添加一个滑动效果,具体就是仿照X宝的商品详情页,拉到页面底部时有一个粘滞效果,如下图X东的商品详情页,如果用户继续向上拉的话就进入商品图文描述界面: 刚开始是想拿来主义,直接从网上找个现成的demo来用, 但是网上无一例外的答案都特别统一: 几乎全部是ScrollView中再套两个ScrollView,或者是一个LinearLayout中套两个ScrollView. 通过指定父view和子view的focus来切换滑动的处理界面---即通过view的requ

  • vue3封装京东商品详情页放大镜效果组件

    本文实例为大家分享了vue3封装类似京东商品详情页放大镜效果组件的具体代码,供大家参考,具体内容如下 首先先完成基本布局 完成图片的切换效果,通过 mouseenter 事件切换图片 落地代码 <template> <div class="goods-image"> <!-- 预览大图 --> <div class="large" v-show="show" :style="[{ backgro

  • Android仿京东分类模块左侧分类条目效果

    本文实例为大家分享了Android仿京东左侧分类条目效果的具体代码,供大家参考,具体内容如下 import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; import com.frame.R;

随机推荐