- 安卓canvas无法触发bindtouchstart、bindtouchmove等事件
安卓canvas无法触发bindtouchstart、bindtouchmove、bindtouchend等事件导致Echarts无法弹出toolTip窗。
2019-12-19 - 腾讯位置服务GPS轨迹回放-安卓篇
前言 当我们使用地图进行开发时,利用已经录制好的轨迹进行轨迹回放来检查导航的准确性是十分常用的手段,并且上一篇已经讲完了关于地图使用时GPS轨迹文件的录制,现在对于安卓系统下使用腾讯导航SDK进行轨迹回放做一个分享 前期准备 腾讯导航SDK依赖于腾讯地图SDK、腾讯定位SDK,具体权限的开通需要去lbs.qq.com的官网控制台去操作,另外导航SDK的权限可以联系小助手咨询(如下图所示),这里就不多做探讨 [图片] 轨迹回放正片 系统架构 [图片] GPS回放系统分成两部分:GPSPlaybackActivity 和 GPSPlaybackEngine。 GPSPlayback负责和外界的交互,主要是信息的传递和导航SDK的交互,而GPSPlaybackEngine负责具体的读取文件和将定位点通过多线程runnable机制灌入listener。 开始轨迹回放 BaseNaviActivity.java baseNaviActivity 主要是对于导航SDK naviView部分的生命周期的管理,必须实现,否则不能进行导航! [代码] /** * 导航 SDK {@link CarNaviView} 初始化与周期管理类。 */ public abstract class BaseNaviActivity { private static Context mApplicationContext; protected CarNaviView mCarNaviView; // 建立了TencentCarNaviManager 单例模式,也可以直接调用TencentCarNaviManager来建立自己的carNaviManager public static final Singleton<TencentCarNaviManager> mCarManagerSingleton = new Singleton<TencentCarNaviManager>() { @Override protected TencentCarNaviManager create() { return new TencentCarNaviManager(mApplicationContext); } }; public static TencentCarNaviManager getCarNaviManager(Context appContext) { mApplicationContext = appContext; return mCarManagerSingleton.get(); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(getLayoutID()); super.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); mApplicationContext = getApplicationContext(); mCarNaviView = findViewById(R.id.tnk_car_navi_view); mCarManagerSingleton.get().addNaviView(mCarNaviView); } public int getLayoutID() { return R.layout.tnk_activity_navi_base; } protected View getCarNaviViewChaild() { final int count = mCarNaviView.getChildCount(); if (0 >= count) { return mCarNaviView; } return mCarNaviView.getChildAt(count - 1); } @Override protected void onDestroy() { super.onDestroy(); if (!isDestoryMap()) { return; } mCarManagerSingleton.get().removeAllNaviViews(); if (mCarNaviView != null) { mCarNaviView.onDestroy(); } // mCarManagerSingleton.destory(); } @Override protected void onStart() { super.onStart(); if (mCarNaviView != null) { mCarNaviView.onStart(); } } @Override protected void onRestart() { super.onRestart(); if (mCarNaviView != null) { mCarNaviView.onRestart(); } } @Override protected void onResume() { super.onResume(); if (mCarNaviView != null) { mCarNaviView.onResume(); } } @Override protected void onPause() { super.onPause(); if (mCarNaviView != null) { mCarNaviView.onPause(); } } @Override protected void onStop() { super.onStop(); if (mCarNaviView != null) { mCarNaviView.onStop(); } } protected boolean isDestoryMap() { return true; } } [代码] GPSPlaybackActivity.java 这一部分主要是对于导航 SDK的交互和添加导航UI部分初始化工作。注意导航sdk一定要先算路,再开始导航。算路可以取得GPS文件的首行为起点,末行为终点。 用到的fields [代码] private static final String LOG_TAG = "[GpsPlayback]"; // gps 文件路径 private String mGpsTrackPath; // gps 轨迹的起终点 private NaviPoi mFrom, mTo; // 是否是84坐标系 private boolean isLocation84 = true; [代码] 因为在GPSPlaybackEngine已经进行了listener监听,所以需要对于导航SDK进行灌点 [代码]// 腾讯定位sdk的listener private TencentLocationListener listener = new TencentLocationListener() { @Override public void onLocationChanged(TencentLocation tencentLocation, int error, String reason) { if (error != TencentLocation.ERROR_OK || tencentLocation == null) { return; } Log.d(LOG_TAG, "onLocationChanged : " + ", latitude" + tencentLocation.getLatitude() + ", longitude: " + tencentLocation.getLongitude() + ", provider: " + tencentLocation.getProvider() + ", accuracy: " + tencentLocation.getAccuracy()); // 将定位点灌入导航SDK // mCarManagerSingleton是使用导航SDK的carNaviManager创建的单例,开发者可以自己实现 mCarManagerSingleton.get().updateLocation(ConvertHelper .convertToGpsLocation(tencentLocation), error, reason); } @Override public void onStatusUpdate(String provider, int status, String description) { Log.d(LOG_TAG, "onStatusUpdate provider: " + provider + ", status: " + status + ", desc: " + description); // 更新GPS状态. mCarManagerSingleton.get().updateGpsStatus(provider, status, description); } }; [代码] onCreate方法初始化UI和添加callback [代码] @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 获取GPS文件轨迹路径,这里可以由开发者自己获取 mGpsTrackPath = getIntent().getStringExtra("gpsTrackPath"); if (mGpsTrackPath == null || mGpsTrackPath.isEmpty()) { return; } initUi(); addTencentCallback(); new Handler().post(() -> { // 目的获取每条轨迹的arraylist ArrayList<String> gpsLineStrs = readGpsFile(mGpsTrackPath); if (gpsLineStrs == null || gpsLineStrs.isEmpty()) { return; } // 获取起终点 getFromAndTo(gpsLineStrs); if (mFrom == null || mTo == null) { return; } final Handler handlerUi = new Handler(Looper.getMainLooper()); handlerUi.post(() -> searchAndStartNavigation()); }); } private void initUi() { mCarManagerSingleton.get().setInternalTtsEnabled(true); final int margin = CommonUtils.dip2px(this, 36); // 全览模式的路线边距 mCarNaviView.setVisibleRegionMargin(margin, margin, margin, margin); mCarNaviView.setAutoScaleEnabled(true); mCarManagerSingleton.get().setMulteRoutes(true); mCarNaviView.setNaviMapActionCallback(mCarManagerSingleton.get()); // 使用默认UI CarNaviInfoPanel carNaviInfoPanel = mCarNaviView.showNaviInfoPanel(); carNaviInfoPanel.setOnNaviInfoListener(() -> { mCarManagerSingleton.get().stopNavi(); finish(); }); CarNaviInfoPanel.NaviInfoPanelConfig config = new CarNaviInfoPanel.NaviInfoPanelConfig(); config.setRerouteViewEnable(true); // 重算按钮 carNaviInfoPanel.setNaviInfoPanelConfig(config); } private void addTencentCallback() { mCarManagerSingleton.get().addTencentNaviCallback(mTencentCallback); } private TencentNaviCallback mTencentCallback = new TencentNaviCallback() { @Override public void onStartNavi() { } @Override public void onStopNavi() { } @Override public void onOffRoute() { } @Override public void onRecalculateRouteSuccess(int recalculateType, ArrayList<RouteData> routeDataList) { } @Override public void onRecalculateRouteSuccessInFence(int recalculateType) { } @Override public void onRecalculateRouteFailure(int recalculateType, int errorCode, String errorMessage) { } @Override public void onRecalculateRouteStarted(int recalculateType) { } @Override public void onRecalculateRouteCanceled() { } @Override public int onVoiceBroadcast(NaviTts tts) { return 0; } @Override public void onArrivedDestination() { } @Override public void onPassedWayPoint(int passPointIndex) { } @Override public void onUpdateRoadType(int roadType) { } @Override public void onUpdateParallelRoadStatus(ParallelRoadStatus parallelRoadStatus) { } @Override public void onUpdateAttachedLocation(AttachedLocation location) { } @Override public void onFollowRouteClick(String routeId, ArrayList<LatLng> latLngArrayList) { } }; [代码] readGpsFile方法 [代码]private ArrayList<String> readGpsFile(String fileName) { ArrayList<String> gpsLineStrs = new ArrayList<>(); BufferedReader reader = null; try { File file = new File(fileName); InputStream is = new FileInputStream(file); reader = new BufferedReader(new InputStreamReader(is)); String line; while ((line = reader.readLine()) != null) { gpsLineStrs.add(line); } return gpsLineStrs; } catch (Exception e) { Log.e(LOG_TAG, "startMockTencentLocation Exception", e); e.printStackTrace(); } finally { try { if (reader != null) { reader.close(); } } catch (Exception e) { Log.e(LOG_TAG, "startMockTencentLocation Exception", e); e.printStackTrace(); } } return null; } [代码] getFromAndTo方法,获取起终点为进行算路 [代码] private void getFromAndTo(ArrayList<String> gpsLineStrs) { final int size; if ((size = gpsLineStrs.size()) < 2) { return; } final String firstLine = gpsLineStrs.get(0); final String endLine = gpsLineStrs.get(size - 1); try { final String[] fromParts = firstLine.split(","); mFrom = new NaviPoi(Double.valueOf(fromParts[1]), Double.valueOf(fromParts[0])); final String[] endParts = endLine.split(","); mTo = new NaviPoi(Double.valueOf(endParts[1]), Double.valueOf(endParts[0])); } catch (Exception e) { mFrom = null; mTo = null; } } [代码] 算路searchAndStartNavigation() [代码]可以使用导航SDK的算路方法并且获取算路成功和失败的回调 private void searchAndStartNavigation() { mCarManagerSingleton.get() .searchRoute(new TencentRouteSearchCallback() { @Override public void onRouteSearchFailure(int i, String s) { toast("路线规划失败"); } @Override public void onRouteSearchSuccess(ArrayList<RouteData> arrayList) { if (arrayList == null || arrayList.isEmpty()) { toast("未能召回路线"); return; } handleGpsPlayback(); } }); } [代码] 调用GpsPlaybackEngine方法,进行listen定位,然后开始导航 [代码] private void handleGpsPlayback() { // 与GpsPlaybackEngine 进行交互, 添加locationListener GpsPlaybackEngine.getInstance().addTencentLocationListener(listener); //与GpsPlaybackEngine 进行交互,开始定位 GpsPlaybackEngine.getInstance().startMockTencentLocation(mGpsTrackPath, isLocation84); try { mCarManagerSingleton.get().startNavi(0); } catch (Exception e) { toast(e.getMessage()); } } [代码] 结束导航 [代码] @Override protected void onDestroy() { // 与GpsPlaybackEngine 进行交互, removelocationListener mCarManagerSingleton.get().removeTencentNaviCallback(mTencentCallback); //与GpsPlaybackEngine 进行交互,结束定位GpsPlaybackEngine.getInstance().removeTencentLocationListener(listener); GpsPlaybackEngine.getInstance().stopMockLocation(); if (mCarManagerSingleton.get().isNavigating()) { // 结束导航 mCarManagerSingleton.get().stopNavi(); } super.onDestroy(); } [代码] GPSPlaybackEngine.java 这一部分主要是对于GPS文件进行读取并且提供外界可用的add/removelistener方法,start/stopMockLocation方法 因为要让engine运行在自己的线程,所以使用runnable机制 [代码]public class GpsPlaybackEngine implements Runnable{ // 代码在下方 } [代码] 而使用到的fields [代码]// Tencent轨迹Mock, TencentLocationListener需要利用腾讯定位SDK获取 private ArrayList<TencentLocationListener> mTencentLocationListeners = new ArrayList<>(); // 获取的location数据 private List<String> mDatas = new ArrayList<String>(); private boolean mIsReplaying = false; private boolean mIsMockTencentLocation = true; private Thread mMockGpsProviderTask = null; // 是否已经暂停 private boolean mPause = false; private double lastPointTime = 0; private double sleepTime = 0; [代码] 关键方法 listener相关 [代码] // 添加listener public void addTencentLocationListener(TencentLocationListener listener) { if (listener != null) { mTencentLocationListeners.add(listener); } } // 移除listener public void removeTencentLocationListener(TencentLocationListener listener) { if (listener != null) { mTencentLocationListeners.remove(listener); } } [代码] 开始/关闭模拟轨迹 [代码] /* * 模拟轨迹 * @param context * @param fileName 轨迹文件绝对路径 */ public void startMockTencentLocation(String fileName, boolean is84) { // 首先清除以前的data mDatas.clear(); // 判断是否是84坐标系 mIsMockTencentLocation = !is84; BufferedReader reader = null; try { File file = new File(fileName); InputStream is = new FileInputStream(file); reader = new BufferedReader(new InputStreamReader(is)); String line; while ((line = reader.readLine()) != null) { mDatas.add(line); } if (mDatas.size() > 0) { mIsReplaying = true; synchronized (this) { mPause = false; } // 开启异步线程 mMockGpsProviderTask = new Thread(this); mMockGpsProviderTask.start(); } } catch (Exception e) { Log.e(TAG, "startMockTencentLocation Exception", e); e.printStackTrace(); } finally { try { if (reader != null) { reader.close(); } } catch (Exception e) { Log.e(TAG, "startMockTencentLocation Exception", e); e.printStackTrace(); } } } [代码] [代码] /** * 退出应用前也需要调用停止模拟位置,否则手机的正常GPS定位不会恢复 */ public void stopMockTencentLocation() { try { mIsReplaying = false; mMockGpsProviderTask.join(); mMockGpsProviderTask = null; lastPointTime = 0; } catch (Exception e) { Log.e(TAG, "stopMockTencentLocation Exception", e); e.printStackTrace(); } } [代码] runnable相关 [代码] @Override public void run() { for (String line : mDatas) { if (!mIsReplaying) { Log.e(TAG, "stop gps replay"); break; } if (TextUtils.isEmpty(line)) { continue; } try { Thread.sleep(getSleepTime(line) * 1000); } catch (InterruptedException e) { e.printStackTrace(); } boolean mockResult; mockResult = mockTencentLocation(line); if (!mockResult) { break; } try { checkToPauseThread(); } catch (InterruptedException e) { e.printStackTrace(); } } } [代码] 使用到的private方法 [代码]private void checkToPauseThread() throws InterruptedException { synchronized (this) { while (mPause) { wait(); } } } private int getSleepTime(String line) { try { String[] parts = line.split(","); double time = Double.valueOf(parts[6]); time = (int) Math.floor(time); if(lastPointTime != 0) { sleepTime = time - lastPointTime; // 单位s,取整数 } lastPointTime = time; }catch (Exception e) { } return (int)sleepTime; } private boolean mockTencentLocation(String line) { try { String[] parts = line.split(","); double latitude = Double.valueOf(parts[1]); double longitude = Double.valueOf(parts[0]); float accuracy = Float.valueOf(parts[2]); float bearing = Float.valueOf(parts[3]); float speed = Float.valueOf(parts[4]); double altitude = Double.valueOf(parts[7]); double time = Double.valueOf(parts[6]); String buildingId; String floorName; if (parts.length >= 10) { buildingId = parts[8]; floorName = parts[9]; } else { buildingId = ""; floorName = ""; } if (!mIsMockTencentLocation) { double[] result = CoordinateConverter.wgs84togcj02(longitude, latitude); longitude = result[0]; latitude = result[1]; } GpsPlaybackEngine.MyTencentLocation location = new GpsPlaybackEngine.MyTencentLocation(); location.setProvider("gps"); location.setLongitude(longitude); location.setLatitude(latitude); location.setAccuracy(accuracy); location.setDirection(bearing); location.setVelocity(speed); location.setAltitude(altitude); location.setBuildingId(buildingId); location.setFloorName(floorName); location.setRssi(4); location.setTime(System.currentTimeMillis()); // location.setTime((long) time * 1000); for (TencentLocationListener listener : mTencentLocationListeners) { if (listener != null) { String curTime; if (location != null && location.getTime() != 0) { long millisecond = location.getTime(); Date date = new Date(millisecond); SimpleDateFormat format = new SimpleDateFormat("yyyy.MM.dd hh:mm:ss"); curTime = format.format(date); } else { curTime = "null"; } Log.e(TAG, "time : " + curTime + ", longitude : " + longitude + " , latitude : " + latitude); listener.onLocationChanged(location, 0, ""); listener.onStatusUpdate(LocationManager.GPS_PROVIDER, mMockGpsStatus, ""); } } } catch(Exception e) { Log.e(TAG, "Mock Location Exception", e); // 如果未开位置模拟,这里可能出异常 e.printStackTrace(); return false; } return true; } [代码] CoordinateConverter.wg84togcj02 [代码] /** * WGS84转GCJ02(火星坐标系) * * @param lng WGS84坐标系的经度 * @param lat WGS84坐标系的纬度 * @return 火星坐标数组 */ public static double[] wgs84togcj02(double lng, double lat) { if (out_of_china(lng, lat)) { return new double[] { lng, lat }; } double dlat = transformlat(lng - 105.0, lat - 35.0); double dlng = transformlng(lng - 105.0, lat - 35.0); double radlat = lat / 180.0 * pi; double magic = Math.sin(radlat); magic = 1 - ee * magic * magic; double sqrtmagic = Math.sqrt(magic); dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi); dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * pi); double mglat = lat + dlat; double mglng = lng + dlng; return new double[] { mglng, mglat }; } [代码] 内部类MyTencentLocation implements 定位sdk的接口 [代码]class MyTencentLocation implements TencentLocation { /** * 纬度 */ private double latitude = 0; /** * 经度 */ private double longitude = 0; /** * 精度 */ private float accuracy = 0; /** * gps方向 */ private float direction = -1; /** * 速度 */ private float velocity = 0; /** * 时间 */ private long time = 0; /** * 海拔高度 */ private double altitude = 0; /** * 定位来源 */ private String provider = ""; /** * GPS信号等级 */ private int rssi = 0; /** * 手机的机头方向 */ private float phoneDirection = -1; private String buildingId = ""; private String floorName = ""; private String fusionProvider = ""; @Override public String getProvider() { return provider; } @Override public String getSourceProvider() { return null; } @Override public String getFusionProvider() { return fusionProvider; } @Override public String getCityPhoneCode() { return null; } @Override public double getLatitude() { return latitude; } @Override public double getLongitude() { return longitude; } @Override public double getAltitude() { return latitude; } @Override public float getAccuracy() { return accuracy; } @Override public String getName() { return null; } @Override public String getAddress() { return null; } @Override public String getNation() { return null; } @Override public String getProvince() { return null; } @Override public String getCity() { return null; } @Override public String getDistrict() { return null; } @Override public String getTown() { return null; } @Override public String getVillage() { return null; } @Override public String getStreet() { return null; } @Override public String getStreetNo() { return null; } @Override public Integer getAreaStat() { return null; } @Override public List<TencentPoi> getPoiList() { return null; } @Override public float getBearing() { return direction; } @Override public float getSpeed() { return velocity; } @Override public long getTime() { return time; } @Override public long getElapsedRealtime() { return time; } @Override public int getGPSRssi() { return rssi; } @Override public String getIndoorBuildingId() { return buildingId; } @Override public String getIndoorBuildingFloor() { return floorName; } @Override public int getIndoorLocationType() { return 0; } @Override public double getDirection() { return phoneDirection; } @Override public String getCityCode() { return null; } @Override public TencentMotion getMotion() { return null; } @Override public int getGpsQuality() { return 0; } @Override public float getDeltaAngle() { return 0; } @Override public float getDeltaSpeed() { return 0; } @Override public int getCoordinateType() { return 0; } @Override public int getFakeReason() { return 0; } @Override public int isMockGps() { return 0; } @Override public Bundle getExtra() { return null; } @Override public int getInOutStatus() { return 0; } public void setLatitude(double latitude) { this.latitude = latitude; } public void setLongitude(double longitude) { this.longitude = longitude; } public void setAccuracy(float accuracy) { this.accuracy = accuracy; } public void setDirection(float direction) { this.direction = direction; } public void setVelocity(float velocity) { this.velocity = velocity; } public void setTime(long time) { this.time = time; } public void setAltitude(double altitude) { this.altitude = altitude; } public void setProvider(String provider) { this.provider = provider; } public void setFusionProvider(String fusionProvider) { this.fusionProvider = fusionProvider; } public void setRssi(int rssi) { this.rssi = rssi; } public void setPhoneDirection(float phoneDirection) { this.phoneDirection = phoneDirection; } public void setBuildingId(String buildingId) { this.buildingId = buildingId; } public void setFloorName(String floorName) { this.floorName = floorName; } } [代码] 效果展示 最终根据已经录制好的轨迹(具体录制方法可以参见上期腾讯位置服务轨迹录制-安卓篇),从中国技术交易大厦到北京西站的gps轨迹进行回放,并通过导航sdk进行展示如下 [图片]
2021-06-04 - 微信小程序输入框光标位置移动
在部分ios手机上面 输入框组件进行数据双向绑定 此时当光标在中间的时候,输入文字光标会跳到内容后面 子组件代码 [图片] [图片] 父组件代码 [图片]
2018-04-11 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 小程序 URL Link,安卓是否不识别?
获取打开小程序任意页面的 URL Link(https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/url-link.html) 安卓现在是不支持吗,接收不到短信
2021-06-02 - textara 单词换行被折断,同样的样式,在view中单词正常显示
textarea bug: 在微信开发工具、安卓环境下,textarea 中输入超过1行英文,行末尾的一个英文单词断在两行显示; 同样的样式,在view中,英文单词没有断行显示。 希望尽快给出答复。 .writing_textarea { width: 100%; box-sizing: border-box; word-break: keep-all; word-wrap: break-word; white-space: pre-wrap; } 微信开发工具中的截图: [图片]
2021-06-02 - 小程序自动判断/切换开发环境、正式环境的方案
前段时间我也碰到了这个问题: https://developers.weixin.qq.com/community/develop/doc/0004cc6fe543405bb0882c1b156800 开发环境和正式环境的接口地址是不一样的,同样的代码,不用修改任何代码,就能自动访问对应的接口地址。 示例代码: [代码]let host = ''; let NODE_ENV = 'pro'; if( NODE_ENV === 'pro' ){ host = 'https://pro.qq.com'; }else{ host = 'https://dev.qq.com'; } [代码] 一般情况下大家都会这么写,那么NODE_ENV只能手动修改,官方没有给出当前的环境变量,我们只能自己想办法了。 我的灵感来自于“构建npm”功能,当安装了插件并构建npm之后,点上传代码,“node_modules”文件夹是不会上传的。 后来我搜索了文档,project.config.json配置项中的packOptions.ignore字段,用以配置打包时对符合指定规则的文件或文件夹进行忽略,以跳过打包的过程,这些文件或文件夹将不会出现在预览或上传的结果内。 好了,方案来了,我们可以在根目录新建一个文件,local.txt,然后配置它打包时不上传。那么我们可以添加如下代码: [代码]const fileManager = wx.getFileSystemManager(); try{ fileManager.accessSync('/local.txt'); NODE_ENV = 'dev'; }catch(e){} [代码] 这样就可以了,因为线上包中没有local.txt这个文件,那么NODE_ENV=‘pro’,本地有这个文件,NODE_ENV=‘dev’。 完整代码: [代码]let host = ''; let NODE_ENV = 'pro'; const fileManager = wx.getFileSystemManager(); try{ fileManager.accessSync('/local.txt'); NODE_ENV = 'dev'; }catch(e){} if( NODE_ENV === 'pro' ){ host = 'https://pro.qq.com'; }else{ host = 'https://dev.qq.com'; } [代码] over PS: 还有的同学需要区分PC和手机,这个也比较简单,使用:wx.getSystemInfoSync();获取系统信息,如果是PC端的开发者工具,则platform = “devtools”
2019-12-02 - 小程序测试环境和生产环境如何区分?
小程序测试环境和生产环境如何区分
2021-01-05 - PC小程序怎么调试呢?
视频不能播放,想看下请求referer是否跟移动端小程序一致,还有一些七七八八的UI错位问题,现在有调试方法吗?
2019-09-18 - 小程序中 rpx、px、pt 单位之间的比例测量
为表达方便,下面都以屏幕宽度为基准度量值 首先,屏幕宽度 = 750 rpx 接下来关于 px,这个不同手机可能不同,开发者可调用 wx.getSystemInfo 接口,取里面的 windowWidth 或 screenWidth(这两个值一般来说是相同的),取到的值就是屏幕宽度的 px 值 最后关于 pt,我找了 Android 和 iOS 的几台手机分别测试了一下,统计出在各个机型上都满足 1 px = 0.75 pt 比如我的魅族p6p,屏幕宽度 = 750 rpx = 360 px = 270 pt 再比如 iPhone 7 Plus,屏幕宽度 = 750 rpx = 414 px = 310.5 pt 这个就和小程序设计规范里认为的 1 px = 0.5 pt 不一致了,做设计稿和标注时需要注意这一点
2018-06-13 - 小程序音频使用的wx.createInnerAudioContext(),如何能够实现熄屏继续播放?
[图片][图片]
2020-06-24 - H5如何跳转微信小程序?
之前遇到一个需求,就是要从H5跳转到小程序里,但是微信之前一直没有提供接口做跳转,我们只能做降级方案,在要跳转小程序的地方做了一个弹窗,弹窗里面放小程序码,引导用户长按识别小程序码,然后跳转到小程序内,整个流程非常之长,转化率可想而知也是很低的。 今天刚好看到有人技术群里面问了这个问题,于是我就去看了下微信的文档,发现微信偷偷的更新的这个接口,可以让微信浏览器下的H5跳转到小程序内。 相关文档在这边: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html 用的是JS-SDK的接口,需要使用到js-sdk-1.6.0的版本才有支持,https://res.wx.qq.com/open/js/jweixin-1.6.0.js wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印 appId: '', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '',// 必填,签名 jsApiList: [], // 必填,需要使用的JS接口列表 openTagList: [] // 可选,需要使用的开放标签列表,例如['wx-open-launch-app'] }); 在wx.config下面多了一项openTagList,开放标签列表,目前支持配置wx-open-launch-weapp,wx-open-launch-app wx-open-launch-weapp 指H5跳转小程序 wx-open-launch-app 指H5跳转app 我们主要介绍的是wx-open-launch-weapp H5跳转小程序 先上才艺: [图片][图片][图片] html代码如下: var btn = document.getElementById('launch-btn'); btn.addEventListener('launch', function (e) { console.log('success'); }); btn.addEventListener('error', function (e) { console.log('fail', e.detail); }); username为小程序的原始id,path对应的是小程序的链接地址。之前有写过微信H5的应该会知道怎么把这段代码嵌入到之前的代码里面。 目前此功能仅开放给已认证的服务号,网页的域名要在服务号的“JS接口安全域名”下。 亲测<wx-open-launch-weapp>可以跳转到任意合法合规的小程序,是任意的小程序都能跳转!!!!这个接口真开放(不怕人干坏事?) PS: 有个坑,官方文件说的path是/a/b/c?d=1&e=2#fg,类似的这样的链接格式,但是我自己亲测如果直接使用/a/b/c?d=1&e=2#fg这样格式的链接会报页面不存在,然后我想到了小程序那边复制链接的时候会在链接后面加上.html,于是挖槽的事情发生了,把path链接格式换成/a/b/c.html?d=1&e=2#fg这样就能正常访问,不知道是微信故意这样设计的还是bug,有待考证。 然后这个接口真的可以干好多坏事,希望大家能用正确的价值观来正确使用此接口。 微信开放标签有最低的微信版本要求,以及最低的系统版本要求。 如果开发过程中出现以下情况的,要确认一下,微信版本要求为:7.0.12及以上。 系统版本要求为:iOS 10.3及以上、Android 5.0及以上。 [图片]
2020-07-09 - 静态网站 H5 跳小程序文档错误
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/staticstorage/jump-miniprogram.html 下面的示例代码是错误的,无法拉起小程序 <wx-open-launch-weapp id="weapp" weappid="小程序 AppID" path="/pages/index/index"> <template> <button style="width: 200px; height: 45px; line-height: 45px; text-align: center; font-size: 17px; border-radius: 22.5px; color:cornflowerblue;">拉起小程序</button> </template> </wx-open-launch-weapp> 按照如下文档的描述,要加上username属性才可以拉起小程序 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html
2020-11-25 - 静态网站H5跳小程序 开发文档HTML示例 是错误的?
开发文档地址 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/staticstorage/jump-miniprogram.html 错误1 自己绑定到静态网站的域名并没有得到免签! 例子: 1.系统分配的域名 errMSg会提示OK。 测试地址:https://ces-9gcyrt1h94533724-1304326639.tcloudbaseapp.com/a0.html 2.自己绑定域名 errMSg会提示 invalid signature。测试地址:https://ces.2ll.co/a0.html 确保该域名已经解析和绑定上了! [图片] [图片] 错误2. [图片] 错误3. jsApiList 改成 jsApiList:['onMenuShareQQ'] 这样 errMSg只会提示OK 而打开小程序按钮不会显示。 测试地址:https://ces-9gcyrt1h94533724-1304326639.tcloudbaseapp.com/a0.html [图片] 尝试调整1 依旧错误! 把 <wx-open-launch-weapp id="weapp" weappid="wx9711e24e7248eb62" path="/pages/index/index"> 改成 <wx-open-launch-weapp id="weapp" username="gh_71e37c67736f" path="/pages/index/index.html"> 这样 errMSg也是只会提示OK 而打开小程序按钮不会显示。 测试地址:https://ces-9gcyrt1h94533724-1304326639.tcloudbaseapp.com/a2.html 尝试调整2 依旧错误! 把wx-open-launch-weapp的html改成 <div> <wx-open-launch-weapp style="width:200px;background-color: #000;position: fixed " id="launch-btn" username="gh_71e37c67736f" path="/pages/index/index.html"> <script type="text/wxtag-template"> <style> .btn { width: 100%; height: 50px; font-size: 16px; border: none; background-color: #000; color: #003c8a; } </style> <button id="sdassad" class="btn">点击这里下载素材</button> </script> </wx-open-launch-weapp> </div> 依旧只提示OK 并不显示按钮 但该写法使用JSSDK 是正常使用的 JSSDK例子:https://qqa55.aizwy.cn/tiao2.html 测试地址:https://ces-9gcyrt1h94533724-1304326639.tcloudbaseapp.com/a1.html 希望早日能得到解决 并且把错误的开发文档更正。这样误导不少人。
2020-11-27