热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

android有阻尼下拉刷新列表的实现方法

下面小编就为大家分享一篇android有阻尼下拉刷新列表的实现方法,具有很好的参考价值,希望对大家有所帮助,一起跟随小编过来看看吧

本文将会介绍有阻尼下拉刷新列表的实现,先来看看效果预览:

这是下拉状态:

这是下拉松开手指后listView回滚到刷新状态时的样子:

1. 如何调用

虽然效果图看起来样子不太好看,主要是因为那个蓝色的背景对不对,没关系,这只是一个背景而已,在了解了我们这个下拉刷新列表的实现之后,你就可以很轻松地修改这个背景,从而实现你想要的UI效果!话不多说,下面我们先来讲讲这个下拉刷新列表是如何使用的,这也是我们编写代码所要实现的目标。

    final PullToRefreshListView eListView = (PullToRefreshListView) rootView.findViewById(R.id.profile_listView);
    eListView.setOnLoadCallBack(new PullToRefreshListView.OnLoadCallBack() {
      @Override
      public int whereToLoad() {
        return PullToRefreshListView.DEFAULT_WHERE_TO_LOAD;
      }
      @Override
      public void onLoad() {
        eListView.postDelayed(new Runnable() {
          @Override
          public void run() {
            eListView.setLoadingFinish();
          }
        }, 5000);
      }
      @Override
      public void cancelLoad() {
      }
      @Override
      public Drawable refreshDrawable() {
        return new ColorDrawable(Color.CYAN);
      }
    });
    eListView.setAdapter(new BaseAdapter() {
      @Override
      public int getCount() {
        return 30;
      }
      @Override
      public Object getItem(int position) {
        return null;
      }
      @Override
      public long getItemId(int position) {
        return 0;
      }
      @Override
      public View getView(int position, View convertView, ViewGroup parent) {
        TextView tv;
        if (cOnvertView== null) {
          tv = new TextView(getActivity());
          tv.setGravity(Gravity.CENTER_VERTICAL);
          tv.setHeight(200);
          tv.setBackgroundColor(Color.WHITE);
        } else {
          tv = (TextView) convertView;
        }
        tv.setText(position+"");
        return tv;
      }
    });

在上述代码中,我们可以看到PullToRefreshListView的使用在adapter上跟ListView是一样的,这个当然,因为我们实现下拉刷新功能并不需要修改数据适配器。我们也看到,PullToRefreshListView的实例需要设置一个OnLoadCallBack回调,该回调需要实现4个方法,包括:

  /**
   * 下拉刷新的回调
   */
  public interface OnLoadCallBack {
    /**
     * 下拉结束后将listView定位到哪个位置等待刷新完成
     * @return listView的定位y坐标值,in dp
     */
    int whereToLoad();
    /**
     * 下拉结束后进行刷新的回调
     */
    void onLoad();
    /**
     * 取消刷新
     */
    void cancelLoad();
    /**
     * 下拉刷新的背景
     * @return 背景drawable
     */
    Drawable refreshDrawable();
  }

whereToLoad方法告知PullToRefreshListView对象下拉刷新时停留在哪个位置,具体点说,也就是上述第二章效果图中蓝色背景的高度。onLoad方法是下拉刷新的回调,调用者可以在这里实现刷新动作。cancelLoad方法是取消刷新动作的回调,调用者需要在这里将刷新动作取消。

根据上述方法,我们可以猜测,在onLoad方法中执行的应该是一个线程或者AsyncTask,而在cancelLoad方法中要做的就是将这个线程或者AsyncTask取消掉。最后还有一个refreshDrawable方法,这个方法是为修改listView的背景而提供给调用者的,调用者可以返回任意一个喜欢的背景Drawable。

知道如何调用以后,我们就要一步一步地实现这个PullToRefreshListView了。

2. 在dispatchDraw中重画子View实现下拉视觉

PullToRefreshListView实现的关键在于重画该listVIew的子View。重画ViewGroup的子View一般是在dispatchDraw方法中实现的。因此,我们的PullToRefreshListView继承自ListView类,重载其dispatchDraw方法。

  @Override
  protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    if (distanceY > 0) {
      if (refreshDrawable == null) {
        refreshDrawable = onLoadCallBack.refreshDrawable();
      }
      if (refreshDrawable == null) {
        canvas.drawColor(Color.GRAY);
      } else {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        refreshDrawable.setBounds(left, top, getWidth()+left, getHeight()+top);
        refreshDrawable.draw(canvas);
      }
      canvas.save();
      canvas.translate(getPaddingLeft(), getPaddingTop() + distanceY);
      for (int i=0;i

重画子View的关键在于这一句代码:

canvas.translate(getPaddingLeft(), getPaddingTop() + distanceY); 

在重画子View之前,我们需要先将canvas向上移动distanceY距离。这是为什么呢?我们先来看看在canvas画子View的方法

drawChild方法的文档是怎么说的。

protected boolean drawChild (Canvas canvas, View child, long drawingTime)

Added in API level 1 Draw one child of this View Group. This method is responsible for getting the canvas in the right state. This includes clipping, translating so that the child's scrolled origin is at 0, 0, and applying any animation transformations.

Parameters canvas The canvas on which to draw the child child Who to draw drawingTime The time at which draw is occurring Returns True if an invalidate() was issued

我来翻译一下,drawChild方法可以画出这个View Group的一个子View。该方法需要使canvas处于一个正确的状态,该状态就

是通过对canvas进行clip裁剪,translate评议操作等以使得该子View位于canvas的(0,0)位置。

什么意思呢?简单来说就是,drawChild方法会将child view画在canvas的(0,0)位置,因此为了使得该child view位于

canvas的正确位置,我们需要在重画之前对canvas进行裁剪平移等操作。举个例子,有一个canvas和一个child view,本来

child view要画在(0,0)位置上,于是呈现在我们眼前的child view就是位于canvas的顶部,但是如果在画之前我们将

canvas向上移动100个像素单位,然后再将child view画在(0,0)位置上,那么呈现在我们眼前的child view的位置将会是

位于canvas的(0,100)位置上。

根据以上分析,我们可以知道,重画子View的原理就是:

当PullToRefreshListView已经滚动到顶部的时候,通过监控滑动手势来计算distanceY,从而确定要将canvas向上移动多少再重画子View,就可以实现PullToRefreshListView跟随滑动手势进行下拉的功能了。

3. 计算下拉距离

实现了重画以后,我们需要做的就是如何计算distanceY。我们的初步想法是,根据滑动的距离来计算,考虑到我们要实现阻尼效果,即随着滑动距离的变长,PullToRefreshListView的下拉距离会越来越短。在PullToRefreshListView实现中,我使用指数函数来实现这一阻尼效果,具体计算如下:

distanceY = ev.getY() - pullStartY; 
distanceY = (float) (Math.exp(-ev.getY() / pullStartY / 40) * distanceY); 

我们知道负指数是加速度随距离变小的单调递增函数,我使用手指滑动距离计算负指数作为PullToRefreshListView的滑动距离的参考标准,便可以实现有阻尼下拉效果。

4. 监控手势判断ListView是否进入下拉状态并更新distanceY

更进一步,我们要实现的就是对手势的监控,在PullToRefreshListView中,我们在onTouchEvent方法中进行处理。

@Override 
public boolean onTouchEvent(MotionEvent ev) { 
  if (lastAction == -1 && ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 
    // 按下的时候 
    lastAction = MotionEvent.ACTION_DOWN; 
    cancelAnimating(); 
    L.d(TAG, "touch down"); 
  } else if (lastAction == MotionEvent.ACTION_MOVE && ev.getActionMasked() == MotionEvent.ACTION_UP) { 
    // 放开手指,开始回滚 
    isPulling = false; 
    lastAction = -1; 
    startAnimating(); 
    L.d(TAG, "touch up"); 
  } else if (lastAction == MotionEvent.ACTION_DOWN) { 
    if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) { 
      // 在按下手指的基础上,开始滑动 
      if (isTop && !isPulling) { 
        // listView在顶部而且不处于下拉刷新状态,开始下拉 
        pullStartY = ev.getY(); 
        lastAction = MotionEvent.ACTION_MOVE; 
        isPulling = true; 
      } 
    } 
  } else if (lastAction == MotionEvent.ACTION_MOVE) { 
    if (isTop) { 
      // 下拉 
      distanceY = ev.getY() - pullStartY; 
      L.d(TAG, distanceY + ""); 
      if (distanceY > 0) { 
        distanceY = (float) (Math.exp(-ev.getY() / pullStartY / 40) * distanceY); 
        // 在下拉状态时取消系统对move动作的响应,完全由本类响应 
        ev.setAction(MotionEvent.ACTION_DOWN); 
      } else { 
        distanceY = 0; 
        // 在下拉过程中往上拉动该listView使得其回到顶部位置,则将该move动作交由系统进行响应 
        ev.setAction(MotionEvent.ACTION_MOVE); 
      } 
    } else { 
      // 在下拉过程中往上拉动listView使listView往下滚动到其没有滚动到顶部,则取消其下拉状态,回到手指按下的初始状态 
      lastAction = MotionEvent.ACTION_DOWN; 
      isPulling = false; 
      distanceY = 0; 
    } 
  } 
  return super.onTouchEvent(ev); 
} 

这一段代码相对有一点复杂,我们慢慢解析。首先,我们有一个lastAction变量来记录上一个手势是什么,有一个isPulling变量来记录当前PullToRefreshListView是否处于下拉状态,有一个isTop变量记录当前PullToRefreshListView是否已经滚动到顶部。

在onTouchEvent方法的重载实现中,一开始PullToRefreshListView没有接受任何手势,然后当用户按下手指出发ACTION_DOWN事件时,我记录下这个动作,然后当用户进行滑动时,如果此时PullToRefreshListView没有“滚动到顶部”,则不做任何处理,反之则将lastAction更新为ACTION_MOVE状态,更新isPulling变量,记录当前手指的位置作为计算下拉距离的起始位置,开始下拉刷新,然后在下拉的过程中计算PullToRefreshListView下拉的距离以重画子View。

在这个手势处理的实现中,当用户在下拉过程中突然将PullToRefreshListView往上拉,如果将PullToRefreshListView 拉到不处于“滚动到顶部的状态”时,则重置下拉状态,使得:

lastAction = MotionEvent.ACTION_DOWN; 

于是PullToRefreshListView接下来的下滑手势响应权被交还给系统,知道用户又将PullToRefreshListView下拉到“滚动到顶部”状态,则又重新执行上述操作,使PullToRefreshListView进入下拉状态。

5. 如何判断ListView是否已经滚动到顶部

下一步,我们如何判断ListView是否处于“滚动到顶部”状态呢?这一问题我PullToRefreshListView的onScroll中解决。

    setOnScrollListener(new OnScrollListener() {
      @Override
      public void onScrollStateChanged(AbsListView view, int scrollState) {
      }
      @Override
      public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        // 没有子view的时候(没有数据,或者被拉到看不到子view),意味着该listView滚动到顶部
        if (getChildCount() == 0) {
          isTop = true;
          return;
        }
        if (firstVisibleItem == 0) {
          View firstView = getChildAt(0);
          if (firstView.getTop() + distanceY >= 0) {
            // 第一个view可见且其相对parent(该listView)的顶部距离大于等于0,意味着该listView也是滚动到顶部
            isTop = true;
            return;
          }
        }
        isTop = false;
      }
    });

为PullToRefreshListView设置一个OnScrollListener回调,并在其onScroll方法中监控其滚动位置,具体看注释也已经一目了然,我就不多解释了。

6. 下拉后的回滚动画

最后,当下拉结束松开手指时,我们需要为PullToRefreshListView执行一个回滚的动画,我们在onTouchEvent方法中看到:

    // ......
    else if (lastAction == MotionEvent.ACTION_MOVE && ev.getActionMasked() == MotionEvent.ACTION_UP) {
      // 放开手指,开始回滚
      isPulling = false;
      lastAction = -1;
      startAnimating();
      L.d(TAG, "touch up");
    }
    // ...... 

startAnimating方法的实现如下:

  /**
   * 下拉结束时进行回滚动画并执行刷新动作
   */
  private void startAnimating() {
    int whereToLoad = dp2px(onLoadCallBack.whereToLoad());
    final boolean toLoad;
    if (distanceY <= whereToLoad) {
      pullCancelAnimator = ValueAnimator.ofFloat(distanceY, 0);
      toLoad = false;
    } else {
      pullCancelAnimator = ValueAnimator.ofFloat(distanceY, whereToLoad);
      toLoad = true;
    }
    pullCancelAnimator.setDuration((long) (DEFAULT_BASE_ANIMATING_TIME_PER_100DP*px2dp(distanceY)/100));
    pullCancelAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    pullCancelAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        distanceY = (float) animation.getAnimatedValue();
        ViewCompat.postInvalidateOnAnimation(PullToRefreshListView.this);
      }
    });
    pullCancelAnimator.addListener(new Animator.AnimatorListener() {
      @Override
      public void onAnimationStart(Animator animation) {
      }
      @Override
      public void onAnimationEnd(Animator animation) {
        post(new Runnable() {
          @Override
          public void run() {
            pullCancelAnimator = null;
            if (toLoad) {
              onLoadCallBack.onLoad();
            }
          }
        });
      }
      @Override
      public void onAnimationCancel(Animator animation) {
        post(new Runnable() {
          @Override
          public void run() {
            pullCancelAnimator = null;
            if (toLoad) {
              onLoadCallBack.cancelLoad();
            }
          }
        });
      }
      @Override
      public void onAnimationRepeat(Animator animation) {
      }
    });
    pullCancelAnimator.start();
  }

我使用ValueAnimator来实现这一回滚动画,其中为ValueAnimator设置的回调中,在动画更新和动画结束以及动画取消中分别调用了OnLoadCallBack的3歌回调方法,从而实现PullToRefreshListView的下拉刷新动作。我们可以看到,onLoad方法是在UI线程执行的,因此如果在onLoad方法中执行耗时操作的话,需要在后台线程中操作,这与我们前面的解析是对应的。

7. 改进和问题

(1) 我们可以将onLoad回调修改成一个返回一个异步任务对象的方法,然后PullToRefreshListView在下拉结束后执行这个异步任务,因此我们就可以不需要cancelLoading回调了,直接就可以在PullToRefreshListView内部进行取消操作,这样做可以增强封装性,但相对目前的做法自由度就没有那么高了。

(2) 回滚动画应该也可以进行优化,具体怎么优化我也不清楚。。。各位朋友有好的想法可以在评论区提议一下,谢谢~

(3) 下拉的时候对多点触碰的响应并不完美,虽然也可以接受,但是做不到像qq客户端的聊天列表那样。

8. 源码

至此,我已经解析了如何实现一个下拉刷新列表,PullToRefreshListView的源码如下。

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.AbsListView;
import android.widget.ListView;
import com.ivan.healthcare.healthcare_android.log.L;
/**
 * 支持下拉刷新的的listView
 * Created by Ivan on 16/2/14.
 */
public class PullToRefreshListView extends ListView {
  private final String TAG = "PullToRefreshListView";
  private final int DEFAULT_BASE_ANIMATING_TIME_PER_100DP = 150;
  public static final int DEFAULT_WHERE_TO_LOAD = 80;
  private int lastAction = -1;
  private float pullStartY = -1;
  private boolean isTop = true;
  private float distanceY = 0;
  private boolean isPulling = false;
  private ValueAnimator pullCancelAnimator;
  private Context context;
  private Drawable refreshDrawable;
  private OnLoadCallBack OnLoadCallBack= new OnLoadCallBack() {
    @Override
    public int whereToLoad() {
      return DEFAULT_WHERE_TO_LOAD;
    }
    @Override
    public void onLoad() {
    }
    @Override
    public void cancelLoad() {
    }
    @Override
    public Drawable refreshDrawable() {
      return null;
    }
  };
  public PullToRefreshListView(Context context) {
    super(context);
    initView(context);
  }
  public PullToRefreshListView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView(context);
  }
  private void initView(Context context) {
    this.cOntext= context;
    setOnScrollListener(new OnScrollListener() {
      @Override
      public void onScrollStateChanged(AbsListView view, int scrollState) {
      }
      @Override
      public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        // 没有子view的时候(没有数据,或者被拉到看不到子view),意味着该listView滚动到顶部
        if (getChildCount() == 0) {
          isTop = true;
          return;
        }
        if (firstVisibleItem == 0) {
          View firstView = getChildAt(0);
          if (firstView.getTop() + distanceY >= 0) {
            // 第一个view可见且其相对parent(该listView)的顶部距离大于等于0,意味着该listView也是滚动到顶部
            isTop = true;
            return;
          }
        }
        isTop = false;
      }
    });
  }
  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    if (lastAction == -1 && ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
      // 按下的时候
      lastAction = MotionEvent.ACTION_DOWN;
      cancelAnimating();
      L.d(TAG, "touch down");
    } else if (lastAction == MotionEvent.ACTION_MOVE && ev.getActionMasked() == MotionEvent.ACTION_UP) {
      // 放开手指,开始回滚
      isPulling = false;
      lastAction = -1;
      startAnimating();
      L.d(TAG, "touch up");
    } else if (lastAction == MotionEvent.ACTION_DOWN) {
      if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
        // 在按下手指的基础上,开始滑动
        if (isTop && !isPulling) {
          // listView在顶部而且不处于下拉刷新状态,开始下拉
          pullStartY = ev.getY();
          lastAction = MotionEvent.ACTION_MOVE;
          isPulling = true;
        }
      }
    } else if (lastAction == MotionEvent.ACTION_MOVE) {
      if (isTop) {
        // 下拉
        distanceY = ev.getY() - pullStartY;
        L.d(TAG, distanceY + "");
        if (distanceY > 0) {
          distanceY = (float) (Math.exp(-ev.getY() / pullStartY / 40) * distanceY);
          // 在下拉状态时取消系统对move动作的响应,完全由本类响应
          ev.setAction(MotionEvent.ACTION_DOWN);
        } else {
          distanceY = 0;
          // 在下拉过程中往上拉动该listView使得其回到顶部位置,则将该move动作交由系统进行响应
          ev.setAction(MotionEvent.ACTION_MOVE);
        }
      } else {
        // 在下拉过程中往上拉动listView使listView往下滚动到其没有滚动到顶部,则取消其下拉状态,回到手指按下的初始状态
        lastAction = MotionEvent.ACTION_DOWN;
        isPulling = false;
        distanceY = 0;
      }
    }
    return super.onTouchEvent(ev);
  }
  @Override
  protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    if (distanceY > 0) {
      if (refreshDrawable == null) {
        refreshDrawable = onLoadCallBack.refreshDrawable();
      }
      if (refreshDrawable == null) {
        canvas.drawColor(Color.GRAY);
      } else {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        refreshDrawable.setBounds(left, top, getWidth()+left, getHeight()+top);
        refreshDrawable.draw(canvas);
      }
      canvas.save();
      canvas.translate(getPaddingLeft(), getPaddingTop() + distanceY);
      for (int i=0;i

以上这篇android 有阻尼下拉刷新列表的实现方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。


推荐阅读
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文讲述了如何通过代码在Android中更改Recycler视图项的背景颜色。通过在onBindViewHolder方法中设置条件判断,可以实现根据条件改变背景颜色的效果。同时,还介绍了如何修改底部边框颜色以及提供了RecyclerView Fragment layout.xml和项目布局文件的示例代码。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 【Windows】实现微信双开或多开的方法及步骤详解
    本文介绍了在Windows系统下实现微信双开或多开的方法,通过安装微信电脑版、复制微信程序启动路径、修改文本文件为bat文件等步骤,实现同时登录两个或多个微信的效果。相比于使用虚拟机的方法,本方法更简单易行,适用于任何电脑,并且不会消耗过多系统资源。详细步骤和原理解释请参考本文内容。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文讲述了作者通过点火测试男友的性格和承受能力,以考验婚姻问题。作者故意不安慰男友并再次点火,观察他的反应。这个行为是善意的玩人,旨在了解男友的性格和避免婚姻问题。 ... [详细]
author-avatar
小花姐姐
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有